mirror of
https://github.com/vinta/awesome-python.git
synced 2026-05-07 14:17:36 -05:00
fix(seo): align JSON-LD with Yoast/RankMath conventions
- Wrap category pages in a self-contained @graph (WebSite + CollectionPage)
- Set canonical @id on CollectionPage to its URL (no hash fragment)
- Expand isPartOf to typed object {"@type": "WebSite", "@id": ...}
- Extract _website_node() and ISPARTOF_WEBSITE constants to avoid repetition
- Update tests to assert @graph structure on category pages
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -119,6 +119,16 @@ def build_robots_txt() -> str:
|
||||
|
||||
|
||||
WEBSITE_ID = f"{SITE_URL}#website"
|
||||
ISPARTOF_WEBSITE = {"@type": "WebSite", "@id": WEBSITE_ID}
|
||||
|
||||
|
||||
def _website_node() -> dict:
|
||||
return {
|
||||
"@type": "WebSite",
|
||||
"@id": WEBSITE_ID,
|
||||
"name": "Awesome Python",
|
||||
"url": SITE_URL,
|
||||
}
|
||||
|
||||
|
||||
def _item_list_payload(entries: Sequence[TemplateEntry]) -> dict:
|
||||
@@ -146,19 +156,14 @@ def build_homepage_json_ld(entries: Sequence[TemplateEntry], total_categories: i
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "WebSite",
|
||||
"@id": WEBSITE_ID,
|
||||
"name": "Awesome Python",
|
||||
"url": SITE_URL,
|
||||
},
|
||||
_website_node(),
|
||||
{
|
||||
"@type": "CollectionPage",
|
||||
"@id": f"{SITE_URL}#collectionpage",
|
||||
"@id": SITE_URL,
|
||||
"name": "Awesome Python",
|
||||
"url": SITE_URL,
|
||||
"description": description,
|
||||
"isPartOf": {"@id": WEBSITE_ID},
|
||||
"isPartOf": ISPARTOF_WEBSITE,
|
||||
"mainEntity": _item_list_payload(entries),
|
||||
},
|
||||
],
|
||||
@@ -173,12 +178,18 @@ def category_meta_description(name: str, entry_count: int, description: str) ->
|
||||
def build_category_json_ld(name: str, url: str, description: str, entries: Sequence[TemplateEntry]) -> dict:
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "CollectionPage",
|
||||
"name": f"{name} Python Libraries",
|
||||
"url": url,
|
||||
"description": description,
|
||||
"isPartOf": {"@id": WEBSITE_ID},
|
||||
"mainEntity": _item_list_payload(entries),
|
||||
"@graph": [
|
||||
_website_node(),
|
||||
{
|
||||
"@type": "CollectionPage",
|
||||
"@id": url,
|
||||
"name": f"{name} Python Libraries",
|
||||
"url": url,
|
||||
"description": description,
|
||||
"isPartOf": ISPARTOF_WEBSITE,
|
||||
"mainEntity": _item_list_payload(entries),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -496,10 +496,12 @@ class TestBuild:
|
||||
assert set(graph) == {"WebSite", "CollectionPage"}
|
||||
assert graph["WebSite"]["url"] == "https://awesome-python.com/"
|
||||
assert graph["WebSite"]["name"] == "Awesome Python"
|
||||
assert graph["WebSite"]["@id"] == "https://awesome-python.com/#website"
|
||||
|
||||
collection = graph["CollectionPage"]
|
||||
assert collection["@id"] == "https://awesome-python.com/"
|
||||
assert collection["url"] == "https://awesome-python.com/"
|
||||
assert collection["isPartOf"]["@id"] == graph["WebSite"]["@id"]
|
||||
assert collection["isPartOf"] == {"@type": "WebSite", "@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
|
||||
|
||||
@@ -553,13 +555,17 @@ class TestBuild:
|
||||
data = json.loads(block)
|
||||
|
||||
assert data["@context"] == "https://schema.org"
|
||||
assert data["@type"] == "CollectionPage"
|
||||
assert data["name"] == "Widgets Python Libraries"
|
||||
assert data["url"] == "https://awesome-python.com/categories/widgets/"
|
||||
assert data["description"] == "Explore 2 curated Python projects in Widgets. Widget libraries."
|
||||
assert data["isPartOf"] == {"@id": "https://awesome-python.com/#website"}
|
||||
graph = {node["@type"]: node for node in data["@graph"]}
|
||||
assert set(graph) == {"WebSite", "CollectionPage"}
|
||||
assert graph["WebSite"]["@id"] == "https://awesome-python.com/#website"
|
||||
collection = graph["CollectionPage"]
|
||||
assert collection["name"] == "Widgets Python Libraries"
|
||||
assert collection["@id"] == "https://awesome-python.com/categories/widgets/"
|
||||
assert collection["url"] == "https://awesome-python.com/categories/widgets/"
|
||||
assert collection["description"] == "Explore 2 curated Python projects in Widgets. Widget libraries."
|
||||
assert collection["isPartOf"] == {"@type": "WebSite", "@id": "https://awesome-python.com/#website"}
|
||||
|
||||
item_list = data["mainEntity"]
|
||||
item_list = collection["mainEntity"]
|
||||
assert item_list["@type"] == "ItemList"
|
||||
assert item_list["numberOfItems"] == 2
|
||||
names = {item["name"] for item in item_list["itemListElement"]}
|
||||
@@ -595,10 +601,12 @@ class TestBuild:
|
||||
end = group_html.index("</script>", start)
|
||||
data = json.loads(group_html[start:end])
|
||||
|
||||
assert data["@type"] == "CollectionPage"
|
||||
assert data["name"] == "AI & ML Python Libraries"
|
||||
assert data["url"] == "https://awesome-python.com/categories/ai-ml/"
|
||||
assert data["description"] == "Explore 1 curated Python projects in AI & ML. Part of the Awesome Python catalog."
|
||||
graph = {node["@type"]: node for node in data["@graph"]}
|
||||
collection = graph["CollectionPage"]
|
||||
assert collection["name"] == "AI & ML Python 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."
|
||||
|
||||
def test_build_creates_subcategory_pages(self, tmp_path):
|
||||
readme = textwrap.dedent("""\
|
||||
|
||||
Reference in New Issue
Block a user