mirror of
https://github.com/vinta/awesome-python.git
synced 2026-05-06 06:06:45 -04:00
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:
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user