From 32ae78fd9606b1b74d1e8f7098897014f66e38c9 Mon Sep 17 00:00:00 2001 From: Vinta Chen Date: Sun, 7 Jun 2026 03:08:43 +0800 Subject: [PATCH] fix: improve website SEO metadata --- website/build.py | 92 +++++++++++++++++++++++++++--- website/templates/category.html | 2 +- website/templates/sponsorship.html | 5 +- website/tests/test_build.py | 78 ++++++++++++++++++++++++- 4 files changed, 167 insertions(+), 10 deletions(-) diff --git a/website/build.py b/website/build.py index bcfc64d..347c4fb 100644 --- a/website/build.py +++ b/website/build.py @@ -28,6 +28,7 @@ BUILTIN_PUBLIC_URL = f"{SITE_URL}categories/{BUILTIN_SLUG}/" SPONSORSHIP_PATH = "/sponsorship/" SPONSORSHIP_PUBLIC_URL = f"{SITE_URL}sponsorship/" +SPONSORSHIP_DESCRIPTION = "Sponsorship for awesome-python: tiers, audience, and how to get your product in front of professional Python developers evaluating tools for production use." SOURCE_TYPE_DOMAINS = { "docs.python.org": "Built-in", @@ -128,6 +129,8 @@ def _website_node() -> dict: "@id": WEBSITE_ID, "name": "Awesome Python", "url": SITE_URL, + "inLanguage": "en", + "sameAs": "https://github.com/vinta/awesome-python", } @@ -164,21 +167,59 @@ def build_homepage_json_ld(entries: Sequence[TemplateEntry], total_categories: i "url": SITE_URL, "description": description, "isPartOf": ISPARTOF_WEBSITE, + "inLanguage": "en", "mainEntity": _item_list_payload(entries), }, ], } -def category_meta_description(name: str, entry_count: int, description: str) -> str: - count_sentence = f"Explore {entry_count} curated Python projects in {name}." +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" + title = f"{name} Python Libraries - Awesome Python" + if len(title) <= 60: + return title + 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}." 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 build_category_json_ld(name: str, url: str, description: str, entries: Sequence[TemplateEntry]) -> dict: +def build_breadcrumb_json_ld(items: Sequence[tuple[str, str]]) -> dict: + return { + "@type": "BreadcrumbList", + "itemListElement": [ + { + "@type": "ListItem", + "position": i, + "name": name, + "item": url, + } + for i, (name, url) in enumerate(items, start=1) + ], + } + + +def build_category_json_ld( + name: str, + url: str, + description: str, + entries: Sequence[TemplateEntry], + breadcrumbs: Sequence[tuple[str, str]], +) -> dict: return { "@context": "https://schema.org", "@graph": [ @@ -186,12 +227,38 @@ def build_category_json_ld(name: str, url: str, description: str, entries: Seque { "@type": "CollectionPage", "@id": url, - "name": f"{name} Python Libraries", + "name": name, "url": url, "description": description, "isPartOf": ISPARTOF_WEBSITE, + "inLanguage": "en", "mainEntity": _item_list_payload(entries), }, + build_breadcrumb_json_ld(breadcrumbs), + ], + } + + +def build_sponsorship_json_ld() -> dict: + return { + "@context": "https://schema.org", + "@graph": [ + _website_node(), + { + "@type": "WebPage", + "@id": SPONSORSHIP_PUBLIC_URL, + "name": "Sponsor Awesome Python", + "url": SPONSORSHIP_PUBLIC_URL, + "description": SPONSORSHIP_DESCRIPTION, + "isPartOf": ISPARTOF_WEBSITE, + "inLanguage": "en", + }, + build_breadcrumb_json_ld( + [ + ("Awesome Python", SITE_URL), + ("Sponsorship", SPONSORSHIP_PUBLIC_URL), + ] + ), ], } @@ -548,14 +615,21 @@ def build(repo_root: Path) -> None: group_categories: Sequence[ParsedSection] | None = None, ) -> None: page_dir.mkdir(parents=True, exist_ok=True) - category_description = category_meta_description(category["name"], len(entries), category["description"]) + parent_name = parent_category["name"] if parent_category else None + category_title = category_meta_title(category["name"], parent_name) + category_description = category_meta_description(category["name"], len(entries), category["description"], parent_name) + breadcrumbs = [("Awesome Python", SITE_URL)] + if parent_category: + breadcrumbs.append((parent_category["name"], category_public_url(parent_category))) + breadcrumbs.append((category["name"], category_url)) category_json_ld = json.dumps( - build_category_json_ld(category["name"], category_url, category_description, entries), + build_category_json_ld(category_title.removesuffix(" - Awesome Python"), category_url, category_description, entries, breadcrumbs), ensure_ascii=False, ).replace(" None: hero_stats.append(f"{repo_stars}+ stars on GitHub") hero_stats.append(f"Updated {build_date.strftime('%B %d, %Y')}") (sponsorship_dir / "index.html").write_text( - tpl_sponsorship.render(hero_stats=hero_stats), + tpl_sponsorship.render( + hero_stats=hero_stats, + sponsorship_description=SPONSORSHIP_DESCRIPTION, + sponsorship_json_ld=json.dumps(build_sponsorship_json_ld(), ensure_ascii=False).replace("{{ sponsorship_json_ld | safe }} +{% endblock %} {% block header %}
diff --git a/website/tests/test_build.py b/website/tests/test_build.py index caada78..5649f29 100644 --- a/website/tests/test_build.py +++ b/website/tests/test_build.py @@ -569,7 +569,7 @@ class TestBuild: assert data["@context"] == "https://schema.org" graph = {node["@type"]: node for node in data["@graph"]} - assert set(graph) == {"WebSite", "CollectionPage"} + assert set(graph) == {"WebSite", "CollectionPage", "BreadcrumbList"} assert graph["WebSite"]["@id"] == "https://awesome-python.com/#website" collection = graph["CollectionPage"] assert collection["name"] == "Widgets Python Libraries" @@ -588,6 +588,12 @@ class TestBuild: positions = sorted(item["position"] for item in item_list["itemListElement"]) assert positions == [1, 2] + breadcrumbs = graph["BreadcrumbList"]["itemListElement"] + assert breadcrumbs == [ + {"@type": "ListItem", "position": 1, "name": "Awesome Python", "item": "https://awesome-python.com/"}, + {"@type": "ListItem", "position": 2, "name": "Widgets", "item": "https://awesome-python.com/categories/widgets/"}, + ] + def test_group_page_falls_back_to_default_description_in_json_ld(self, tmp_path): readme = textwrap.dedent("""\ # T @@ -685,9 +691,79 @@ class TestBuild: assert "

Synchronous

" in sync assert "category-breadcrumb" in sync + 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." + + 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["BreadcrumbList"]["itemListElement"] == [ + {"@type": "ListItem", "position": 1, "name": "Awesome Python", "item": "https://awesome-python.com/"}, + { + "@type": "ListItem", + "position": 2, + "name": "Web Frameworks", + "item": "https://awesome-python.com/categories/web-frameworks/", + }, + { + "@type": "ListItem", + "position": 3, + "name": "Synchronous", + "item": "https://awesome-python.com/categories/web-frameworks/synchronous/", + }, + ] + parent = (site / "categories" / "web-frameworks" / "index.html").read_text(encoding="utf-8") assert "category-breadcrumb" not in parent + def test_sponsorship_page_contains_json_ld(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + ## Projects + + **Tools** + + ## Widgets + + - [w1](https://example.com/w1) - A widget. + + # Contributing + + Done. + """) + self._copy_real_templates(tmp_path) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + build(tmp_path) + + site = tmp_path / "website" / "output" + html = (site / "sponsorship" / "index.html").read_text(encoding="utf-8") + parser = HeadMetadataParser() + parser.feed(html) + + assert parser.title.strip() == "Sponsor Awesome Python" + assert parser.meta_by_name["description"] == ( + "Sponsorship for awesome-python: tiers, audience, and how to get your product in front of professional Python developers evaluating tools for production use." + ) + assert parser.links_by_rel["canonical"] == "https://awesome-python.com/sponsorship/" + + marker = '", start) + graph = {node["@type"]: node for node in json.loads(html[start:end])["@graph"]} + + assert set(graph) == {"WebSite", "WebPage", "BreadcrumbList"} + assert graph["WebPage"]["@id"] == "https://awesome-python.com/sponsorship/" + assert graph["WebPage"]["url"] == "https://awesome-python.com/sponsorship/" + assert graph["BreadcrumbList"]["itemListElement"] == [ + {"@type": "ListItem", "position": 1, "name": "Awesome Python", "item": "https://awesome-python.com/"}, + {"@type": "ListItem", "position": 2, "name": "Sponsorship", "item": "https://awesome-python.com/sponsorship/"}, + ] + def test_index_embeds_filter_urls_json(self, tmp_path): readme = textwrap.dedent("""\ # T