Skip to content

Commit 32ae78f

Browse files
committed
fix: improve website SEO metadata
1 parent 9f156de commit 32ae78f

4 files changed

Lines changed: 167 additions & 10 deletions

File tree

website/build.py

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
SPONSORSHIP_PATH = "/sponsorship/"
3030
SPONSORSHIP_PUBLIC_URL = f"{SITE_URL}sponsorship/"
31+
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."
3132

3233
SOURCE_TYPE_DOMAINS = {
3334
"docs.python.org": "Built-in",
@@ -128,6 +129,8 @@ def _website_node() -> dict:
128129
"@id": WEBSITE_ID,
129130
"name": "Awesome Python",
130131
"url": SITE_URL,
132+
"inLanguage": "en",
133+
"sameAs": "https://github.com/vinta/awesome-python",
131134
}
132135

133136

@@ -164,34 +167,98 @@ def build_homepage_json_ld(entries: Sequence[TemplateEntry], total_categories: i
164167
"url": SITE_URL,
165168
"description": description,
166169
"isPartOf": ISPARTOF_WEBSITE,
170+
"inLanguage": "en",
167171
"mainEntity": _item_list_payload(entries),
168172
},
169173
],
170174
}
171175

172176

173-
def category_meta_description(name: str, entry_count: int, description: str) -> str:
174-
count_sentence = f"Explore {entry_count} curated Python projects in {name}."
177+
def category_meta_title(name: str, parent_name: str | None = None) -> str:
178+
if parent_name:
179+
title = f"{name} for {parent_name} - Awesome Python"
180+
if len(title) <= 60:
181+
return title
182+
title = f"{parent_name}: {name} - Awesome Python"
183+
if len(title) <= 60:
184+
return title
185+
return f"{name} - Awesome Python"
186+
title = f"{name} Python Libraries - Awesome Python"
187+
if len(title) <= 60:
188+
return title
189+
return f"{name} - Awesome Python"
190+
191+
192+
def category_meta_description(name: str, entry_count: int, description: str, parent_name: str | None = None) -> str:
193+
target = f"{name} for {parent_name}" if parent_name else name
194+
count_sentence = f"Explore {entry_count} curated Python projects in {target}."
175195
if description:
176196
lead = description if description.endswith((".", "!", "?")) else f"{description}."
177197
return f"{lead} {count_sentence}"
178198
return f"{count_sentence} Part of the Awesome Python catalog."
179199

180200

181-
def build_category_json_ld(name: str, url: str, description: str, entries: Sequence[TemplateEntry]) -> dict:
201+
def build_breadcrumb_json_ld(items: Sequence[tuple[str, str]]) -> dict:
202+
return {
203+
"@type": "BreadcrumbList",
204+
"itemListElement": [
205+
{
206+
"@type": "ListItem",
207+
"position": i,
208+
"name": name,
209+
"item": url,
210+
}
211+
for i, (name, url) in enumerate(items, start=1)
212+
],
213+
}
214+
215+
216+
def build_category_json_ld(
217+
name: str,
218+
url: str,
219+
description: str,
220+
entries: Sequence[TemplateEntry],
221+
breadcrumbs: Sequence[tuple[str, str]],
222+
) -> dict:
182223
return {
183224
"@context": "https://schema.org",
184225
"@graph": [
185226
_website_node(),
186227
{
187228
"@type": "CollectionPage",
188229
"@id": url,
189-
"name": f"{name} Python Libraries",
230+
"name": name,
190231
"url": url,
191232
"description": description,
192233
"isPartOf": ISPARTOF_WEBSITE,
234+
"inLanguage": "en",
193235
"mainEntity": _item_list_payload(entries),
194236
},
237+
build_breadcrumb_json_ld(breadcrumbs),
238+
],
239+
}
240+
241+
242+
def build_sponsorship_json_ld() -> dict:
243+
return {
244+
"@context": "https://schema.org",
245+
"@graph": [
246+
_website_node(),
247+
{
248+
"@type": "WebPage",
249+
"@id": SPONSORSHIP_PUBLIC_URL,
250+
"name": "Sponsor Awesome Python",
251+
"url": SPONSORSHIP_PUBLIC_URL,
252+
"description": SPONSORSHIP_DESCRIPTION,
253+
"isPartOf": ISPARTOF_WEBSITE,
254+
"inLanguage": "en",
255+
},
256+
build_breadcrumb_json_ld(
257+
[
258+
("Awesome Python", SITE_URL),
259+
("Sponsorship", SPONSORSHIP_PUBLIC_URL),
260+
]
261+
),
195262
],
196263
}
197264

