feat(website): add homepage JSON-LD with WebSite, CollectionPage, ItemList for SEO/AEO

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Vinta Chen
2026-05-03 19:18:15 +08:00
parent 138059feeb
commit b2910d59c8
4 changed files with 98 additions and 0 deletions
+47
View File
@@ -118,6 +118,48 @@ def build_robots_txt() -> str:
return f"User-agent: *\nContent-Signal: search=yes, ai-input=yes, ai-train=yes\nAllow: /\n\nSitemap: {SITEMAP_URL}\n"
def build_homepage_json_ld(entries: Sequence[TemplateEntry], total_categories: int) -> dict:
description = (
"An opinionated guide to the best Python frameworks, libraries, and tools. "
f"Explore {len(entries)} curated projects across {total_categories} categories, "
"from AI and agents to data science and web development."
)
website_id = f"{SITE_URL}#website"
item_list = {
"@type": "ItemList",
"numberOfItems": len(entries),
"itemListElement": [
{
"@type": "ListItem",
"position": i,
"name": entry["name"],
"url": entry["url"],
}
for i, entry in enumerate(entries, start=1)
],
}
return {
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebSite",
"@id": website_id,
"name": "Awesome Python",
"url": SITE_URL,
},
{
"@type": "CollectionPage",
"@id": f"{SITE_URL}#collectionpage",
"name": "Awesome Python",
"url": SITE_URL,
"description": description,
"isPartOf": {"@id": website_id},
"mainEntity": item_list,
},
],
}
def category_path(category: ParsedSection) -> str:
return f"/categories/{category['slug']}/"
@@ -378,6 +420,10 @@ def build(repo_root: Path) -> None:
site_dir.mkdir(parents=True)
filter_urls_json = json.dumps(filter_urls, sort_keys=True, ensure_ascii=False).replace("</", "<\\/")
homepage_json_ld = json.dumps(
build_homepage_json_ld(entries, len(categories)),
ensure_ascii=False,
).replace("</", "<\\/")
tpl_index = env.get_template("index.html")
(site_dir / "index.html").write_text(
@@ -393,6 +439,7 @@ def build(repo_root: Path) -> None:
category_urls=category_urls,
filter_urls=filter_urls,
filter_urls_json=filter_urls_json,
homepage_json_ld=homepage_json_ld,
),
encoding="utf-8",
)
+1
View File
@@ -53,6 +53,7 @@
gtag("js", new Date());
gtag("config", "G-0LMLYE0HER");
</script>
{% block extra_head %}{% endblock %}
</head>
<body>
<a href="#content" class="skip-link">Skip to content</a>
+3
View File
@@ -1,4 +1,7 @@
{% extends "base.html" %}
{% block extra_head %}
<script type="application/ld+json">{{ homepage_json_ld | safe }}</script>
{% endblock %}
{% block header %}
<header class="hero">
<div class="hero-sheen" aria-hidden="true"></div>
+47
View File
@@ -471,6 +471,53 @@ class TestBuild:
assert 'id="hero-category-heading">Browse by category</h2>' in html
assert 'class="hero-category-link" href="/categories/ai-and-agents/"' in html
def test_index_contains_homepage_json_ld(self, tmp_path):
readme = (Path(__file__).parents[2] / "README.md").read_text(encoding="utf-8")
(tmp_path / "README.md").write_text(readme, encoding="utf-8")
self._copy_real_templates(tmp_path)
build(tmp_path)
parsed_groups = parse_readme(readme)
categories = [cat for group in parsed_groups for cat in group["categories"]]
entries = extract_entries(categories, parsed_groups)
html = (tmp_path / "website" / "output" / "index.html").read_text(encoding="utf-8")
marker = '<script type="application/ld+json">'
assert marker in html
start = html.index(marker) + len(marker)
end = html.index("</script>", start)
block = html[start:end]
assert "</script>" not in block
data = json.loads(block)
assert data["@context"] == "https://schema.org"
graph = {node["@type"]: node for node in data["@graph"]}
assert set(graph) == {"WebSite", "CollectionPage"}
assert graph["WebSite"]["url"] == "https://awesome-python.com/"
assert graph["WebSite"]["name"] == "Awesome Python"
collection = graph["CollectionPage"]
assert collection["url"] == "https://awesome-python.com/"
assert collection["isPartOf"]["@id"] == graph["WebSite"]["@id"]
expected_description = f"An opinionated guide to the best Python frameworks, libraries, and tools. Explore {len(entries)} curated projects across {len(categories)} categories, from AI and agents to data science and web development."
assert collection["description"] == expected_description
item_list = collection["mainEntity"]
assert item_list["@type"] == "ItemList"
assert item_list["numberOfItems"] == len(entries)
assert len(item_list["itemListElement"]) == len(entries)
positions = [item["position"] for item in item_list["itemListElement"]]
assert positions == list(range(1, len(entries) + 1))
assert all(item["@type"] == "ListItem" for item in item_list["itemListElement"])
assert all(item["url"].startswith(("http://", "https://")) for item in item_list["itemListElement"])
rendered_names = {item["name"] for item in item_list["itemListElement"]}
rendered_urls = {item["url"] for item in item_list["itemListElement"]}
assert rendered_names == {e["name"] for e in entries}
assert rendered_urls == {e["url"] for e in entries}
def test_build_creates_subcategory_pages(self, tmp_path):
readme = textwrap.dedent("""\
# T