Skip to content

Conversation

@kattni
Copy link
Collaborator

@kattni kattni commented Nov 3, 2025

Migration from Lektor to MkDocs

This PR is the initial step of the migration from Lektor to MkDocs. We have opted to land this as a proof of concept, and intend to finalise the rest of the tasks in subsequent PRs.

Following an audit of the existing site, there have been some changes to content structure.

TODOs for @freakboy3742

  • Update GitHub sponsors sponsorship levels.
    • Update Sponsor page to reflect updated levels (or leave this for me once you've completed the first step).
  • Vet the text in community/index.md under Where is the BeeWare community?
  • Update about/branding.md to include formal guidelines for asset usage.
  • The primary index page layout is overall wider than the rest of the site because that was the only way to include the sidebar within the main content, and have the text be the same 45rem wide that we have for the other text on the site. Look at this and see if it works for you.

Known outstanding tasks before we can go live

  • Translations:
    • Merge existing translation PO files with updated POT file.
    • Ensure translation workflow is present in the repo and working properly.
    • Initial translation pass through DeepL to get machine translation.
    • Set up Read the Docs translations for every language.
    • Set up Weblate:
      • Add website repo
      • Identify strings that do not need to be presented for translation.
  • Branding guidelines:
    - Complete set of branding guidelines.
    - Update BeeWare logos on branding page to latest.
  • Header anchors:
    - Add explicit anchors to any headers being linked from elsewhere to avoid failures when translating headers.
  • rumdl:
    • Work through issues to get rumdl passing
    • Ensure any rules that are conflicting with MkDocs syntax are disabled in pyproject.toml rumdl configuration
  • Redirects:
    • Sort out comprehensive list of necessary redirects
    • Add list to configuration.
  • Docs linting:
    • Enable Markdown link checker and PySpelling
    • Get both passing
  • Code linting:
    • Set up ruff to lint the Python scripts present in this repo
  • .readthedocs.yaml
    • Remove --build-with-warnings
  • BeeWare Docs Tools PR #150
    • Land this PR before everything goes live.
  • pyproject.toml
    • Restore beeware-docs-tools main branch
  • tox.ini
    • Restore full suite of translation commands
    • Remove --build-with-warnings where necessary
    • Enable linting tools (as noted above)
  • DNS updates
    • Point beeware.org to Read the Docs

PR Checklist:

  • All new features have been tested
  • All new features have been documented
  • I have read the CONTRIBUTING.md file
  • I will abide by the code of conduct

Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post generation script looks great! This would work great as-is; a couple of suggestions inline about ways to simplify the code to make long-term maintenance easier.

Comment on lines 12 to 18
def validate_url(url):
"""
Validates a URL.

NOTE: CHECK YOUR ENTRY. There are edge cases where this will pass with an invalid entry.
"""
try:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some type annotations and elaboration on what it does do would be helpful here:

Suggested change
def validate_url(url):
"""
Validates a URL.
NOTE: CHECK YOUR ENTRY. There are edge cases where this will pass with an invalid entry.
"""
try:
def validate_url(url: str) -> bool:
"""Validates a URL.
NOTE: CHECK YOUR ENTRY. This does a simple HEAD request check, but only if
you are connected to the internet. There are edge cases where this will
succeed with an invalid entry.
"""
try:

return False
else:
parsing_validated = False
while not parsing_validated:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is a loop needed here? Since both branches of the if have returns, this can only run through once AFAICT.

Comment on lines 52 to 55
if choice in post_types:
post_type = post_types[choice]
else:
print("Invalid; you must choose a number from the list.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is entirely valid Python; however it might make more sense to write it as a try/except pair (as "invalid" really is the exception):

Suggested change
if choice in post_types:
post_type = post_types[choice]
else:
print("Invalid; you must choose a number from the list.")
try:
post_type = post_types[choice]
except KeyError:
print("Invalid; you must choose a number from the list.")

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer necessary; this code was refactored out.

Comment on lines 63 to 66
date = datetime.date.today()
return {
"title": blog_title,
"date": date,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't incorrect; but if we're only using date once, we might as well use it inline.

Suggested change
date = datetime.date.today()
return {
"title": blog_title,
"date": date,
return {
"title": blog_title,
"date": datetime.date.today(),

Comment on lines 73 to 78
event_name = input("Event name: ")
event_url = None
while event_url is None:
event_url_entry = input("Event URL: ")
if validate_url(event_url_entry):
event_url = event_url_entry
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some blank lines could aid readability here, making it clear that there are logical "blocks" of activity.

Suggested change
event_name = input("Event name: ")
event_url = None
while event_url is None:
event_url_entry = input("Event URL: ")
if validate_url(event_url_entry):
event_url = event_url_entry
event_name = input("Event name: ")
event_url = None
while event_url is None:
event_url_entry = input("Event URL: ")
if validate_url(event_url_entry):
event_url = event_url_entry

Comment on lines 87 to 94
valid_event_end_date = False
while not valid_event_end_date:
try:
event_end_date_input = input("Event end date (e.g. 2026-01-01; leave blank if same as event start date): ") or event_start_date_input
event_end_date = datetime.datetime.strptime(event_end_date_input, "%Y-%m-%d").date()
valid_event_end_date = True
except ValueError:
print("Invalid date format. Must be YYYY-DD-MM format.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic here is almost identical to the logic for start date. it would be worth pulling out input_date() as a method that takes a prompt (and optionally, a default value), so that the usage here is event_end_date = input_date("Event end date ...:", event_start_date)

You could do similar refactoring for input_url(); you could possibly even go as far as input_choice where there's a "select item from dictionary" logic.

return content


class NoAliasDumper(yaml.SafeDumper):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be worth adding a comment here to document what "nonstandard" behavior we're encoding here:

  • Disabling aliases (YAML's behavior of factoring out any repeated values as constants that are referenced)
  • Ensuring multiline-strings are output with | syntax

Comment on lines 261 to 262
Path(Path(__file__).parent.parent / f"docs/en/news/posts/{filename_metadata["date"].year}/{filename_metadata["categories"][0].lower()}").mkdir(parents=True, exist_ok=True)
return Path(__file__).parent.parent / f"docs/en/news/posts/{filename_metadata["date"].year}/{filename_metadata["categories"][0].lower()}" / filename
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pathname here is (a) long and (b) shared between these two lines. It would be worth constructing that pathname as a variable (which also gives an opportunity to break the path over multiple lines of code), and then using that variable twice.

if filename.is_file():
print("Post already exists.")
else:
content = yaml.dump(metadata, Dumper=NoAliasDumper, sort_keys=False, width=9999)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth documenting inline why the two extra keys are needed.

Comment on lines 280 to 285
payload = dedent("""\
Add blog post introduction here.

<!-- more -->

Add blog post content here.""")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it helpful to indent "dedented" strings as if it were code - that is, at one level deeper than the surrounding statements. That makes the "scope" of the string clearer:

Suggested change
payload = dedent("""\
Add blog post introduction here.
<!-- more -->
Add blog post content here.""")
payload = dedent("""\
Add blog post introduction here.
<!-- more -->
Add blog post content here."""
)

@kattni kattni changed the title Initial build of website; homepage. Initial step of migration of beeware.org from Lektor to MkDocs Jan 23, 2026
@kattni kattni requested a review from freakboy3742 January 23, 2026 01:43
@kattni kattni marked this pull request as ready for review January 23, 2026 02:16
Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome! I've pushed the requested content updates, and some tweaks to the macros and blog script, but this is an incredible effort. Thanks so much - can't wait to see this go live!

Comment on lines 139 to 141
further_involvement = input("Is the team involved in another way? (y/N): ") or "N"
if further_involvement in ["N", "n", "no"]:
break
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified a little, and made more robust; if you check further_involvement[0].upper() == "Y"`, you get a boolean that is only true if you write something that starts with "Y" or "y". Enter, or any variant on "no" will return false.

Comment on lines 12 to 14
def validate_url(url: str) -> bool:
"""
Validates a URL.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a minor style thing where PyCharm's default is different; but I've always thought docstrings should be on the line with the quotes.

Suggested change
def validate_url(url: str) -> bool:
"""
Validates a URL.
def validate_url(url: str) -> bool:
"""Validates a URL.

# verification when an internet connection is unavailable.
parsing_url = urlparse(url)
if parsing_url.scheme in ["http", "https"] and parsing_url.netloc != "":
print("URL is the correct format, however it has not been validated. Verify it on post creation.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a couple of line-length issues and minor formatting things that I think will get picked up by a ruff pass.

def generate_file_path(filename_metadata):
file_path = f"{re.sub(r"[^\w ]", "", filename_metadata["title"]).lower().replace(" ", "-")}.md"
docs_path = f"docs/en/news/posts/{filename_metadata["date"].year}/{filename_metadata["categories"][0].lower()}"
Path(Path(__file__).parent.parent / docs_path).mkdir(parents=True, exist_ok=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. There's no need to wrap this in Path if it's already a Path object;
  2. Since the full .parent.parent/docs_path is what is being re-used, we might as well factor the parent.parent part into docs_path.



def generate_entry(metadata, payload):
filename = generate_file_path(metadata)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is only used once, so it's not really worth factoring out.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a user ergonomics thing - making this script scripts/new_post.py, and putting a __init__.py in the folder means you can run it as python -m scripts.new_post.

docs/macros.py Outdated
Comment on lines 87 to 109
organizing_introduction = None
attending_introduction = None
find_us = None
for inv in involvement:
if inv["type"] == "organizing":
organizing_introduction = dedent(f"""\
{attendees(authors, team)} will be organizing [{event.name}]({event.url}), which will happen {event_timeframe}!\n\n
""")
elif inv["type"] == "attending" or "attending" not in inv["type"]:
attending_introduction = dedent(f"""\
{attendees(authors, team)} will be attending [{event.name}]({event.url}) {event_timeframe}!\n\n
""")
else:
find_us = f"You can find us throughout {event.name}:\n\n"

if organizing_introduction:
content.append(organizing_introduction)
if attending_introduction:
content.append(attending_introduction)
content.append("\n<!-- more -->\n")
content.append(f"{event.description}\n\n")
if find_us:
content.append(find_us)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this block can be simplified a little by pre-computing the list of all involvements, and checking membership of that list.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of ruff/line length issues in this file.

@kattni kattni merged commit c78a350 into beeware:main Jan 23, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants