Standard-library entries rendered "Not on PyPI" in the PyPI Downloads
column, which read like missing data rather than a deliberate category
— the build already forces downloads to None for them so they never
pick up a same-named PyPI backport. They now render a "Stdlib" badge
instead, while genuine non-PyPI entries keep "Not on PyPI".
The filter tag is renamed to match, so the source-type value, the row
filter tag, and the synthetic category heading all read Stdlib now.
The literal "Built-in" strings scattered through build.py are routed
through the existing BUILTIN_FILTER constant so the label lives in one
place. The category page slug stays "built-in" so the public URL
/categories/built-in/ does not break.
Co-Authored-By: Claude <noreply@anthropic.com>
Entries now end with "(PyPI downloads/month: N, GitHub stars: M)" where
known, replacing the stars-only note, since download counts are the
list's stated primary evidence signal. annotate_entries_with_stars is
renamed to annotate_entries_with_stats and looks downloads up by the
first link's display name, skipping category-index bullets (which link
into the site itself) and Built-in entries (which would otherwise hit
same-named PyPI backports like logging or asyncio).
The intro now mirrors the README subtitle verbatim with the
project/category totals on their own line below, and the "opinionated
catalog" wording is gone since the shortlist ADR is literally titled
"shortlist, not a catalog".
Co-Authored-By: Claude <noreply@anthropic.com>
Entries with a download count now sort first (descending), with
stars, then Built-in, then name as fallback tiers for entries that
lack a count. main.js mirrors this in its default activeSort, clean
URL check, and third-click reset target. Sorting by stars remains one
header click away.
Co-Authored-By: Claude <noreply@anthropic.com>
Sourced from website/data/pypi_downloads.tsv the same way
github_stars.json feeds the stars column. The new sortable column
sits between GitHub Stars and Last Commit on the homepage and
category pages, formatted with thousands separators like stars, with
an em dash when no PyPI data exists. Rows are matched by normalized
README display name; Built-in entries never show counts since
same-named PyPI packages are stdlib backports (e.g. the asyncio
package).
Below 960px the column hides and the count moves into the expand
row, mirroring the existing Last Commit treatment. main.js gains the
downloads sort branch and URL param.
The deploy workflow fetches the TSV via the new
make fetch_pypi_downloads target with a daily actions/cache
fallback, mirroring the stars fetch, but non-fatal: the column
degrades to dashes when the fetch fails, unlike stars which the
build requires.
Co-Authored-By: Claude <noreply@anthropic.com>
- Wrap category pages in a self-contained @graph (WebSite + CollectionPage)
- Set canonical @id on CollectionPage to its URL (no hash fragment)
- Expand isPartOf to typed object {"@type": "WebSite", "@id": ...}
- Extract _website_node() and ISPARTOF_WEBSITE constants to avoid repetition
- Update tests to assert @graph structure on category pages
Co-Authored-By: Claude <noreply@anthropic.com>
Remove the 'All projects' nav link and total_entries hero stat from the
sponsorship page. Rename 'View the repository' CTA to 'View on GitHub'.
Co-Authored-By: Claude <noreply@anthropic.com>
- Extract render_category() helper to deduplicate the three category/group/builtin
rendering blocks in build.py
- Replace synthetic dict literals with synthetic_category() helper
- Rewrite subcategory rendering to avoid O(n²) loop using precomputed dicts
- Pass filter_urls (not just JSON) to templates so Jinja can look up group URLs
directly instead of applying the slugify filter at render time
- Remove slugify from env.filters (no longer used in templates)
- Replace isIndexPage() wrapper with isIndexDocument constant in main.js
- Fix: call applyFilters() on page load when activeFilter is set
- Remove dead else branch in tag click handler (category pages with no URL)
- Switch .hero-category-links from CSS columns to CSS grid for more even layout
- Remove max-width cap on .category-subtitle
Co-Authored-By: Claude <noreply@anthropic.com>
Adds a dedicated sponsorship page at /sponsorship/ built from the Jinja2
template, with hero stats, tier cards, and CSS. Updates the index.html
sponsor sidebar link to point to /sponsorship/ instead of the GitHub
SPONSORSHIP.md. Adds the URL to the sitemap and test fixtures.
Also renames .impeccable.md to DESIGN.md.
Co-Authored-By: Claude <noreply@anthropic.com>
Register Built-in as a navigable filter path alongside regular category
and group slugs, emit the page during build, add it to the sitemap, and
wire the Built-in tag buttons in index.html and category.html to navigate
there via data-url.
Co-Authored-By: Claude <noreply@anthropic.com>
Add search input, filter chips, no-results block, and back-to-top
button to category/group/subcategory pages. Pass filter_urls_json to
all page types so tag-chip navigation works site-wide. Fix JS so
filter-clear and no-results-clear redirect to / on non-index pages
instead of trying to filter a non-existent local table. Remove the
now-redundant .category-results CSS overrides.
Co-Authored-By: Claude <noreply@anthropic.com>
The category template rendered a tag for `category.name` plus a tag for
`entry.groups[0]`, which duplicated the group name on group pages where
those values are identical (e.g. /categories/python-language/ showing
"Python Language" twice). It also never rendered `entry.categories`, so
group pages omitted each project's actual category.
Mirror the index template's tag rendering on category, group, and
subcategory pages, and mark whichever tag matches the current page URL
as active. Pass `category_urls` and `current_path` to each render call
so the template can match by URL.
`| safe` bypasses Jinja autoescape. If a category name ever contained
"</script>", the literal substring would close the script block early,
leaking JSON content into the DOM and creating an XSS vector. Replace
"</" with "<\\/" (still valid JSON) and pass ensure_ascii=False so
non-ASCII names render readably. Also add a group_path() helper to
parallel category_path()/subcategory_path() and reuse category_urls
when seeding filter_urls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds filter_urls dict (categories, groups, subcategories) in build.py,
passes filter_urls_json to the template, and injects a JSON script block
before the results section in index.html. Covered by a new test that
verifies all three URL types are present and correctly resolved.
Co-Authored-By: Claude <noreply@anthropic.com>
Use the precomputed sub["url"] to identify which subcategories belong
to a category. Avoids parsing the "Cat > Sub" value string, which would
silently misfire if a category name ever contained " > ".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Categories and groups will share the /categories/ URL namespace.
Fail the build with a clear error message if a future README change
introduces a collision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add llms.txt Jinja2 template with a categories_md placeholder
- Extract categories body from README and inject it into the template
- Annotate bullet-entry lines with GitHub star counts (N GitHub stars)
for the main index.md and bare numbers for llms.txt
- Add TestAnnotateEntriesWithStars unit tests
Co-Authored-By: Claude <noreply@anthropic.com>
* update gitignore
* feat: tighten homepage metadata
* fix: trim generated HTML whitespace
* feat(website): add discovery files and markdown alternate
* feat(website): add sitemap lastmod
* feat(seo): add Content-Signal directive to robots.txt
Signals search, ai-input, and ai-train to crawlers
via the experimental Content-Signal header in robots.txt.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Collapse the if-seen/else-new branches so the category/group/subcategory
merge logic runs once per entry unconditionally, appending to empty lists
on first sight instead of duplicating the append logic in the else branch.
Annotate seen and entries as dict[str, Any] so ty can resolve the mixed
value types (str, list, None) in each entry dict.
Co-Authored-By: Claude <noreply@anthropic.com>
The helper only appeared once and the logic is two lines, so the named
function added indirection without clarity. Removed the four dedicated
unit tests that covered the function directly.
Co-Authored-By: Claude <noreply@anthropic.com>
Cache-write shape mismatches the TypedDict and callers mix .get() and
direct access, so the stricter type was providing false safety. Using
dict[str, dict] accurately reflects the actual runtime contract.
Co-Authored-By: Claude <noreply@anthropic.com>
Replace loose list[dict] annotations with concrete TypedDicts imported
from readme_parser so ty can verify call-site compatibility.
Co-Authored-By: Claude <noreply@anthropic.com>
Python 3.11 introduced datetime.UTC as a cleaner alias for
datetime.timezone.utc. Both build.py and fetch_github_stars.py
are updated to use the shorter form.
Co-Authored-By: Claude <noreply@anthropic.com>
Parse the # Sponsors heading in README.md into structured data and
render a dedicated sponsor band above the library index on the site.
Co-Authored-By: Claude <noreply@anthropic.com>
Display the awesome-python repo's star count (formatted as '230k+') and
the last data refresh date below the hero CTA. Fetches the self-repo
star count by always including vinta/awesome-python in the stars fetch.
Also removes the footer date stamp, which is now surfaced in the hero.
Co-Authored-By: Claude <noreply@anthropic.com>
parse_readme now returns list[ParsedGroup] instead of a tuple. The
resources section (Newsletters, Podcasts), preview string, and
content_html rendering are no longer produced by the parser or consumed
by the build. Removes _render_section_html, _group_by_h2, and the
associated dead code and tests.
Co-Authored-By: Claude <noreply@anthropic.com>
Subcategories with the same name (e.g. 'Frameworks') across different
top-level categories were sharing a filter value, so clicking one
subcategory tag would match entries from unrelated categories.
Each subcategory now stores both a display name and a scoped value
('Category > Subcategory') used for data-cats matching. The template
renders the display name on tags and mobile-cat span, but uses the
scoped value for filtering. Subcategory tags are also moved before
category tags so the most-specific label appears first.
Co-Authored-By: Claude <noreply@anthropic.com>
Entries nested under a plain-text subcategory heading (e.g. "Frameworks"
inside Testing) now carry a subcategory field populated by the parser.
The build pipeline collects these into a subcategories list on each merged
entry, and the template renders them as tag-subcat buttons that plug into
the existing data-cats filter mechanism.
A dedicated .tag-subcat style distinguishes them visually from category
tags, and both are hidden on mobile alongside .tag-group.
Co-Authored-By: Claude <noreply@anthropic.com>
group_categories only ever appended a Resources group when the
resources list was non-empty. All call sites passed an empty list,
making it a no-op indirection. Inline parsed_groups directly and
remove the dead code along with its tests.
Co-Authored-By: Claude <noreply@anthropic.com>
Resources are no longer passed through parse_readme, group_categories,
or the index template — they are replaced with empty lists and the
unused variable is prefixed with an underscore.
Co-Authored-By: Claude <noreply@anthropic.com>