Merge pull request #3107 from vinta/chore/refine-ui

chore: refine UI, add JSON-LD, polish category SEO
This commit is contained in:
Vinta Chen
2026-05-03 20:13:14 +08:00
committed by GitHub
10 changed files with 368 additions and 33 deletions

View File

@@ -75,6 +75,7 @@ Layout and sizing:
- The impeccable skill reference rule "cap line length at ~65-75ch" does NOT apply here. Ignore it. Readability at wide widths is carried by vertical rhythm, leading, and the modular type scale instead.
- If you believe a width cap is actually necessary for some specific element, ask first with a concrete reason before adding it.
- Body type floor is 16px (`--text-base: 1rem`). Content-heavy passages may go to 1.125rem.
- Absolute minimum font size is 12px (`0.75rem`) for ANY text, including pills, badges, tags, captions, footnotes. Anything smaller hits Chrome's default minimum-font-size floor and renders inconsistently across browsers and user accessibility settings. Use `var(--text-xs)` (`0.8rem`) as the smallest token.
- When in doubt about any type size, pick one step larger than what the impeccable skill's scale references suggest. The user has repeatedly corrected sizes upward (11+ separate requests across 8 sessions). Never reduce an existing size unprompted. Footer, meta rows, expand content, labels, headings all trend too small by default.
- Row numbers in the table: left-align, no leading zeros. The user tried zero-padding and rejected it.
- Adjacent heading levels differ by at least 0.25rem of rendered size.

View File

