- 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>
Within the same star count, built-in (stdlib) entries were interleaved
with third-party entries. Add a builtin tier to the sort key so stdlib
entries always rank below non-stdlib entries at equal star counts.
Co-Authored-By: Claude <noreply@anthropic.com>
Entries with source_type 'Built-in' have no extractable GitHub repo key
from their URL, so they never received star/metadata enrichment. Fall
back to python/cpython for these entries so the star count and related
data are populated correctly.
Co-Authored-By: Claude <noreply@anthropic.com>
Previously, two entries sharing the same URL but different names would be
collapsed into one. Using a composite (url, name) key preserves distinct
entries while still merging true duplicates.
Co-Authored-By: Claude <noreply@anthropic.com>
Detect the hosting source (stdlib, GitLab, Bitbucket, External) from
the entry URL and surface it as a small badge in the stars column where
a star count would otherwise show an em dash.
Stdlib entries also get their own sort tier — between starred entries
and other no-star entries — so the standard library is not buried at
the bottom of each category.
Co-Authored-By: Claude <noreply@anthropic.com>
Entry was dead code with no callers. Switching from shutil.copy to
write_text uses the already-loaded readme_text variable directly instead
of re-reading the file from disk.
Co-Authored-By: Claude <noreply@anthropic.com>
The website builder previously relied on a hardcoded SECTION_GROUPS list in
build.py to organize categories into thematic groups. This was fragile: any
rename or addition to README.md required a matching code change.
Replace this with a parser-driven approach:
- readme_parser.py now detects bold-only paragraphs (**Group Name**) as
group boundary markers and groups H2 categories beneath them into
ParsedGroup structs.
- build.py drops SECTION_GROUPS entirely; group_categories() now just
passes parsed groups through and appends the Resources group.
- sort.py is removed as it relied on the old flat section model.
- Tests updated throughout to reflect the new (groups, resources) return
shape and to cover the new grouping logic.
Co-Authored-By: Claude <noreply@anthropic.com>
Entries appearing in more than one category were previously emitted as
separate rows. They are now deduplicated in build.py by URL, collecting
all category and group names into lists.
The template encodes those lists as pipe-delimited data attributes
(data-cats, data-groups) and renders a tag button per category.
The JS filter is updated to split on '||' and check for membership,
so clicking any category tag correctly shows the merged row.
Co-Authored-By: Claude <noreply@anthropic.com>
AI & ML: AI and Agents, Machine Learning, Deep Learning, Computer Vision,
Natural Language Processing, Recommender Systems, Robotics.
Data & Science: Data Analysis, Data Validation, Data Visualization,
Geolocation, Science, Quantum Computing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the Specialized catchall group. Redistribute its categories:
- Web & API: Admin Panels, CMS, Email, Static Site Generator, URL Manipulation
- AI & Data: Geolocation, Robotics
- Content & Media: Game Development, Internationalization
- System & Runtime: Date and Time, Hardware, Microsoft Windows
- Development Tools: Algorithms and Design Patterns
Only Miscellaneous remains ungrouped (falls into Other).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>