mirror of
https://github.com/vinta/awesome-python.git
synced 2026-06-11 18:57:12 +00:00
fix: improve website SEO metadata
This commit is contained in:
parent
9f156de2b4
commit
32ae78fd96
4 changed files with 167 additions and 10 deletions
|
|
@ -28,6 +28,7 @@ BUILTIN_PUBLIC_URL = f"{SITE_URL}categories/{BUILTIN_SLUG}/"
|
|||
|
||||
SPONSORSHIP_PATH = "/sponsorship/"
|
||||
SPONSORSHIP_PUBLIC_URL = f"{SITE_URL}sponsorship/"
|
||||
SPONSORSHIP_DESCRIPTION = "Sponsorship for awesome-python: tiers, audience, and how to get your product in front of professional Python developers evaluating tools for production use."
|
||||
|
||||
SOURCE_TYPE_DOMAINS = {
|
||||
"docs.python.org": "Built-in",
|
||||
|
|
@ -128,6 +129,8 @@ def _website_node() -> dict:
|
|||
"@id": WEBSITE_ID,
|
||||
"name": "Awesome Python",
|
||||
"url": SITE_URL,
|
||||
"inLanguage": "en",
|
||||
"sameAs": "https://github.com/vinta/awesome-python",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -164,21 +167,59 @@ def build_homepage_json_ld(entries: Sequence[TemplateEntry], total_categories: i
|
|||
"url": SITE_URL,
|
||||
"description": description,
|
||||
"isPartOf": ISPARTOF_WEBSITE,
|
||||
"inLanguage": "en",
|
||||
"mainEntity": _item_list_payload(entries),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def category_meta_description(name: str, entry_count: int, description: str) -> str:
|
||||
count_sentence = f"Explore {entry_count} curated Python projects in {name}."
|
||||
def category_meta_title(name: str, parent_name: str | None = None) -> str:
|
||||
if parent_name:
|
||||
title = f"{name} for {parent_name} - Awesome Python"
|
||||
if len(title) <= 60:
|
||||
return title
|
||||
title = f"{parent_name}: {name} - Awesome Python"
|
||||
if len(title) <= 60:
|
||||
return title
|
||||
return f"{name} - Awesome Python"
|
||||
title = f"{name} Python Libraries - Awesome Python"
|
||||
if len(title) <= 60:
|
||||
return title
|
||||
return f"{name} - Awesome Python"
|
||||
|
||||
|
||||
def category_meta_description(name: str, entry_count: int, description: str, parent_name: str | None = None) -> str:
|
||||
target = f"{name} for {parent_name}" if parent_name else name
|
||||
count_sentence = f"Explore {entry_count} curated Python projects in {target}."
|
||||
if description:
|
||||
lead = description if description.endswith((".", "!", "?")) else f"{description}."
|
||||
return f"{lead} {count_sentence}"
|
||||
return f"{count_sentence} Part of the Awesome Python catalog."
|
||||
|
||||
|
||||
def build_category_json_ld(name: str, url: str, description: str, entries: Sequence[TemplateEntry]) -> dict:
|
||||
def build_breadcrumb_json_ld(items: Sequence[tuple[str, str]]) -> dict:
|
||||
return {
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": i,
|
||||
"name": name,
|
||||
"item": url,
|
||||
}
|
||||
for i, (name, url) in enumerate(items, start=1)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def build_category_json_ld(
|
||||
name: str,
|
||||
url: str,
|
||||
description: str,
|
||||
entries: Sequence[TemplateEntry],
|
||||
breadcrumbs: Sequence[tuple[str, str]],
|
||||
) -> dict:
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
|
|
@ -186,12 +227,38 @@ def build_category_json_ld(name: str, url: str, description: str, entries: Seque
|
|||
{
|
||||
"@type": "CollectionPage",
|
||||
"@id": url,
|
||||
"name": f"{name} Python Libraries",
|
||||
"name": name,
|
||||
"url": url,
|
||||
"description": description,
|
||||
"isPartOf": ISPARTOF_WEBSITE,
|
||||
"inLanguage": "en",
|
||||
"mainEntity": _item_list_payload(entries),
|
||||
},
|
||||
build_breadcrumb_json_ld(breadcrumbs),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def build_sponsorship_json_ld() -> dict:
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
_website_node(),
|
||||
{
|
||||
"@type": "WebPage",
|
||||
"@id": SPONSORSHIP_PUBLIC_URL,
|
||||
"name": "Sponsor Awesome Python",
|
||||
"url": SPONSORSHIP_PUBLIC_URL,
|
||||
"description": SPONSORSHIP_DESCRIPTION,
|
||||
"isPartOf": ISPARTOF_WEBSITE,
|
||||
"inLanguage": "en",
|
||||
},
|
||||
build_breadcrumb_json_ld(
|
||||
[
|
||||
("Awesome Python", SITE_URL),
|
||||
("Sponsorship", SPONSORSHIP_PUBLIC_URL),
|
||||
]
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
|
|
@ -548,14 +615,21 @@ def build(repo_root: Path) -> None:
|
|||
group_categories: Sequence[ParsedSection] | None = None,
|
||||
) -> None:
|
||||
page_dir.mkdir(parents=True, exist_ok=True)
|
||||
category_description = category_meta_description(category["name"], len(entries), category["description"])
|
||||
parent_name = parent_category["name"] if parent_category else None
|
||||
category_title = category_meta_title(category["name"], parent_name)
|
||||
category_description = category_meta_description(category["name"], len(entries), category["description"], parent_name)
|
||||
breadcrumbs = [("Awesome Python", SITE_URL)]
|
||||
if parent_category:
|
||||
breadcrumbs.append((parent_category["name"], category_public_url(parent_category)))
|
||||
breadcrumbs.append((category["name"], category_url))
|
||||
category_json_ld = json.dumps(
|
||||
build_category_json_ld(category["name"], category_url, category_description, entries),
|
||||
build_category_json_ld(category_title.removesuffix(" - Awesome Python"), category_url, category_description, entries, breadcrumbs),
|
||||
ensure_ascii=False,
|
||||
).replace("</", "<\\/")
|
||||
(page_dir / "index.html").write_text(
|
||||
tpl_category.render(
|
||||
category=category,
|
||||
category_title=category_title,
|
||||
category_url=category_url,
|
||||
category_description=category_description,
|
||||
entries=entries,
|
||||
|
|
@ -607,7 +681,11 @@ def build(repo_root: Path) -> None:
|
|||
hero_stats.append(f"{repo_stars}+ stars on GitHub")
|
||||
hero_stats.append(f"Updated {build_date.strftime('%B %d, %Y')}")
|
||||
(sponsorship_dir / "index.html").write_text(
|
||||
tpl_sponsorship.render(hero_stats=hero_stats),
|
||||
tpl_sponsorship.render(
|
||||
hero_stats=hero_stats,
|
||||
sponsorship_description=SPONSORSHIP_DESCRIPTION,
|
||||
sponsorship_json_ld=json.dumps(build_sponsorship_json_ld(), ensure_ascii=False).replace("</", "<\\/"),
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}{{ category.name }} Python Libraries - Awesome Python{% endblock %}
|
||||
{% block title %}{{ category_title }}{% endblock %}
|
||||
{% block description %}{{ category_description }}{% endblock %}
|
||||
{% block canonical_url %}{{ category_url }}{% endblock %}
|
||||
{% block alternate_links %}{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Sponsor Awesome Python{% endblock %}
|
||||
{% block description %}Sponsorship for awesome-python: tiers, audience, and how to get your product in front of professional Python developers evaluating tools for production use.{% endblock %}
|
||||
{% block description %}{{ sponsorship_description }}{% endblock %}
|
||||
{% block canonical_url %}https://awesome-python.com/sponsorship/{% endblock %}
|
||||
{% block alternate_links %}{% endblock %}
|
||||
{% block extra_head %}
|
||||
<script type="application/ld+json">{{ sponsorship_json_ld | safe }}</script>
|
||||
{% endblock %}
|
||||
{% block header %}
|
||||
<header class="category-hero sponsorship-hero">
|
||||
<div class="hero-sheen" aria-hidden="true"></div>
|
||||
|
|
|
|||
|
|
@ -569,7 +569,7 @@ class TestBuild:
|
|||
|
||||
assert data["@context"] == "https://schema.org"
|
||||
graph = {node["@type"]: node for node in data["@graph"]}
|
||||
assert set(graph) == {"WebSite", "CollectionPage"}
|
||||
assert set(graph) == {"WebSite", "CollectionPage", "BreadcrumbList"}
|
||||
assert graph["WebSite"]["@id"] == "https://awesome-python.com/#website"
|
||||
collection = graph["CollectionPage"]
|
||||
assert collection["name"] == "Widgets Python Libraries"
|
||||
|
|
@ -588,6 +588,12 @@ class TestBuild:
|
|||
positions = sorted(item["position"] for item in item_list["itemListElement"])
|
||||
assert positions == [1, 2]
|
||||
|
||||
breadcrumbs = graph["BreadcrumbList"]["itemListElement"]
|
||||
assert breadcrumbs == [
|
||||
{"@type": "ListItem", "position": 1, "name": "Awesome Python", "item": "https://awesome-python.com/"},
|
||||
{"@type": "ListItem", "position": 2, "name": "Widgets", "item": "https://awesome-python.com/categories/widgets/"},
|
||||
]
|
||||
|
||||
def test_group_page_falls_back_to_default_description_in_json_ld(self, tmp_path):
|
||||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
|
|
@ -685,9 +691,79 @@ class TestBuild:
|
|||
assert "<h1>Synchronous</h1>" in sync
|
||||
assert "category-breadcrumb" in sync
|
||||
|
||||
parser = HeadMetadataParser()
|
||||
parser.feed(sync)
|
||||
assert parser.title.strip() == "Synchronous for Web Frameworks - Awesome Python"
|
||||
assert parser.meta_by_name["description"] == "Explore 1 curated Python projects in Synchronous for Web Frameworks. Part of the Awesome Python catalog."
|
||||
|
||||
marker = '<script type="application/ld+json">'
|
||||
start = sync.index(marker) + len(marker)
|
||||
end = sync.index("</script>", start)
|
||||
graph = {node["@type"]: node for node in json.loads(sync[start:end])["@graph"]}
|
||||
assert graph["CollectionPage"]["name"] == "Synchronous for Web Frameworks"
|
||||
assert graph["BreadcrumbList"]["itemListElement"] == [
|
||||
{"@type": "ListItem", "position": 1, "name": "Awesome Python", "item": "https://awesome-python.com/"},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "Web Frameworks",
|
||||
"item": "https://awesome-python.com/categories/web-frameworks/",
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 3,
|
||||
"name": "Synchronous",
|
||||
"item": "https://awesome-python.com/categories/web-frameworks/synchronous/",
|
||||
},
|
||||
]
|
||||
|
||||
parent = (site / "categories" / "web-frameworks" / "index.html").read_text(encoding="utf-8")
|
||||
assert "category-breadcrumb" not in parent
|
||||
|
||||
def test_sponsorship_page_contains_json_ld(self, tmp_path):
|
||||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
|
||||
## Projects
|
||||
|
||||
**Tools**
|
||||
|
||||
## Widgets
|
||||
|
||||
- [w1](https://example.com/w1) - A widget.
|
||||
|
||||
# Contributing
|
||||
|
||||
Done.
|
||||
""")
|
||||
self._copy_real_templates(tmp_path)
|
||||
(tmp_path / "README.md").write_text(readme, encoding="utf-8")
|
||||
build(tmp_path)
|
||||
|
||||
site = tmp_path / "website" / "output"
|
||||
html = (site / "sponsorship" / "index.html").read_text(encoding="utf-8")
|
||||
parser = HeadMetadataParser()
|
||||
parser.feed(html)
|
||||
|
||||
assert parser.title.strip() == "Sponsor Awesome Python"
|
||||
assert parser.meta_by_name["description"] == (
|
||||
"Sponsorship for awesome-python: tiers, audience, and how to get your product in front of professional Python developers evaluating tools for production use."
|
||||
)
|
||||
assert parser.links_by_rel["canonical"] == "https://awesome-python.com/sponsorship/"
|
||||
|
||||
marker = '<script type="application/ld+json">'
|
||||
start = html.index(marker) + len(marker)
|
||||
end = html.index("</script>", start)
|
||||
graph = {node["@type"]: node for node in json.loads(html[start:end])["@graph"]}
|
||||
|
||||
assert set(graph) == {"WebSite", "WebPage", "BreadcrumbList"}
|
||||
assert graph["WebPage"]["@id"] == "https://awesome-python.com/sponsorship/"
|
||||
assert graph["WebPage"]["url"] == "https://awesome-python.com/sponsorship/"
|
||||
assert graph["BreadcrumbList"]["itemListElement"] == [
|
||||
{"@type": "ListItem", "position": 1, "name": "Awesome Python", "item": "https://awesome-python.com/"},
|
||||
{"@type": "ListItem", "position": 2, "name": "Sponsorship", "item": "https://awesome-python.com/sponsorship/"},
|
||||
]
|
||||
|
||||
def test_index_embeds_filter_urls_json(self, tmp_path):
|
||||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue