mirror of
https://github.com/vinta/awesome-python.git
synced 2026-05-09 11:13:01 +08:00
feat(website): add CollectionPage JSON-LD to category, group, and subcategory pages
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+43
-11
@@ -118,14 +118,11 @@ 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"
|
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:
|
WEBSITE_ID = f"{SITE_URL}#website"
|
||||||
description = (
|
|
||||||
"An opinionated guide to the best Python frameworks, libraries, and tools. "
|
|
||||||
f"Explore {len(entries)} curated projects across {total_categories} categories, "
|
def _item_list_payload(entries: Sequence[TemplateEntry]) -> dict:
|
||||||
"from AI and agents to data science and web development."
|
return {
|
||||||
)
|
|
||||||
website_id = f"{SITE_URL}#website"
|
|
||||||
item_list = {
|
|
||||||
"@type": "ItemList",
|
"@type": "ItemList",
|
||||||
"numberOfItems": len(entries),
|
"numberOfItems": len(entries),
|
||||||
"itemListElement": [
|
"itemListElement": [
|
||||||
@@ -138,12 +135,20 @@ def build_homepage_json_ld(entries: Sequence[TemplateEntry], total_categories: i
|
|||||||
for i, entry in enumerate(entries, start=1)
|
for i, entry in enumerate(entries, start=1)
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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."
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@graph": [
|
"@graph": [
|
||||||
{
|
{
|
||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
"@id": website_id,
|
"@id": WEBSITE_ID,
|
||||||
"name": "Awesome Python",
|
"name": "Awesome Python",
|
||||||
"url": SITE_URL,
|
"url": SITE_URL,
|
||||||
},
|
},
|
||||||
@@ -153,13 +158,30 @@ def build_homepage_json_ld(entries: Sequence[TemplateEntry], total_categories: i
|
|||||||
"name": "Awesome Python",
|
"name": "Awesome Python",
|
||||||
"url": SITE_URL,
|
"url": SITE_URL,
|
||||||
"description": description,
|
"description": description,
|
||||||
"isPartOf": {"@id": website_id},
|
"isPartOf": {"@id": WEBSITE_ID},
|
||||||
"mainEntity": item_list,
|
"mainEntity": _item_list_payload(entries),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def category_meta_description(name: str, entry_count: int, description: str) -> str:
|
||||||
|
suffix = description if description else "Part of the Awesome Python catalog."
|
||||||
|
return f"Explore {entry_count} curated Python projects in {name}. {suffix}"
|
||||||
|
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def category_path(category: ParsedSection) -> str:
|
def category_path(category: ParsedSection) -> str:
|
||||||
return f"/categories/{category['slug']}/"
|
return f"/categories/{category['slug']}/"
|
||||||
|
|
||||||
@@ -458,6 +480,15 @@ def build(repo_root: Path) -> None:
|
|||||||
group_categories: Sequence[ParsedSection] | None = None,
|
group_categories: Sequence[ParsedSection] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
page_dir.mkdir(parents=True, exist_ok=True)
|
page_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
category_description = category_meta_description(
|
||||||
|
category["name"], len(entries), category["description"]
|
||||||
|
)
|
||||||
|
category_json_ld = json.dumps(
|
||||||
|
build_category_json_ld(
|
||||||
|
category["name"], category_url, category_description, entries
|
||||||
|
),
|
||||||
|
ensure_ascii=False,
|
||||||
|
).replace("</", "<\\/")
|
||||||
(page_dir / "index.html").write_text(
|
(page_dir / "index.html").write_text(
|
||||||
tpl_category.render(
|
tpl_category.render(
|
||||||
category=category,
|
category=category,
|
||||||
@@ -470,6 +501,7 @@ def build(repo_root: Path) -> None:
|
|||||||
filter_urls_json=filter_urls_json,
|
filter_urls_json=filter_urls_json,
|
||||||
parent_category=parent_category,
|
parent_category=parent_category,
|
||||||
group_categories=group_categories,
|
group_categories=group_categories,
|
||||||
|
category_json_ld=category_json_ld,
|
||||||
),
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
{% block description %}Explore {{ entries | length }} curated Python projects in {{ category.name }}. {% if category.description %}{{ category.description }}{% else %}Part of the Awesome Python catalog.{% endif %}{% endblock %}
|
{% block description %}Explore {{ entries | length }} curated Python projects in {{ category.name }}. {% if category.description %}{{ category.description }}{% else %}Part of the Awesome Python catalog.{% endif %}{% endblock %}
|
||||||
{% block canonical_url %}{{ category_url }}{% endblock %}
|
{% block canonical_url %}{{ category_url }}{% endblock %}
|
||||||
{% block alternate_links %}{% endblock %}
|
{% block alternate_links %}{% endblock %}
|
||||||
|
{% block extra_head %}
|
||||||
|
<script type="application/ld+json">{{ category_json_ld | safe }}</script>
|
||||||
|
{% endblock %}
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<header class="category-hero">
|
<header class="category-hero">
|
||||||
<div class="hero-sheen" aria-hidden="true"></div>
|
<div class="hero-sheen" aria-hidden="true"></div>
|
||||||
|
|||||||
@@ -518,6 +518,88 @@ class TestBuild:
|
|||||||
assert rendered_names == {e["name"] for e in entries}
|
assert rendered_names == {e["name"] for e in entries}
|
||||||
assert rendered_urls == {e["url"] for e in entries}
|
assert rendered_urls == {e["url"] for e in entries}
|
||||||
|
|
||||||
|
def test_category_page_contains_json_ld(self, tmp_path):
|
||||||
|
readme = textwrap.dedent("""\
|
||||||
|
# Awesome Python
|
||||||
|
|
||||||
|
Intro.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Tools**
|
||||||
|
|
||||||
|
## Widgets
|
||||||
|
|
||||||
|
_Widget libraries._
|
||||||
|
|
||||||
|
- [w1](https://example.com/w1) - A widget.
|
||||||
|
- [w2](https://github.com/owner/w2) - A starred widget.
|
||||||
|
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
Help!
|
||||||
|
""")
|
||||||
|
(tmp_path / "README.md").write_text(readme, encoding="utf-8")
|
||||||
|
self._copy_real_templates(tmp_path)
|
||||||
|
build(tmp_path)
|
||||||
|
|
||||||
|
category_html = (tmp_path / "website" / "output" / "categories" / "widgets" / "index.html").read_text(encoding="utf-8")
|
||||||
|
marker = '<script type="application/ld+json">'
|
||||||
|
assert marker in category_html
|
||||||
|
start = category_html.index(marker) + len(marker)
|
||||||
|
end = category_html.index("</script>", start)
|
||||||
|
block = category_html[start:end]
|
||||||
|
assert "</script>" not in block
|
||||||
|
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"}
|
||||||
|
|
||||||
|
item_list = data["mainEntity"]
|
||||||
|
assert item_list["@type"] == "ItemList"
|
||||||
|
assert item_list["numberOfItems"] == 2
|
||||||
|
names = {item["name"] for item in item_list["itemListElement"]}
|
||||||
|
urls = {item["url"] for item in item_list["itemListElement"]}
|
||||||
|
assert names == {"w1", "w2"}
|
||||||
|
assert urls == {"https://example.com/w1", "https://github.com/owner/w2"}
|
||||||
|
positions = sorted(item["position"] for item in item_list["itemListElement"])
|
||||||
|
assert positions == [1, 2]
|
||||||
|
|
||||||
|
def test_group_page_falls_back_to_default_description_in_json_ld(self, tmp_path):
|
||||||
|
readme = textwrap.dedent("""\
|
||||||
|
# T
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**AI & ML**
|
||||||
|
|
||||||
|
## Deep Learning
|
||||||
|
|
||||||
|
- [dl1](https://example.com/dl1) - DL.
|
||||||
|
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
Done.
|
||||||
|
""")
|
||||||
|
self._copy_real_templates(tmp_path)
|
||||||
|
(tmp_path / "README.md").write_text(readme, encoding="utf-8")
|
||||||
|
build(tmp_path)
|
||||||
|
|
||||||
|
group_html = (tmp_path / "website" / "output" / "categories" / "ai-ml" / "index.html").read_text(encoding="utf-8")
|
||||||
|
marker = '<script type="application/ld+json">'
|
||||||
|
start = group_html.index(marker) + len(marker)
|
||||||
|
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."
|
||||||
|
|
||||||
def test_build_creates_subcategory_pages(self, tmp_path):
|
def test_build_creates_subcategory_pages(self, tmp_path):
|
||||||
readme = textwrap.dedent("""\
|
readme = textwrap.dedent("""\
|
||||||
# T
|
# T
|
||||||
|
|||||||
Reference in New Issue
Block a user