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(" 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 %} 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