diff --git a/website/build.py b/website/build.py
index cf759283..f1d00c93 100644
--- a/website/build.py
+++ b/website/build.py
@@ -9,7 +9,7 @@ from pathlib import Path
from typing import TypedDict
from jinja2 import Environment, FileSystemLoader
-from readme_parser import parse_readme
+from readme_parser import parse_readme, parse_sponsors
class StarData(TypedDict):
@@ -147,6 +147,7 @@ def build(repo_root: str) -> None:
break
parsed_groups = parse_readme(readme_text)
+ sponsors = parse_sponsors(readme_text)
categories = [cat for g in parsed_groups for cat in g["categories"]]
total_entries = sum(c["entry_count"] for c in categories)
@@ -189,6 +190,7 @@ def build(repo_root: str) -> None:
total_categories=len(categories),
repo_stars=repo_stars,
build_date=datetime.now(timezone.utc).strftime("%B %d, %Y"),
+ sponsors=sponsors,
),
encoding="utf-8",
)
diff --git a/website/readme_parser.py b/website/readme_parser.py
index 4f36ed77..c736b7cc 100644
--- a/website/readme_parser.py
+++ b/website/readme_parser.py
@@ -37,6 +37,12 @@ class ParsedGroup(TypedDict):
categories: list[ParsedSection]
+class ParsedSponsor(TypedDict):
+ name: str
+ url: str
+ description: str # inline HTML, properly escaped
+
+
# --- Slugify ----------------------------------------------------------------
_SLUG_NON_ALNUM_RE = re.compile(r"[^a-z0-9\s-]")
@@ -350,6 +356,80 @@ def _parse_grouped_sections(
return groups
+_SPONSOR_SEP_RE = re.compile(r"^\s*[:\-\u2013\u2014]\s*")
+
+
+def _find_link_deep(node: SyntaxTreeNode) -> SyntaxTreeNode | None:
+ """Find the first link anywhere in the subtree (including nested in strong/em)."""
+ for child in node.children:
+ if child.type == "link":
+ return child
+ found = _find_link_deep(child)
+ if found:
+ return found
+ return None
+
+
+def _parse_sponsor_item(inline: SyntaxTreeNode) -> ParsedSponsor | None:
+ """Parse `**[name](url)**: description` (or `[name](url) - description`)."""
+ link = _find_link_deep(inline)
+ if link is None:
+ return None
+ name = render_inline_text(link.children)
+ url = link.attrGet("href") or ""
+
+ split_idx = None
+ for i, child in enumerate(inline.children):
+ if child is link or _find_link_deep(child) is link:
+ split_idx = i
+ break
+ if split_idx is None:
+ return None
+ desc_html = render_inline_html(inline.children[split_idx + 1 :])
+ desc_html = _SPONSOR_SEP_RE.sub("", desc_html)
+ return ParsedSponsor(name=name, url=url, description=desc_html)
+
+
+def parse_sponsors(text: str) -> list[ParsedSponsor]:
+ """Parse the `# Sponsors` section of README.md into a list of sponsors.
+
+ Expects bullets in the form `**[name](url)**: description`.
+ Returns [] if no Sponsors section exists.
+ """
+ md = MarkdownIt("commonmark")
+ tokens = md.parse(text)
+ root = SyntaxTreeNode(tokens)
+ children = root.children
+
+ start_idx = None
+ end_idx = len(children)
+ for i, node in enumerate(children):
+ if node.type == "heading" and node.tag == "h1":
+ title = _heading_text(node).strip().lower()
+ if start_idx is None and title == "sponsors":
+ start_idx = i + 1
+ elif start_idx is not None:
+ end_idx = i
+ break
+ if start_idx is None:
+ return []
+
+ sponsors: list[ParsedSponsor] = []
+ for node in children[start_idx:end_idx]:
+ if node.type != "bullet_list":
+ continue
+ for list_item in node.children:
+ if list_item.type != "list_item":
+ continue
+ inline = _find_inline(list_item)
+ if inline is None:
+ continue
+ sponsor = _parse_sponsor_item(inline)
+ if sponsor:
+ sponsors.append(sponsor)
+ return sponsors
+
+
def parse_readme(text: str) -> list[ParsedGroup]:
"""Parse README.md text into grouped categories.
diff --git a/website/static/style.css b/website/static/style.css
index ee24ce94..2bae2c95 100644
--- a/website/static/style.css
+++ b/website/static/style.css
@@ -392,6 +392,109 @@ kbd {
outline-offset: 3px;
}
+.sponsor-band {
+ padding-block: clamp(2.5rem, 5.5vw, 4rem);
+ background:
+ linear-gradient(180deg, var(--bg-paper-strong), var(--bg-paper));
+ border-bottom: 1px solid var(--line);
+}
+
+.sponsor-shell {
+ display: grid;
+ grid-template-columns: minmax(0, 14rem) minmax(0, 1fr);
+ gap: clamp(1.5rem, 5vw, 3.5rem);
+ align-items: start;
+}
+
+.sponsor-meta {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.sponsor-meta .section-label {
+ margin-bottom: 0;
+}
+
+.sponsor-become {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ align-self: start;
+ color: var(--ink-soft);
+ font-size: var(--text-sm);
+ font-weight: 700;
+ letter-spacing: 0.01em;
+ border-bottom: 1px solid var(--line-strong);
+ padding-bottom: 0.2rem;
+ transition:
+ color 180ms ease,
+ border-color 180ms ease;
+}
+
+.sponsor-become:hover {
+ color: var(--accent-deep);
+ border-bottom-color: var(--accent);
+}
+
+.sponsor-become-arrow {
+ transition: transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
+}
+
+.sponsor-become:hover .sponsor-become-arrow {
+ transform: translateX(0.3rem);
+}
+
+.sponsor-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: grid;
+ gap: clamp(1.5rem, 3vw, 2.25rem);
+}
+
+.sponsor {
+ display: grid;
+ gap: 0.65rem;
+}
+
+.sponsor-link {
+ display: inline-flex;
+ align-items: baseline;
+ gap: 0.5rem;
+ color: var(--ink);
+ transition: color 180ms ease;
+}
+
+.sponsor-link:hover {
+ color: var(--accent-deep);
+}
+
+.sponsor-name {
+ font-family: var(--font-display);
+ font-size: clamp(2.25rem, 4.2vw, 3.25rem);
+ font-weight: 600;
+ line-height: 0.95;
+ letter-spacing: -0.025em;
+}
+
+.sponsor-desc {
+ color: var(--ink-soft);
+ font-size: clamp(1rem, 1.5vw, 1.1rem);
+ line-height: 1.55;
+}
+
+.sponsor-desc a {
+ color: var(--accent-deep);
+ text-decoration: underline;
+ text-decoration-color: var(--accent-underline);
+ text-underline-offset: 0.18em;
+}
+
+.sponsor-desc a:hover {
+ color: var(--accent);
+}
+
.results-intro h2,
.final-cta h2 {
font-family: var(--font-display);
@@ -1025,7 +1128,8 @@ th[data-sort].sort-asc::after {
}
.hero-grid,
- .results-intro {
+ .results-intro,
+ .sponsor-shell {
grid-template-columns: 1fr;
}
diff --git a/website/templates/index.html b/website/templates/index.html
index 1003c1a7..87cfbf29 100644
--- a/website/templates/index.html
+++ b/website/templates/index.html
@@ -65,6 +65,40 @@
{% endblock %}
{% block content %}
+{% if sponsors %}
+
+{% endif %}
+