feat(website): add source type badges for non-GitHub entries

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>
This commit is contained in:
Vinta Chen
2026-03-22 02:03:00 +08:00
parent f7b51a6207
commit 666f6e52d0
3 changed files with 41 additions and 3 deletions

View File

@@ -39,6 +39,24 @@ class StarData(TypedDict):
GITHUB_REPO_URL_RE = re.compile(r"^https?://github\.com/([^/]+/[^/]+?)(?:\.git)?/?$") GITHUB_REPO_URL_RE = re.compile(r"^https?://github\.com/([^/]+/[^/]+?)(?:\.git)?/?$")
SOURCE_TYPE_DOMAINS = {
"docs.python.org": "stdlib",
"gitlab.com": "GitLab",
"bitbucket.org": "Bitbucket",
}
def detect_source_type(url: str) -> str | None:
"""Detect source type from URL domain. Returns None for GitHub URLs."""
if GITHUB_REPO_URL_RE.match(url):
return None
for domain, source_type in SOURCE_TYPE_DOMAINS.items():
if domain in url:
return source_type
if "github.com" not in url:
return "External"
return None
def extract_github_repo(url: str) -> str | None: def extract_github_repo(url: str) -> str | None:
"""Extract owner/repo from a GitHub repo URL. Returns None for non-GitHub URLs.""" """Extract owner/repo from a GitHub repo URL. Returns None for non-GitHub URLs."""
@@ -57,14 +75,19 @@ def load_stars(path: Path) -> dict[str, StarData]:
def sort_entries(entries: list[dict]) -> list[dict]: def sort_entries(entries: list[dict]) -> list[dict]:
"""Sort entries by stars descending, then name ascending. No-star entries go last.""" """Sort entries by stars descending, then name ascending.
Three tiers: starred entries first, stdlib second, other non-starred last.
"""
def sort_key(entry: dict) -> tuple[int, int, str]: def sort_key(entry: dict) -> tuple[int, int, str]:
stars = entry["stars"] stars = entry["stars"]
name = entry["name"].lower() name = entry["name"].lower()
if stars is None: if stars is not None:
return (0, -stars, name)
if entry.get("source_type") == "stdlib":
return (1, 0, name) return (1, 0, name)
return (0, -stars, name) return (2, 0, name)
return sorted(entries, key=sort_key) return sorted(entries, key=sort_key)
@@ -105,6 +128,7 @@ def extract_entries(
"stars": None, "stars": None,
"owner": None, "owner": None,
"last_commit_at": None, "last_commit_at": None,
"source_type": detect_source_type(url),
"also_see": entry["also_see"], "also_see": entry["also_see"],
} }
seen[url] = merged seen[url] = merged

View File

@@ -334,6 +334,19 @@ th[data-sort].sort-asc::after {
text-align: right; text-align: right;
} }
/* === Source Badges === */
.source-badge {
display: inline-block;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.03em;
color: var(--text-muted);
background: var(--bg-input);
padding: 0.15rem 0.45rem;
border-radius: 3px;
white-space: nowrap;
}
/* === Arrow Column === */ /* === Arrow Column === */
.col-arrow { .col-arrow {
width: 2.5rem; width: 2.5rem;

View File

@@ -98,6 +98,7 @@
</td> </td>
<td class="col-stars"> <td class="col-stars">
{% if entry.stars is not none %}{{ "{:,}".format(entry.stars) }}{% {% if entry.stars is not none %}{{ "{:,}".format(entry.stars) }}{%
elif entry.source_type %}<span class="source-badge">{{ entry.source_type }}</span>{%
else %}&mdash;{% endif %} else %}&mdash;{% endif %}
</td> </td>
<td <td