@@ -838,7 +838,7 @@ _Libraries for working with graphical user interface applications._
- [nicegui](https://github.com/zauberzeug/nicegui) - An easy-to-use, Python-based UI framework, which shows up in your web browser.
- [pywebview](https://github.com/r0x0r/pywebview/) - A lightweight cross-platform native wrapper around a webview component.
- Terminal
- [curses](https://docs.python.org/3/library/curses.html) - Built-in wrapper for [ncurses](http://www.gnu.org/software/ncurses/) used to create terminal GUI applications.
- [curses](https://docs.python.org/3/library/curses.html) - (Python standard library) The built-in wrapper for [ncurses](http://www.gnu.org/software/ncurses/) used to create terminal GUI applications.
- [urwid](https://github.com/urwid/urwid) - A library for creating terminal GUI applications with strong support for widgets, events, rich colors, etc.
- Wrappers
- [gooey](https://github.com/chriskiehl/Gooey) - Turn command line programs into a full GUI application with one line.

2
uv.lock generated
View File

@@ -3,7 +3,7 @@ revision = 3
requires-python = ">=3.13"
[options]
exclude-newer = "2026-04-30T04:22:17.540198Z"
exclude-newer = "2026-04-30T04:38:39.45925Z"
exclude-newer-span = "P3D"
[[package]]

View File

@@ -118,6 +118,84 @@ 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"
WEBSITE_ID = f"{SITE_URL}#website"
ISPARTOF_WEBSITE = {"@type": "WebSite", "@id": WEBSITE_ID}
def _website_node() -> dict:
return {
"@type": "WebSite",
"@id": WEBSITE_ID,
"name": "Awesome Python",
"url": SITE_URL,
}
def _item_list_payload(entries: Sequence[TemplateEntry]) -> dict:
return {
"@type": "ItemList",
"numberOfItems": len(entries),
"itemListElement": [
{
"@type": "ListItem",
"position": i,
"name": entry["name"],
"url": entry["url"],
}
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 {
"@context": "https://schema.org",
"@graph": [
_website_node(),
{
"@type": "CollectionPage",
"@id": SITE_URL,
"name": "Awesome Python",
"url": SITE_URL,
"description": description,
"isPartOf": ISPARTOF_WEBSITE,
"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}."
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:
return {
"@context": "https://schema.org",
"@graph": [
_website_node(),
{
"@type": "CollectionPage",
"@id": url,
"name": f"{name} Python Libraries",
"url": url,
"description": description,
"isPartOf": ISPARTOF_WEBSITE,
"mainEntity": _item_list_payload(entries),
},
],
}
def category_path(category: ParsedSection) -> str:
return f"/categories/{category['slug']}/"
@@ -378,6 +456,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 +475,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",
)
@@ -411,10 +494,20 @@ 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"]
)
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(
tpl_category.render(
category=category,
category_url=category_url,
category_description=category_description,
entries=entries,
total_categories=len(categories),
category_urls=category_urls,
@@ -423,6 +516,7 @@ def build(repo_root: Path) -> None:
filter_urls_json=filter_urls_json,
parent_category=parent_category,
group_categories=group_categories,
category_json_ld=category_json_ld,
),
encoding="utf-8",
)

View File

@@ -113,7 +113,12 @@ document
rows.forEach(function (row, i) {
row._origIndex = i;
row._expandRow = row.nextElementSibling;
let next = row.nextElementSibling;
if (next && next.classList.contains("desc-row")) {
row._descRow = next;
next = next.nextElementSibling;
}
row._expandRow = next;
});
function collapseAll() {
@@ -127,6 +132,7 @@ function collapseAll() {
function applyFilters() {
const query = searchInput ? searchInput.value.toLowerCase().trim() : "";
const descRowsVisible = !isIndexDocument || activeFilter !== null;
let visibleCount = 0;
collapseAll();
@@ -142,9 +148,11 @@ function applyFilters() {
if (show && query) {
if (!row._searchText) {
let text = row.textContent.toLowerCase();
const next = row.nextElementSibling;
if (next && next.classList.contains("expand-row")) {
text += " " + next.textContent.toLowerCase();
if (row._descRow) {
text += " " + row._descRow.textContent.toLowerCase();
}
if (row._expandRow) {
text += " " + row._expandRow.textContent.toLowerCase();
}
row._searchText = text;
}
@@ -152,6 +160,12 @@ function applyFilters() {
}
if (row.hidden !== !show) row.hidden = !show;
if (row._descRow) {
const descHidden = !show || !descRowsVisible;
if (row._descRow.hidden !== descHidden) {
row._descRow.hidden = descHidden;
}
}
if (show) {
visibleCount++;
@@ -262,7 +276,8 @@ function sortRows() {
const frag = document.createDocumentFragment();
arr.forEach(function (row) {
frag.appendChild(row);
frag.appendChild(row._expandRow);
if (row._descRow) frag.appendChild(row._descRow);
if (row._expandRow) frag.appendChild(row._expandRow);
});
tbody.appendChild(frag);
applyFilters();
@@ -291,7 +306,11 @@ if (tbody) {
// Don't toggle if clicking a link or tag button
if (e.target.closest("a") || e.target.closest(".tag")) return;
const row = e.target.closest("tr.row");
let row = e.target.closest("tr.row");
if (!row) {
const descRow = e.target.closest("tr.desc-row");
if (descRow) row = descRow.previousElementSibling;
}
if (!row) return;
const isOpen = row.classList.contains("open");
@@ -328,7 +347,7 @@ tags.forEach(function (tag) {
}
applyFilters();
} else if (url) {
window.location.href = url;
window.location.href = url + "#library-index";
}
});
});

View File

@@ -788,6 +788,11 @@ kbd {
box-shadow: inset 3px 0 0 var(--accent);
}
.row:has(+ .desc-row:not([hidden])) td {
border-bottom-color: transparent;
padding-bottom: 0.35rem;
}
.row.open td {
background: linear-gradient(180deg, var(--row-open-start), var(--row-open-end));
border-bottom-color: transparent;
@@ -919,14 +924,66 @@ th[data-sort].sort-asc::after {
transform: rotate(90deg);
}
.desc-row td {
padding-top: 0;
padding-bottom: 1rem;
border-bottom: 1px solid var(--line);
color: var(--ink-soft);
font-size: var(--text-sm);
line-height: 1.6;
text-wrap: pretty;
overflow-wrap: break-word;
transition:
background-color 180ms ease,
border-color 180ms ease;
}
.row:not(.open) + .desc-row td {
border-bottom: 1px solid var(--line);
}
.row + .desc-row {
cursor: pointer;
}
.row:not(.open):hover td,
.row:not(.open):hover + .desc-row td {
background: var(--row-hover);
}
.row.open + .desc-row td {
background: linear-gradient(180deg, var(--row-open-start), var(--row-open-end));
border-bottom-color: transparent;
}
.desc-text {
max-width: none;
}
.desc-text a {
color: var(--accent-deep);
}
.desc-text a:hover {
color: var(--accent);
text-decoration: underline;
text-decoration-color: var(--accent-underline);
text-underline-offset: 0.2em;
}
.expand-row {
display: none;
}
.row.open + .desc-row + .expand-row,
.row.open + .expand-row {
display: table-row;
}
.desc-row:not([hidden]) + .expand-row .expand-desc {
display: none;
}
.expand-row td {
padding-top: 0.1rem;
padding-bottom: 1.15rem;
@@ -993,7 +1050,7 @@ th[data-sort].sort-asc::after {
background: var(--accent-soft);
color: var(--accent-deep);
padding: 0.14rem 0.48rem;
font-size: 0.6rem;
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.02em;
cursor: pointer;
@@ -1426,8 +1483,8 @@ th[data-sort].sort-asc::after {
.footer-left {
display: flex;
flex-direction: column;
gap: 0.3rem;
align-items: center;
gap: 0.6rem;
}
.footer-brand {
@@ -1561,7 +1618,7 @@ th[data-sort].sort-asc::after {
.tag {
padding: 0.38rem 0.65rem;
font-size: 0.65rem;
font-size: var(--text-xs);
}
.table-wrap {

View File

@@ -53,6 +53,7 @@
gtag("js", new Date());
gtag("config", "G-0LMLYE0HER");
</script>
{% block extra_head %}{% endblock %}
</head>
<body>
<a href="#content" class="skip-link">Skip to content</a>
@@ -69,7 +70,9 @@
<footer class="footer">
<div class="footer-left">
<span class="footer-brand">Awesome Python</span>
<a href="/" class="footer-brand">Awesome Python</a>
<span class="footer-sep">/</span>
<a href="/sponsorship/">Sponsorship</a>
</div>
<div class="footer-links">
<span

View File

@@ -1,8 +1,11 @@
{% extends "base.html" %}
{% block title %}{{ category.name }} Python Libraries | Awesome Python{% 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 title %}{{ category.name }} Python Libraries - Awesome Python{% endblock %}
{% block description %}{{ category_description }}{% endblock %}
{% block canonical_url %}{{ category_url }}{% endblock %}
{% block alternate_links %}{% endblock %}
{% block extra_head %}
<script type="application/ld+json">{{ category_json_ld | safe }}</script>
{% endblock %}
{% block header %}
<header class="category-hero">
<div class="hero-sheen" aria-hidden="true"></div>
@@ -169,9 +172,9 @@
</td>
<td class="col-cat">
{% for subcat in entry.subcategories %}
<button class="tag{% if subcat.url == current_path %} active{% endif %}" data-value="{{ subcat.value }}" data-url="{{ subcat.url }}">
<a class="tag{% if subcat.url == current_path %} active{% endif %}" href="{{ subcat.url }}" data-value="{{ subcat.value }}" data-url="{{ subcat.url }}">
{{ subcat.name }}
</button>
</a>
{% endfor %}
{% for cat in entry.categories %}
<a
@@ -184,33 +187,41 @@
{% endfor %}
{% if entry.groups %}
{% set group_url = filter_urls[entry.groups[0]] %}
<button
<a
class="tag tag-group{% if group_url == current_path %} active{% endif %}"
href="{{ group_url }}"
data-value="{{ entry.groups[0] }}"
data-url="{{ group_url }}"
>
{{ entry.groups[0] }}
</button>
</a>
{% endif %}
{% if entry.source_type == 'Built-in' %}
<button
<a
class="tag tag-source{% if '/categories/built-in/' == current_path %} active{% endif %}"
href="/categories/built-in/"
data-value="Built-in"
data-url="/categories/built-in/"
>
Built-in
</button>
</a>
{% endif %}
</td>
<td class="col-arrow"><span class="arrow">&rarr;</span></td>
</tr>
{% if entry.description %}
<tr class="desc-row" aria-hidden="true">
<td class="col-num"></td>
<td colspan="5">
<div class="desc-text">{{ entry.description | safe }}</div>
</td>
</tr>
{% endif %}
<tr class="expand-row" id="expand-{{ loop.index }}">
<td></td>
<td colspan="4">
<div class="expand-content">
{% if entry.description %}
<div class="expand-desc">{{ entry.description | safe }}</div>
{% endif %} {% if entry.also_see %}
{% if entry.also_see %}
<div class="expand-also-see">
Also see: {% for see in entry.also_see %}<a
href="{{ see.url }}"

View File

@@ -1,4 +1,7 @@
{% extends "base.html" %}
{% block extra_head %}
<script type="application/ld+json">{{ homepage_json_ld | safe }}</script>
{% endblock %}
{% block header %}
<header class="hero">
<div class="hero-sheen" aria-hidden="true"></div>
@@ -220,9 +223,9 @@
</td>
<td class="col-cat">
{% for subcat in entry.subcategories %}
<button class="tag" data-value="{{ subcat.value }}" data-url="{{ subcat.url }}">
<a class="tag" href="{{ subcat.url }}" data-value="{{ subcat.value }}" data-url="{{ subcat.url }}">
{{ subcat.name }}
</button>
</a>
{% endfor %} {% for cat in entry.categories %}
<a
class="tag"
@@ -232,25 +235,35 @@
>{{ cat }}</a
>
{% endfor %}
<button
<a
class="tag tag-group"
href="{{ filter_urls[entry.groups[0]] }}"
data-value="{{ entry.groups[0] }}"
data-url="{{ filter_urls[entry.groups[0]] }}"
>
{{ entry.groups[0] }}
</button>
</a>
{% if entry.source_type == 'Built-in' %}
<button
<a
class="tag tag-source"
href="/categories/built-in/"
data-value="Built-in"
data-url="/categories/built-in/"
>
Built-in
</button>
</a>
{% endif %}
</td>
<td class="col-arrow"><span class="arrow">&rarr;</span></td>
</tr>
{% if entry.description %}
<tr class="desc-row" aria-hidden="true" hidden>
<td class="col-num"></td>
<td colspan="5">
<div class="desc-text">{{ entry.description | safe }}</div>
</td>
</tr>
{% endif %}
<tr class="expand-row" id="expand-{{ loop.index }}">
<td></td>
<td colspan="4">

View File

@@ -268,8 +268,8 @@ class TestBuild:
assert 'href="/categories/widgets/"' in index_html
assert 'data-value="Widgets"' in index_html
assert parser.title.strip() == "Widgets Python Libraries | Awesome Python"
assert parser.meta_by_name["description"] == "Explore 2 curated Python projects in Widgets. Widget libraries. Also see awesome-widgets."
assert parser.title.strip() == "Widgets Python Libraries - Awesome Python"
assert parser.meta_by_name["description"] == "Widget libraries. Also see awesome-widgets. Explore 2 curated Python projects in Widgets."
assert parser.links_by_rel["canonical"] == "https://awesome-python.com/categories/widgets/"
assert parser.meta_by_property["og:url"] == "https://awesome-python.com/categories/widgets/"
assert '<link rel="alternate" type="text/markdown" href="/index.md" />' not in category_html
@@ -471,6 +471,143 @@ class TestBuild:
assert 'id="hero-category-heading">Browse by category</h2>' 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 = '<script type="application/ld+json">'
assert marker in html
start = html.index(marker) + len(marker)
end = html.index("</script>", start)
block = html[start:end]
assert "</script>" 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"
assert graph["WebSite"]["@id"] == "https://awesome-python.com/#website"
collection = graph["CollectionPage"]
assert collection["@id"] == "https://awesome-python.com/"
assert collection["url"] == "https://awesome-python.com/"
assert collection["isPartOf"] == {"@type": "WebSite", "@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_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"
graph = {node["@type"]: node for node in data["@graph"]}
assert set(graph) == {"WebSite", "CollectionPage"}
assert graph["WebSite"]["@id"] == "https://awesome-python.com/#website"
collection = graph["CollectionPage"]
assert collection["name"] == "Widgets Python Libraries"
assert collection["@id"] == "https://awesome-python.com/categories/widgets/"
assert collection["url"] == "https://awesome-python.com/categories/widgets/"
assert collection["description"] == "Widget libraries. Explore 2 curated Python projects in Widgets."
assert collection["isPartOf"] == {"@type": "WebSite", "@id": "https://awesome-python.com/#website"}
item_list = collection["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])
graph = {node["@type"]: node for node in data["@graph"]}
collection = graph["CollectionPage"]
assert collection["name"] == "AI & ML Python Libraries"
assert collection["@id"] == "https://awesome-python.com/categories/ai-ml/"
assert collection["url"] == "https://awesome-python.com/categories/ai-ml/"
assert collection["description"] == "Explore 1 curated Python projects in AI & ML. Part of the Awesome Python catalog."
def test_build_creates_subcategory_pages(self, tmp_path):
readme = textwrap.dedent("""\
# T