diff --git a/website/build.py b/website/build.py index f9e3aa55..af644bfd 100644 --- a/website/build.py +++ b/website/build.py @@ -84,6 +84,14 @@ def build_robots_txt() -> str: ) +def category_path(category: ParsedSection) -> str: + return f"/categories/{category['slug']}/" + + +def category_public_url(category: ParsedSection) -> str: + return f"{SITE_URL}categories/{category['slug']}/" + + def write_sitemap_xml(path: Path, urls: Sequence[tuple[str, str]]) -> None: ET.register_namespace("", SITEMAP_NS) urlset = ET.Element(f"{{{SITEMAP_NS}}}urlset") @@ -278,6 +286,7 @@ def build(repo_root: Path) -> None: entry["last_commit_at"] = sd.get("last_commit_at", "") entries = sort_entries(entries) + category_urls = {cat["name"]: category_path(cat) for cat in categories} env = Environment( loader=FileSystemLoader(website / "templates"), @@ -302,10 +311,27 @@ def build(repo_root: Path) -> None: repo_stars=repo_stars, build_date=build_date.strftime("%B %d, %Y"), sponsors=sponsors, + category_urls=category_urls, ), encoding="utf-8", ) + tpl_category = env.get_template("category.html") + categories_dir = site_dir / "categories" + for category in categories: + category_entries = [entry for entry in entries if category["name"] in entry["categories"]] + page_dir = categories_dir / category["slug"] + page_dir.mkdir(parents=True, exist_ok=True) + (page_dir / "index.html").write_text( + tpl_category.render( + category=category, + category_url=category_public_url(category), + entries=category_entries, + total_categories=len(categories), + ), + encoding="utf-8", + ) + static_src = website / "static" static_dst = site_dir / "static" if static_src.exists(): @@ -317,11 +343,15 @@ def build(repo_root: Path) -> None: llms_template = (website / "templates" / "llms.txt").read_text(encoding="utf-8") llms_txt = build_llms_txt(llms_template, readme_text, stars_data) (site_dir / "robots.txt").write_text(build_robots_txt(), encoding="utf-8") - write_sitemap_xml(site_dir / "sitemap.xml", [(SITE_URL, build_date.date().isoformat())]) + sitemap_date = build_date.date().isoformat() + sitemap_urls = [(SITE_URL, sitemap_date)] + [ + (category_public_url(category), sitemap_date) for category in categories + ] + write_sitemap_xml(site_dir / "sitemap.xml", sitemap_urls) (site_dir / "index.md").write_text(markdown_index, encoding="utf-8") (site_dir / "llms.txt").write_text(llms_txt, encoding="utf-8") - print(f"Built single page with {len(parsed_groups)} groups, {len(categories)} categories") + print(f"Built site with {len(parsed_groups)} groups, {len(categories)} categories") print(f"Total entries: {total_entries}") print(f"Output: {site_dir}") diff --git a/website/static/main.js b/website/static/main.js index 7353ff2c..f875f8b1 100644 --- a/website/static/main.js +++ b/website/static/main.js @@ -202,6 +202,8 @@ function getSortValue(row, col) { } function sortRows() { + if (!tbody) return; + const arr = Array.prototype.slice.call(rows); const col = activeSort.col; const order = activeSort.order; diff --git a/website/static/style.css b/website/static/style.css index ec395e98..2adeca39 100644 --- a/website/static/style.css +++ b/website/static/style.css @@ -376,18 +376,92 @@ kbd { } .hero-action:focus-visible, +.hero-brand-mini:focus-visible, .hero-topbar-link:focus-visible, .search:focus-visible, .filter-clear:focus-visible, .tag:focus-visible, .back-to-top:focus-visible, .no-results-clear:focus-visible, +.category-table a:focus-visible, .footer a:focus-visible, .sort-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; } +.category-hero { + position: relative; + overflow: clip; + background: linear-gradient(140deg, var(--hero-bg-start) 0%, var(--hero-bg-mid) 58%, var(--hero-bg-end) 100%); + color: var(--hero-text); +} + +.category-hero-shell { + position: relative; + z-index: 1; + width: min(100%, calc(var(--shell-max) + (var(--shell-pad) * 2))); + margin: 0 auto; + padding: 1.25rem var(--shell-pad) clamp(3.75rem, 8vw, 6.75rem); + display: grid; + gap: clamp(3rem, 8vw, 5.5rem); +} + +.category-hero h1 { + font-family: var(--font-display); + font-size: clamp(3.6rem, 9vw, 7rem); + line-height: 0.9; + font-weight: 600; + text-wrap: balance; +} + +.category-subtitle { + max-width: 68ch; + margin-top: 1.1rem; + color: var(--hero-muted); + font-size: clamp(1rem, 1.8vw, 1.18rem); + text-wrap: pretty; +} + +.category-results { + padding-top: clamp(2.5rem, 5vw, 3.75rem); +} + +.category-table .col-name { + width: min(42rem, 48vw); + white-space: normal; +} + +.category-table .col-name > a { + display: inline-block; +} + +.category-row-desc { + display: block; + max-width: 68ch; + margin-top: 0.32rem; + color: var(--ink-soft); + font-size: var(--text-sm); + font-weight: 500; + line-height: 1.55; + text-wrap: pretty; +} + +.category-row-desc a { + color: var(--accent-deep); + text-decoration: underline; + text-decoration-color: var(--accent-underline); + text-underline-offset: 0.18em; +} + +.category-row-desc a:hover { + color: var(--accent); +} + +.category-table .expand-content { + padding-block: 0.25rem 0.15rem; +} + .sponsor-band { padding-block: clamp(2.5rem, 5.5vw, 4rem); background: diff --git a/website/templates/base.html b/website/templates/base.html index af112095..22b56e98 100644 --- a/website/templates/base.html +++ b/website/templates/base.html @@ -3,21 +3,24 @@ {% set default_meta_title = "Awesome Python" %} {% set default_meta_description = "An opinionated guide to the best Python frameworks, libraries, and tools. Explore " ~ (entries | length) ~ " curated projects across " ~ total_categories ~ " categories, from AI and agents to data science and web development." %} - {% set canonical_url = "https://awesome-python.com/" %} + {% set default_canonical_url = "https://awesome-python.com/" %} {% set social_image_url = "https://awesome-python.com/static/og-image.png" %} {% set meta_title %}{% block title %}{{ default_meta_title }}{% endblock %}{% endset %} {% set meta_description %}{% block description %}{{ default_meta_description }}{% endblock %}{% endset %} + {% set canonical_url %}{% block canonical_url %}{{ default_canonical_url }}{% endblock %}{% endset %} {{ meta_title | trim }} - + + {% block alternate_links %} + {% endblock %} - + diff --git a/website/templates/category.html b/website/templates/category.html new file mode 100644 index 00000000..44152071 --- /dev/null +++ b/website/templates/category.html @@ -0,0 +1,195 @@ +{% 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 canonical_url %}{{ category_url }}{% endblock %} +{% block alternate_links %}{% endblock %} +{% block header %} +
+ + + +
+ + +
+

{{ category.name }}

+ {% if category.description %} +

{{ category.description }}

+ {% endif %} +
+
+
+{% endblock %} +{% block content %} +
+
+
+

Projects in {{ category.name }}

+
+

+ Sorted by GitHub stars when available. Click any row for details. +

+
+ +

{{ category.name }} results

+
+ + + + + + + + + + + + + {% for entry in entries %} + + + + + + + + + + + + + + {% endfor %} + +
Row number + + + + + + TagsDetails
+
+ {% if entry.also_see %} +
+ Also see: {% for see in entry.also_see %}{{ see.name }}{% if not loop.last %}, {% endif %}{% endfor %} +
+ {% endif %} +
+ {% if entry.owner %}{{ entry.owner }}/{% endif %}{{ entry.url | replace("https://", "") }} + {% if entry.last_commit_at %}/{% endif %} +
+
+
+
+
+ +
+
+ +

Know a project that belongs here?

+

Tell us what it does and why it stands out.

+ +
+
+{% endblock %} diff --git a/website/templates/index.html b/website/templates/index.html index 53e968d3..e2853d0b 100644 --- a/website/templates/index.html +++ b/website/templates/index.html @@ -215,7 +215,12 @@ {{ subcat.name }} {% endfor %} {% for cat in entry.categories %} - + {{ cat }} {% endfor %}