From 0c5c4e5a139d720a21a897a79f99dd1f29517e62 Mon Sep 17 00:00:00 2001
From: sideshowbarker
Date: Tue, 3 Feb 2026 18:56:57 +0900
Subject: [PATCH] Replace root redirect with spec listing index page and build
issues lists
Instead of redirecting to drafts.csswg.org, build-index.py now generates
an index page with search, sortable views, and per-spec resource links.
Features:
- Search/filter bar for finding specs instantly
- Recent (by date) and Grouped (by shortname family) sort modes
- Activity indicators and relative timestamps
- Per-spec links to GitHub issues, PRs, commit history
- Links to Disposition of Comments documents where they exist
Also adds a workflow step that runs bikeshed issues-list on all .bsi and
issues-*.txt source files, regenerating the Disposition of Comments HTML
pages during deployment.
Relates to https://github.com/w3c/csswg-drafts/issues/12054
---
.github/workflows/build-specs.yml | 6 +
bin/build-index.py | 389 +++++++++++++++++++++++++++++-
2 files changed, 383 insertions(+), 12 deletions(-)
diff --git a/.github/workflows/build-specs.yml b/.github/workflows/build-specs.yml
index de44a693cc01..ca318245146c 100644
--- a/.github/workflows/build-specs.yml
+++ b/.github/workflows/build-specs.yml
@@ -57,6 +57,12 @@ jobs:
SHORT_DATE="$(date --date=@"$TIMESTAMP" --utc +%F)"
bikeshed -f spec "$file" "${file%Overview.bs}index.html" --md-date="$SHORT_DATE" --md-Text-Macro="BUILTBYGITHUBCI foo"
done
+ - name: Build issues lists
+ run: |
+ for file in ./**/*.bsi ./**/issues-*.txt; do
+ echo " $file"
+ bikeshed issues-list "$file" || true
+ done
- name: Build index & symlinks
run: python ./bin/build-index.py
- run: rm -rf ./.git{,attributes,ignore}
diff --git a/bin/build-index.py b/bin/build-index.py
index be3a52ca200b..9b81e4fcd21d 100644
--- a/bin/build-index.py
+++ b/bin/build-index.py
@@ -2,18 +2,19 @@
All the drafts are built by the build-specs workflow itself.
This handles the rest of the work:
-* creates a root page that just redirects to the draft server root listing.
+* creates an index page listing all specs
* creates symlinks for unlevelled urls, linking to the appropriate levelled folder
-* builds timestamps.json, which provides a bunch of metadata about the specs which is consumed by some W3C tooling.
+* builds timestamps.json, which provides metadata about the specs
"""
+import glob
import json
import os
import os.path
import re
-import sys
import subprocess
from collections import defaultdict
+from datetime import datetime, timezone
import bikeshed
from html.parser import HTMLParser
@@ -122,6 +123,21 @@ def create_symlink(shortname, spec_folder):
pass
+def format_timestamp(ts):
+ """Format a Unix timestamp as a human-readable date string."""
+ dt = datetime.fromtimestamp(ts, tz=timezone.utc)
+ return dt.strftime("%Y-%m-%d")
+
+
+def escape_html(text):
+ """Escape HTML special characters."""
+ return (text
+ .replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace('"', """))
+
+
CURRENT_WORK_EXCEPTIONS = {
"css-conditional": 5,
"css-easing": 2,
@@ -159,6 +175,9 @@ def create_symlink(shortname, spec_folder):
metadata["dir"] = entry.name
metadata["currentWork"] = False
+ issues_files = sorted(f for f in glob.glob(os.path.join(entry.path, "issues-*.html"))
+ if not f.endswith(".bsi.html"))
+ metadata["issues"] = [os.path.basename(f) for f in issues_files]
specgroups[metadata["shortname"]].append(metadata)
# Reorder the specs with common shortname based on their level (or year, for
@@ -195,13 +214,359 @@ def create_symlink(shortname, spec_folder):
with open('./timestamps.json', 'w') as f:
json.dump(timestamps, f, indent = 2, sort_keys=True)
-with open("./index.html", mode='w', encoding="UTF-8") as f:
- f.write("""
+# Build the index page
+
+# Flatten all specs into a single list with full metadata
+all_specs = []
+for shortname, specgroup in specgroups.items():
+ group_size = len(specgroup)
+ for spec in specgroup:
+ dir_name = spec["dir"]
+ ts = timestamps.get(dir_name, 0)
+ title = spec["title"] or dir_name
+
+ doc_links = []
+ for fname in spec.get("issues", []):
+ label = fname.replace("issues-", "").replace(".html", "")
+ doc_links.append((f"./{dir_name}/{fname}", label))
+
+ all_specs.append({
+ "shortname": shortname,
+ "dir": dir_name,
+ "title": title,
+ "ts": ts if isinstance(ts, int) else 0,
+ "currentWork": spec["currentWork"],
+ "level": spec["level"],
+ "group_size": group_size,
+ "doc_links": doc_links,
+ })
+
+# Sort by timestamp descending (most recent first) for default view
+all_specs.sort(key=lambda s: s["ts"], reverse=True)
+
+# Generate HTML for each spec
+REPO = "https://github.com/w3c/csswg-drafts"
+spec_items = []
+for spec in all_specs:
+ t = escape_html(spec["title"])
+ d = spec["dir"]
+ sn = spec["shortname"]
+ ts = spec["ts"]
+ lv = spec["level"]
+ gs = spec["group_size"]
+ date_str = format_timestamp(ts) if ts else ""
+ iso_date = datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") if ts else ""
+
+ cw = ' Current Work' if spec["currentWork"] else ""
+
+ links = [
+ f'Issues',
+ f'PRs',
+ f'History',
+ ]
+ for url, label in spec["doc_links"]:
+ links.append(f'DoC: {escape_html(label)}')
+ links_html = ' \u00b7 '.join(links)
+
+ spec_items.append(
+ f' \n'
+ f'
\n'
+ f'
\n'
+ f'
{t}{cw}\n'
+ f'
{escape_html(d)}\n'
+ f'
\n'
+ f'
\n'
+ f'
{links_html}
\n'
+ f'
'
+ )
+
+specs_html = "\n".join(spec_items)
+
+HTML_START = """\
-
-Redirecting to the Drafts listing...
-
-
-""")
+
+
+
+ CSS Working Group Editor Drafts
+
+
+
+ CSS Working Group Editor Drafts
+
+
+
+
+
+
+
+
+
+"""
+
+HTML_END = """\
+
+ No matching specifications.
+
+
+
+"""
+
+with open("./index.html", mode='w', encoding="UTF-8") as f:
+ f.write(HTML_START)
+ f.write(specs_html)
+ f.write(HTML_END)