diff --git a/website/build.py b/website/build.py index 28cd7107..e3dd66b4 100644 --- a/website/build.py +++ b/website/build.py @@ -226,13 +226,10 @@ def build_homepage_json_ld(entries: Sequence[TemplateEntry], total_categories: i def category_meta_title(name: str, parent_name: str | None = None) -> str: if parent_name: - title = f"{name} for {parent_name} - Awesome Python" - if len(title) <= 60: - return title - title = f"{parent_name}: {name} - Awesome Python" - if len(title) <= 60: - return title - return f"{name} - Awesome Python" + # Lead with the section's title so the page matches searches like "python jwt library". + title = f"{category_meta_title(parent_name).removesuffix(' - Awesome Python')}: {name}" + # Google shows the site name above each result, so a long title drops it rather than get cut off. + return f"{title} - Awesome Python" if len(title) <= 43 else title # Names ending in one of these nouns already say what the entries are. noun = "" if name.rsplit(" ", 1)[-1] in PLURAL_NOUNS else " Libraries" prefix = "" if name.startswith("Python ") else "Python " @@ -242,15 +239,19 @@ def category_meta_title(name: str, parent_name: str | None = None) -> str: return f"{name} - Awesome Python" -def category_meta_description(name: str, entry_count: int, description: str, parent_name: str | None = None) -> str: - target = f"{name} for {parent_name}" if parent_name else name - count_sentence = f"Explore {entry_count} curated Python projects in {target}." +def category_meta_description(name: str, entry_count: int, description: str) -> str: + count_sentence = f"Explore {entry_count} curated Python project{'s' if entry_count != 1 else ''} in {name}." if description: lead = description if description.endswith((".", "!", "?")) else f"{description}." return f"{lead} {count_sentence}" return f"{count_sentence} Part of the Awesome Python catalog." +def subcategory_meta_description(name: str, parent_name: str, entry_names: Sequence[str]) -> str: + names = entry_names[0] if len(entry_names) == 1 else f"{', '.join(entry_names[:-1])}{',' if len(entry_names) > 2 else ''} and {entry_names[-1]}" + return f"The {name} picks in Awesome Python's {parent_name} list: {names}." + + def load_category_intro(path: Path) -> tuple[str, str, str, str]: """Render a category intro file to HTML, split at the end of its "How to choose:" list. @@ -775,7 +776,12 @@ def build(repo_root: Path) -> None: parent_name = parent_category["name"] if parent_category else None category_title = category_meta_title(category["name"], parent_name) intro_html, guide_html, intro_lead, _ = load_category_intro(website / "data" / "category_intros" / f"{current_path.removeprefix('/categories/').strip('/')}.md") - category_description = intro_lead or category_meta_description(category["name"], len(entries), category["description"], parent_name) + if intro_lead: + category_description = intro_lead + elif parent_name: + category_description = subcategory_meta_description(category["name"], parent_name, [e["name"] for e in entries]) + else: + category_description = category_meta_description(category["name"], len(entries), category["description"]) breadcrumbs = [("Awesome Python", SITE_URL)] if parent_category: breadcrumbs.append((parent_category["name"], category_public_url(parent_category))) diff --git a/website/tests/test_build.py b/website/tests/test_build.py index 5e1df4eb..b60ef26d 100644 --- a/website/tests/test_build.py +++ b/website/tests/test_build.py @@ -794,7 +794,7 @@ class TestBuild: assert collection["name"] == "Python AI & ML Libraries" assert collection["@id"] == "https://awesome-python.com/categories/ai-ml/" assert collection["url"] == "https://awesome-python.com/categories/ai-ml/" - assert collection["description"] == "Explore 1 curated Python projects in AI & ML. Part of the Awesome Python catalog." + assert collection["description"] == "Explore 1 curated Python project in AI & ML. Part of the Awesome Python catalog." def test_category_title_skips_libraries_after_plural_noun(self, tmp_path): readme = textwrap.dedent("""\ @@ -904,14 +904,14 @@ class TestBuild: parser = HeadMetadataParser() parser.feed(sync) - assert parser.title.strip() == "Synchronous for Web Frameworks - Awesome Python" - assert parser.meta_by_name["description"] == "Explore 1 curated Python projects in Synchronous for Web Frameworks. Part of the Awesome Python catalog." + assert parser.title.strip() == "Python Web Frameworks: Synchronous - Awesome Python" + assert parser.meta_by_name["description"] == "The Synchronous picks in Awesome Python's Web Frameworks list: django." marker = '", start) graph = {node["@type"]: node for node in json.loads(sync[start:end])["@graph"]} - assert graph["CollectionPage"]["name"] == "Synchronous for Web Frameworks" + assert graph["CollectionPage"]["name"] == "Python Web Frameworks: Synchronous" assert graph["BreadcrumbList"]["itemListElement"] == [ {"@type": "ListItem", "position": 1, "name": "Awesome Python", "item": "https://awesome-python.com/"}, {