@@ -548,14 +615,21 @@ def render_category(
548615
group_categories: Sequence[ParsedSection] | None = None,
549616
) -> None:
550617
page_dir.mkdir(parents=True, exist_ok=True)
551-
category_description = category_meta_description(category["name"], len(entries), category["description"])
618+
parent_name = parent_category["name"] if parent_category else None
619+
category_title = category_meta_title(category["name"], parent_name)
620+
category_description = category_meta_description(category["name"], len(entries), category["description"], parent_name)
621+
breadcrumbs = [("Awesome Python", SITE_URL)]
622+
if parent_category:
623+
breadcrumbs.append((parent_category["name"], category_public_url(parent_category)))
624+
breadcrumbs.append((category["name"], category_url))
552625
category_json_ld = json.dumps(
553-
build_category_json_ld(category["name"], category_url, category_description, entries),
626+
build_category_json_ld(category_title.removesuffix(" - Awesome Python"), category_url, category_description, entries, breadcrumbs),
554627
ensure_ascii=False,
555628
).replace("</", "<\\/")
556629
(page_dir / "index.html").write_text(
557630
tpl_category.render(
558631
category=category,
632+
category_title=category_title,
559633
category_url=category_url,
560634
category_description=category_description,
561635
entries=entries,
@@ -607,7 +681,11 @@ def render_category(
607681
hero_stats.append(f"{repo_stars}+ stars on GitHub")
608682
hero_stats.append(f"Updated {build_date.strftime('%B %d, %Y')}")
609683
(sponsorship_dir / "index.html").write_text(
610-
tpl_sponsorship.render(hero_stats=hero_stats),
684+
tpl_sponsorship.render(
685+
hero_stats=hero_stats,
686+
sponsorship_description=SPONSORSHIP_DESCRIPTION,
687+
sponsorship_json_ld=json.dumps(build_sponsorship_json_ld(), ensure_ascii=False).replace("</", "<\\/"),
688+
),
611689
encoding="utf-8",
612690
)
613691

website/templates/category.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% extends "base.html" %}
2-
{% block title %}{{ category.name }} Python Libraries - Awesome Python{% endblock %}
2+
{% block title %}{{ category_title }}{% endblock %}
33
{% block description %}{{ category_description }}{% endblock %}
44
{% block canonical_url %}{{ category_url }}{% endblock %}
55
{% block alternate_links %}{% endblock %}

website/templates/sponsorship.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
{% extends "base.html" %}
22
{% block title %}Sponsor Awesome Python{% endblock %}
3-
{% 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 %}
3+
{% block description %}{{ sponsorship_description }}{% endblock %}
44
{% block canonical_url %}https://awesome-python.com/sponsorship/{% endblock %}
55
{% block alternate_links %}{% endblock %}
6+
{% block extra_head %}
7+
<script type="application/ld+json">{{ sponsorship_json_ld | safe }}</script>
8+
{% endblock %}
69
{% block header %}
710
<header class="category-hero sponsorship-hero">
811
<div class="hero-sheen" aria-hidden="true"></div>

website/tests/test_build.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ def test_category_page_contains_json_ld(self, tmp_path):
569569

570570
assert data["@context"] == "https://schema.org"
571571
graph = {node["@type"]: node for node in data["@graph"]}
572-
assert set(graph) == {"WebSite", "CollectionPage"}
572+
assert set(graph) == {"WebSite", "CollectionPage", "BreadcrumbList"}
573573
assert graph["WebSite"]["@id"] == "https://awesome-python.com/#website"
574574
collection = graph["CollectionPage"]
575575
assert collection["name"] == "Widgets Python Libraries"
@@ -588,6 +588,12 @@ def test_category_page_contains_json_ld(self, tmp_path):
588588
positions = sorted(item["position"] for item in item_list["itemListElement"])
589589
assert positions == [1, 2]
590590

591+
breadcrumbs = graph["BreadcrumbList"]["itemListElement"]
592+
assert breadcrumbs == [
593+
{"@type": "ListItem", "position": 1, "name": "Awesome Python", "item": "https://awesome-python.com/"},
594+
{"@type": "ListItem", "position": 2, "name": "Widgets", "item": "https://awesome-python.com/categories/widgets/"},
595+
]
596+
591597
def test_group_page_falls_back_to_default_description_in_json_ld(self, tmp_path):
592598
readme = textwrap.dedent("""\
593599
# T
@@ -685,9 +691,79 @@ def test_subcategory_page_shows_breadcrumb(self, tmp_path):
685691
assert "<h1>Synchronous</h1>" in sync
686692
assert "category-breadcrumb" in sync
687693

694+
parser = HeadMetadataParser()
695+
parser.feed(sync)
696+
assert parser.title.strip() == "Synchronous for Web Frameworks - Awesome Python"
697+
assert parser.meta_by_name["description"] == "Explore 1 curated Python projects in Synchronous for Web Frameworks. Part of the Awesome Python catalog."
698+
699+
marker = '<script type="application/ld+json">'
700+
start = sync.index(marker) + len(marker)
701+
end = sync.index("</script>", start)
702+
graph = {node["@type"]: node for node in json.loads(sync[start:end])["@graph"]}
703+
assert graph["CollectionPage"]["name"] == "Synchronous for Web Frameworks"
704+
assert graph["BreadcrumbList"]["itemListElement"] == [
705+
{"@type": "ListItem", "position": 1, "name": "Awesome Python", "item": "https://awesome-python.com/"},
706+
{
707+
"@type": "ListItem",
708+
"position": 2,
709+
"name": "Web Frameworks",
710+
"item": "https://awesome-python.com/categories/web-frameworks/",
711+
},
712+
{
713+
"@type": "ListItem",
714+
"position": 3,
715+
"name": "Synchronous",
716+
"item": "https://awesome-python.com/categories/web-frameworks/synchronous/",
717+
},
718+
]
719+
688720
parent = (site / "categories" / "web-frameworks" / "index.html").read_text(encoding="utf-8")
689721
assert "category-breadcrumb" not in parent
690722

723+
def test_sponsorship_page_contains_json_ld(self, tmp_path):
724+
readme = textwrap.dedent("""\
725+
# T
726+
727+
## Projects
728+
729+
**Tools**
730+
731+
## Widgets
732+
733+
- [w1](https://example.com/w1) - A widget.
734+
735+
# Contributing
736+
737+
Done.
738+
""")
739+
self._copy_real_templates(tmp_path)
740+
(tmp_path / "README.md").write_text(readme, encoding="utf-8")
741+
build(tmp_path)
742+
743+
site = tmp_path / "website" / "output"
744+
html = (site / "sponsorship" / "index.html").read_text(encoding="utf-8")
745+
parser = HeadMetadataParser()
746+
parser.feed(html)
747+
748+
assert parser.title.strip() == "Sponsor Awesome Python"
749+
assert parser.meta_by_name["description"] == (
750+
"Sponsorship for awesome-python: tiers, audience, and how to get your product in front of professional Python developers evaluating tools for production use."
751+
)
752+
assert parser.links_by_rel["canonical"] == "https://awesome-python.com/sponsorship/"
753+
754+
marker = '<script type="application/ld+json">'
755+
start = html.index(marker) + len(marker)
756+
end = html.index("</script>", start)
757+
graph = {node["@type"]: node for node in json.loads(html[start:end])["@graph"]}
758+
759+
assert set(graph) == {"WebSite", "WebPage", "BreadcrumbList"}
760+
assert graph["WebPage"]["@id"] == "https://awesome-python.com/sponsorship/"
761+
assert graph["WebPage"]["url"] == "https://awesome-python.com/sponsorship/"
762+
assert graph["BreadcrumbList"]["itemListElement"] == [
763+
{"@type": "ListItem", "position": 1, "name": "Awesome Python", "item": "https://awesome-python.com/"},
764+
{"@type": "ListItem", "position": 2, "name": "Sponsorship", "item": "https://awesome-python.com/sponsorship/"},
765+
]
766+
691767
def test_index_embeds_filter_urls_json(self, tmp_path):
692768
readme = textwrap.dedent("""\
693769
# T

0 commit comments

Comments
 (0)