diff --git a/website/build.py b/website/build.py
index 1e5585c9..9d278f7d 100644
--- a/website/build.py
+++ b/website/build.py
@@ -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",
)
diff --git a/website/templates/base.html b/website/templates/base.html
index 2b219bd4..72f246e1 100644
--- a/website/templates/base.html
+++ b/website/templates/base.html
@@ -53,6 +53,7 @@
gtag("js", new Date());
gtag("config", "G-0LMLYE0HER");
+ {% block extra_head %}{% endblock %}
Skip to content
diff --git a/website/templates/index.html b/website/templates/index.html
index c76cd7c7..bf2abd21 100644
--- a/website/templates/index.html
+++ b/website/templates/index.html
@@ -1,4 +1,7 @@
{% extends "base.html" %}
+{% block extra_head %}
+
+{% endblock %}
{% block header %}
diff --git a/website/tests/test_build.py b/website/tests/test_build.py
index cc56342d..841ea414 100644
--- a/website/tests/test_build.py
+++ b/website/tests/test_build.py
@@ -471,6 +471,53 @@ class TestBuild:
assert 'id="hero-category-heading">Browse by category' 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 = '", start)
+ block = html[start:end]
+ assert "" 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