mirror of
https://github.com/vinta/awesome-python.git
synced 2026-05-09 22:53:49 +08:00
feat(website): generate static category pages
This commit is contained in:
+32
-2
@@ -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:
|
def write_sitemap_xml(path: Path, urls: Sequence[tuple[str, str]]) -> None:
|
||||||
ET.register_namespace("", SITEMAP_NS)
|
ET.register_namespace("", SITEMAP_NS)
|
||||||
urlset = ET.Element(f"{{{SITEMAP_NS}}}urlset")
|
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", "")
|
entry["last_commit_at"] = sd.get("last_commit_at", "")
|
||||||
|
|
||||||
entries = sort_entries(entries)
|
entries = sort_entries(entries)
|
||||||
|
category_urls = {cat["name"]: category_path(cat) for cat in categories}
|
||||||
|
|
||||||
env = Environment(
|
env = Environment(
|
||||||
loader=FileSystemLoader(website / "templates"),
|
loader=FileSystemLoader(website / "templates"),
|
||||||
@@ -302,10 +311,27 @@ def build(repo_root: Path) -> None:
|
|||||||
repo_stars=repo_stars,
|
repo_stars=repo_stars,
|
||||||
build_date=build_date.strftime("%B %d, %Y"),
|
build_date=build_date.strftime("%B %d, %Y"),
|
||||||
sponsors=sponsors,
|
sponsors=sponsors,
|
||||||
|
category_urls=category_urls,
|
||||||
),
|
),
|
||||||
encoding="utf-8",
|
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_src = website / "static"
|
||||||
static_dst = site_dir / "static"
|
static_dst = site_dir / "static"
|
||||||
if static_src.exists():
|
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_template = (website / "templates" / "llms.txt").read_text(encoding="utf-8")
|
||||||
llms_txt = build_llms_txt(llms_template, readme_text, stars_data)
|
llms_txt = build_llms_txt(llms_template, readme_text, stars_data)
|
||||||
(site_dir / "robots.txt").write_text(build_robots_txt(), encoding="utf-8")
|
(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 / "index.md").write_text(markdown_index, encoding="utf-8")
|
||||||
(site_dir / "llms.txt").write_text(llms_txt, 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"Total entries: {total_entries}")
|
||||||
print(f"Output: {site_dir}")
|
print(f"Output: {site_dir}")
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,8 @@ function getSortValue(row, col) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sortRows() {
|
function sortRows() {
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
const arr = Array.prototype.slice.call(rows);
|
const arr = Array.prototype.slice.call(rows);
|
||||||
const col = activeSort.col;
|
const col = activeSort.col;
|
||||||
const order = activeSort.order;
|
const order = activeSort.order;
|
||||||
|
|||||||
@@ -376,18 +376,92 @@ kbd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero-action:focus-visible,
|
.hero-action:focus-visible,
|
||||||
|
.hero-brand-mini:focus-visible,
|
||||||
.hero-topbar-link:focus-visible,
|
.hero-topbar-link:focus-visible,
|
||||||
.search:focus-visible,
|
.search:focus-visible,
|
||||||
.filter-clear:focus-visible,
|
.filter-clear:focus-visible,
|
||||||
.tag:focus-visible,
|
.tag:focus-visible,
|
||||||
.back-to-top:focus-visible,
|
.back-to-top:focus-visible,
|
||||||
.no-results-clear:focus-visible,
|
.no-results-clear:focus-visible,
|
||||||
|
.category-table a:focus-visible,
|
||||||
.footer a:focus-visible,
|
.footer a:focus-visible,
|
||||||
.sort-btn:focus-visible {
|
.sort-btn:focus-visible {
|
||||||
outline: 2px solid var(--accent);
|
outline: 2px solid var(--accent);
|
||||||
outline-offset: 3px;
|
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 {
|
.sponsor-band {
|
||||||
padding-block: clamp(2.5rem, 5.5vw, 4rem);
|
padding-block: clamp(2.5rem, 5.5vw, 4rem);
|
||||||
background:
|
background:
|
||||||
|
|||||||
@@ -3,21 +3,24 @@
|
|||||||
<head>
|
<head>
|
||||||
{% set default_meta_title = "Awesome Python" %}
|
{% 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 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 social_image_url = "https://awesome-python.com/static/og-image.png" %}
|
||||||
{% set meta_title %}{% block title %}{{ default_meta_title }}{% endblock %}{% endset %}
|
{% set meta_title %}{% block title %}{{ default_meta_title }}{% endblock %}{% endset %}
|
||||||
{% set meta_description %}{% block description %}{{ default_meta_description }}{% endblock %}{% endset %}
|
{% set meta_description %}{% block description %}{{ default_meta_description }}{% endblock %}{% endset %}
|
||||||
|
{% set canonical_url %}{% block canonical_url %}{{ default_canonical_url }}{% endblock %}{% endset %}
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>{{ meta_title | trim }}</title>
|
<title>{{ meta_title | trim }}</title>
|
||||||
<meta name="description" content="{{ meta_description | trim }}" />
|
<meta name="description" content="{{ meta_description | trim }}" />
|
||||||
<link rel="canonical" href="{{ canonical_url }}" />
|
<link rel="canonical" href="{{ canonical_url | trim }}" />
|
||||||
|
{% block alternate_links %}
|
||||||
<link rel="alternate" type="text/markdown" href="/index.md" />
|
<link rel="alternate" type="text/markdown" href="/index.md" />
|
||||||
|
{% endblock %}
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:title" content="{{ meta_title | trim }}" />
|
<meta property="og:title" content="{{ meta_title | trim }}" />
|
||||||
<meta property="og:description" content="{{ meta_description | trim }}" />
|
<meta property="og:description" content="{{ meta_description | trim }}" />
|
||||||
<meta property="og:image" content="{{ social_image_url }}" />
|
<meta property="og:image" content="{{ social_image_url }}" />
|
||||||
<meta property="og:url" content="{{ canonical_url }}" />
|
<meta property="og:url" content="{{ canonical_url | trim }}" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:title" content="{{ meta_title | trim }}" />
|
<meta name="twitter:title" content="{{ meta_title | trim }}" />
|
||||||
<meta name="twitter:description" content="{{ meta_description | trim }}" />
|
<meta name="twitter:description" content="{{ meta_description | trim }}" />
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
<header class="category-hero">
|
||||||
|
<div class="hero-sheen" aria-hidden="true"></div>
|
||||||
|
<div class="hero-noise" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<div class="category-hero-shell">
|
||||||
|
<nav class="hero-topbar category-topbar" aria-label="Site">
|
||||||
|
<a href="/" class="hero-brand-mini">Awesome Python</a>
|
||||||
|
<div class="hero-topbar-actions">
|
||||||
|
<a href="/#library-index" class="hero-topbar-link">All projects</a>
|
||||||
|
<a
|
||||||
|
href="https://github.com/vinta/awesome-python/blob/master/CONTRIBUTING.md"
|
||||||
|
class="hero-topbar-link hero-topbar-link-strong"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>Submit a project</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="category-hero-copy">
|
||||||
|
<h1>{{ category.name }}</h1>
|
||||||
|
{% if category.description %}
|
||||||
|
<p class="category-subtitle">{{ category.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="results-section category-results" id="category-index">
|
||||||
|
<div class="results-intro section-shell" data-reveal>
|
||||||
|
<div>
|
||||||
|
<h2>Projects in {{ category.name }}</h2>
|
||||||
|
</div>
|
||||||
|
<p class="results-note">
|
||||||
|
Sorted by GitHub stars when available. Click any row for details.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="sr-only">{{ category.name }} results</h2>
|
||||||
|
<div
|
||||||
|
class="table-wrap"
|
||||||
|
tabindex="0"
|
||||||
|
role="region"
|
||||||
|
aria-label="{{ category.name }} libraries table"
|
||||||
|
>
|
||||||
|
<table class="table category-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="col-num"><span class="sr-only">Row number</span></th>
|
||||||
|
<th class="col-name" data-sort="name">
|
||||||
|
<button type="button" class="sort-btn">Project Name</button>
|
||||||
|
</th>
|
||||||
|
<th class="col-stars" data-sort="stars">
|
||||||
|
<button type="button" class="sort-btn">GitHub Stars</button>
|
||||||
|
</th>
|
||||||
|
<th class="col-commit" data-sort="commit-time">
|
||||||
|
<button type="button" class="sort-btn">Last Commit</button>
|
||||||
|
</th>
|
||||||
|
<th class="col-cat">Tags</th>
|
||||||
|
<th class="col-arrow"><span class="sr-only">Details</span></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in entries %}
|
||||||
|
<tr
|
||||||
|
class="row"
|
||||||
|
data-tags="{{ entry.categories | join('||') }}{% if entry.subcategories %}||{{ entry.subcategories | map(attribute='value') | join('||') }}{% endif %}||{{ entry.groups | join('||') }}{% if entry.source_type == 'Built-in' %}||Built-in{% endif %}"
|
||||||
|
tabindex="0"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="category-expand-{{ loop.index }}"
|
||||||
|
>
|
||||||
|
<td class="col-num">{{ loop.index }}</td>
|
||||||
|
<td class="col-name">
|
||||||
|
<a href="{{ entry.url }}" target="_blank" rel="noopener"
|
||||||
|
>{{ entry.name }}</a
|
||||||
|
>
|
||||||
|
{% if entry.description %}
|
||||||
|
<span class="category-row-desc">{{ entry.description | safe }}</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="mobile-cat"
|
||||||
|
>{% if entry.subcategories %}{{ entry.subcategories[0].name }}{%
|
||||||
|
else %}{{ category.name }}{% endif %}</span
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td class="col-stars">
|
||||||
|
{% if entry.stars is not none %}{{ "{:,}".format(entry.stars) }}{%
|
||||||
|
elif entry.source_type %}<span class="source-badge"
|
||||||
|
>{{ entry.source_type }}</span
|
||||||
|
>{% else %}—{% endif %}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="col-commit"
|
||||||
|
{%
|
||||||
|
if
|
||||||
|
entry.last_commit_at
|
||||||
|
%}data-commit="{{ entry.last_commit_at }}"
|
||||||
|
{%
|
||||||
|
endif
|
||||||
|
%}
|
||||||
|
>
|
||||||
|
{% if entry.last_commit_at %}<time
|
||||||
|
datetime="{{ entry.last_commit_at }}"
|
||||||
|
>{{ entry.last_commit_at[:10] }}</time
|
||||||
|
>{% else %}—{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="col-cat">
|
||||||
|
{% for subcat in entry.subcategories %}
|
||||||
|
<button class="tag" data-value="{{ subcat.value }}">
|
||||||
|
{{ subcat.name }}
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
<button class="tag active" data-value="{{ category.name }}">
|
||||||
|
{{ category.name }}
|
||||||
|
</button>
|
||||||
|
{% if entry.groups %}
|
||||||
|
<button class="tag tag-group" data-value="{{ entry.groups[0] }}">
|
||||||
|
{{ entry.groups[0] }}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if entry.source_type == 'Built-in' %}
|
||||||
|
<button class="tag tag-source" data-value="Built-in">
|
||||||
|
Built-in
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="col-arrow"><span class="arrow">→</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="expand-row" id="category-expand-{{ loop.index }}">
|
||||||
|
<td></td>
|
||||||
|
<td colspan="4">
|
||||||
|
<div class="expand-content">
|
||||||
|
{% if entry.also_see %}
|
||||||
|
<div class="expand-also-see">
|
||||||
|
Also see: {% for see in entry.also_see %}<a
|
||||||
|
href="{{ see.url }}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>{{ see.name }}</a
|
||||||
|
>{% if not loop.last %}, {% endif %}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="expand-meta">
|
||||||
|
{% if entry.owner %}<a
|
||||||
|
href="https://github.com/{{ entry.owner }}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>{{ entry.owner }}</a
|
||||||
|
><span class="expand-sep">/</span>{% endif %}<a
|
||||||
|
href="{{ entry.url }}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>{{ entry.url | replace("https://", "") }}</a
|
||||||
|
>
|
||||||
|
{% if entry.last_commit_at %}<span class="expand-commit"
|
||||||
|
><span class="expand-sep">/</span
|
||||||
|
><time datetime="{{ entry.last_commit_at }}"
|
||||||
|
>{{ entry.last_commit_at[:10] }}</time
|
||||||
|
></span
|
||||||
|
>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="final-cta" data-reveal>
|
||||||
|
<div class="section-shell">
|
||||||
|
<p class="section-label">Contribute</p>
|
||||||
|
<h2>Know a project that belongs here?</h2>
|
||||||
|
<p>Tell us what it does and why it stands out.</p>
|
||||||
|
<div class="final-cta-actions">
|
||||||
|
<a
|
||||||
|
href="https://github.com/vinta/awesome-python/blob/master/CONTRIBUTING.md"
|
||||||
|
class="hero-action hero-action-primary"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>Submit a project</a
|
||||||
|
>
|
||||||
|
<a href="/" class="hero-action hero-action-secondary">Browse all</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@@ -215,7 +215,12 @@
|
|||||||
{{ subcat.name }}
|
{{ subcat.name }}
|
||||||
</button>
|
</button>
|
||||||
{% endfor %} {% for cat in entry.categories %}
|
{% endfor %} {% for cat in entry.categories %}
|
||||||
<button class="tag" data-value="{{ cat }}">{{ cat }}</button>
|
<a
|
||||||
|
class="tag"
|
||||||
|
href="{{ category_urls[cat] }}"
|
||||||
|
data-value="{{ cat }}"
|
||||||
|
>{{ cat }}</a
|
||||||
|
>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<button class="tag tag-group" data-value="{{ entry.groups[0] }}">
|
<button class="tag tag-group" data-value="{{ entry.groups[0] }}">
|
||||||
{{ entry.groups[0] }}
|
{{ entry.groups[0] }}
|
||||||
|
|||||||
@@ -109,6 +109,15 @@ class TestBuild:
|
|||||||
"{% endblock %}",
|
"{% endblock %}",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
(tpl_dir / "category.html").write_text(
|
||||||
|
'{% extends "base.html" %}{% block content %}'
|
||||||
|
"<h1>{{ category.name }}</h1>"
|
||||||
|
"{% for entry in entries %}"
|
||||||
|
'<a href="{{ entry.url }}">{{ entry.name }}</a>'
|
||||||
|
"{% endfor %}"
|
||||||
|
"{% endblock %}",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
(tpl_dir / "llms.txt").write_text(
|
(tpl_dir / "llms.txt").write_text(
|
||||||
"# Awesome Python\n"
|
"# Awesome Python\n"
|
||||||
"\n"
|
"\n"
|
||||||
@@ -125,7 +134,7 @@ class TestBuild:
|
|||||||
tpl_dir = tmp_path / "website" / "templates"
|
tpl_dir = tmp_path / "website" / "templates"
|
||||||
shutil.copytree(real_tpl, tpl_dir)
|
shutil.copytree(real_tpl, tpl_dir)
|
||||||
|
|
||||||
def test_build_creates_single_page(self, tmp_path):
|
def test_build_creates_homepage_and_category_pages(self, tmp_path):
|
||||||
readme = textwrap.dedent("""\
|
readme = textwrap.dedent("""\
|
||||||
# Awesome Python
|
# Awesome Python
|
||||||
|
|
||||||
@@ -164,8 +173,8 @@ class TestBuild:
|
|||||||
|
|
||||||
site = tmp_path / "website" / "output"
|
site = tmp_path / "website" / "output"
|
||||||
assert (site / "index.html").exists()
|
assert (site / "index.html").exists()
|
||||||
# No category sub-pages
|
assert (site / "categories" / "widgets" / "index.html").exists()
|
||||||
assert not (site / "categories").exists()
|
assert (site / "categories" / "gadgets" / "index.html").exists()
|
||||||
|
|
||||||
def test_build_creates_root_discovery_files(self, tmp_path):
|
def test_build_creates_root_discovery_files(self, tmp_path):
|
||||||
readme = textwrap.dedent("""\
|
readme = textwrap.dedent("""\
|
||||||
@@ -205,12 +214,81 @@ class TestBuild:
|
|||||||
lastmods = [lastmod.text for lastmod in root.findall("sitemap:url/sitemap:lastmod", ns)]
|
lastmods = [lastmod.text for lastmod in root.findall("sitemap:url/sitemap:lastmod", ns)]
|
||||||
|
|
||||||
assert root.tag == "{http://www.sitemaps.org/schemas/sitemap/0.9}urlset"
|
assert root.tag == "{http://www.sitemaps.org/schemas/sitemap/0.9}urlset"
|
||||||
assert locs == ["https://awesome-python.com/"]
|
assert locs == [
|
||||||
assert len(lastmods) == 1
|
"https://awesome-python.com/",
|
||||||
assert start_date <= date.fromisoformat(lastmods[0]) <= end_date
|
"https://awesome-python.com/categories/widgets/",
|
||||||
|
]
|
||||||
|
assert len(lastmods) == 2
|
||||||
|
assert all(start_date <= date.fromisoformat(lastmod) <= end_date for lastmod in lastmods)
|
||||||
assert all(loc.startswith("https://awesome-python.com/") for loc in locs)
|
assert all(loc.startswith("https://awesome-python.com/") for loc in locs)
|
||||||
assert all("?" not in loc for loc in locs)
|
assert all("?" not in loc for loc in locs)
|
||||||
|
|
||||||
|
def test_build_creates_category_pages_with_metadata_and_links(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.
|
||||||
|
|
||||||
|
## Gadgets
|
||||||
|
|
||||||
|
_Gadget tools._
|
||||||
|
|
||||||
|
- [g1](https://example.com/g1) - A gadget.
|
||||||
|
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
Help!
|
||||||
|
""")
|
||||||
|
(tmp_path / "README.md").write_text(readme, encoding="utf-8")
|
||||||
|
self._copy_real_templates(tmp_path)
|
||||||
|
|
||||||
|
data_dir = tmp_path / "website" / "data"
|
||||||
|
data_dir.mkdir(parents=True)
|
||||||
|
stars = {
|
||||||
|
"owner/w2": {
|
||||||
|
"stars": 42,
|
||||||
|
"owner": "owner",
|
||||||
|
"last_commit_at": "2026-01-01T00:00:00+00:00",
|
||||||
|
"fetched_at": "2026-01-01T00:00:00+00:00",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
(data_dir / "github_stars.json").write_text(json.dumps(stars), encoding="utf-8")
|
||||||
|
|
||||||
|
build(tmp_path)
|
||||||
|
|
||||||
|
site = tmp_path / "website" / "output"
|
||||||
|
index_html = (site / "index.html").read_text(encoding="utf-8")
|
||||||
|
category_html = (site / "categories" / "widgets" / "index.html").read_text(encoding="utf-8")
|
||||||
|
parser = HeadMetadataParser()
|
||||||
|
parser.feed(category_html)
|
||||||
|
|
||||||
|
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."
|
||||||
|
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
|
||||||
|
assert "<h1>Widgets</h1>" in category_html
|
||||||
|
assert "Widget libraries." in category_html
|
||||||
|
assert 'href="https://example.com/w1"' in category_html
|
||||||
|
assert "A widget." in category_html
|
||||||
|
assert 'href="https://github.com/owner/w2"' in category_html
|
||||||
|
assert '<table class="table category-table">' in category_html
|
||||||
|
assert "42" in category_html
|
||||||
|
assert "2026-01-01T00:00:00+00:00" in category_html
|
||||||
|
|
||||||
def test_build_creates_markdown_alternate_without_sponsors(self, tmp_path):
|
def test_build_creates_markdown_alternate_without_sponsors(self, tmp_path):
|
||||||
readme = textwrap.dedent("""\
|
readme = textwrap.dedent("""\
|
||||||
# Awesome Python
|
# Awesome Python
|
||||||
|
|||||||
Reference in New Issue
Block a user