The 250kb Club is a curated directory of websites whose total page weight is ≤ 256 KB. It periodically re-verifies member sites via YellowLabTools, stores results as Zola-flavored Markdown pages, and builds a static site with Zola. Maintenance (join/update/leave) happens through structured GitHub issues.
.
├── analyser/ # Deno TypeScript analysis engine
│ ├── metrics.ts # YellowLabTools API client (run/status/results)
│ └── toolkit.ts # File I/O, URL helpers, retry logic
├── content/ # ~500 auto-generated .md pages (TOML frontmatter)
│ └── _index.md # Section index: sort_by="weight", paginate_by=1000
├── public/ # Zola build output (one dir per member + static)
├── static/ # Static assets copied to site root
│ ├── favicon.png
│ ├── robots.txt
│ └── *badge*.{png,gif} # 88×31 & larger member badges (8 files)
├── templates/ # Zola Tera templates
│ ├── base.html # Full HTML skeleton + inline CSS + analytics
│ ├── index.html # Homepage: intro, info popup, member <ol>
│ └── page.html # Per-member detail: stats, badge embed codes
├── .github/ISSUE_TEMPLATE/ # Structured issue forms for membership
├── pages.txt # Source-of-truth URL list (~457 lines)
├── config.toml # Zola configuration
├── index.ts # Deno orchestration script (the analyser CLI)
├── index.d.ts # Shared TypeScript types
├── refresh-page.sh # Full pipeline: YLT → analyse → build → commit
├── .woodpecker.yml # CI: builds site with Zola, deploys to host volume
├── deno.json / deno.lock # Deno import map & lockfile
└── package.json # npm scripts: dev/build/update-pages/ylt
| Mode | Entry | How |
|---|---|---|
| Analyse & update pages | index.ts |
deno run --allow-read --allow-write --allow-net index.ts (or yarn update-pages) |
| Local dev server | config.toml |
zola serve (or yarn dev) |
| Production build | config.toml |
zola build (or yarn build) |
| Full refresh pipeline | refresh-page.sh |
Docker YLT → Deno analyse → Zola build → git commit+push |
| CI deploy | .woodpecker.yml |
Runs zola build -o /mnt/dist --force on push |
| YellowLabTools | Docker | yarn ylt starts YLT on port 8383, mounts ./yltresults |
PageRecord— persisted result:title,date,updated,weight(bytes), andextra(source,ratio,sizein KB).Status— YLT job lifecycle:awaiting | running | complete | failed+url.Metric— YLT results split intoscores(pageWeight, domComplexity, globalScore, etc.) andmetrics(body sizes: html, css, js, images, video, fonts, etc.).
pages.txt ──► index.ts (orchestrator)
│
├─ for each URL: check if existing record < 1 week old
│ │ (RECHECK_THRESHOLD in index.ts)
│ │
│ ├─ [stale/missing] → POST job to YLT (metrics.ts)
│ │ └─ poll status in batches of 3 (PARALLEL_JOBS)
│ │ └─ on complete: GET results (metrics.ts)
│ │
│ ├─ contentLength > 256 KB (REJECT_THRESHOLD)?
│ │ └─ yes → removeRecord() (toolkit.ts)
│ │ └─ no → writeRecord() (toolkit.ts)
│ │
│ └─ [fresh] → skip
│
└─ live CLI status table (Cliffy libraries)
content/*.md (TOML frontmatter)
│
▼
Zola (config.toml)
│
├── templates/base.html (skeleton, inline CSS)
├── templates/index.html (paginates _index.md, sort by weight)
└── templates/page.html (single member detail)
│
▼
public/ (static HTML site)
GitHub Issue (request/update/cancel membership)
│
▼
Human curator edits pages.txt
│
▼
refresh-page.sh (or CI) runs analysis → build → deploy
URLs (pages.txt)
│
│ index.ts reads lines starting with "http"
▼
YellowLabTools Docker API (localhost:8383)
│ POST /api/runs → runId
│ GET /api/runs/:runId → status
│ GET /api/results/:runId → Metric JSON
▼
index.ts computes:
- contentSize = htmlSize + imageSize + videoSize
- ratio = contentSize / bodySize × 100
- weight = bodySize (from YLT), size = weight / 1024 (KB)
│
│ reject if contentLength > 262144 bytes
▼
content/<domain>.md (TOML frontmatter, empty body)
│
│ Zola reads frontmatter fields: title, date, updated, weight, extra.*
▼
templates/*.html render:
- index.html: paginator.pages sorted by weight, shows size + ratio bars
- page.html: per-page stats + badge embed snippets
▼
public/<domain>/index.html (final static site, ~500 pages + homepage)
Content size calculation: contentSize = htmlSize + imageSize + videoSize.
This is "actual content" — CSS, JS, fonts count toward total weight but not
content. The displayed ratio = contentSize / totalWeight × 100.
| Dependency | Role | How used |
|---|---|---|
| Zola | Static site generator | Reads content/*.md + templates/, outputs public/. Configured via config.toml. |
| YellowLabTools | Page weight analysis | Docker container on port 8383. Uses Phantomas. analyser/metrics.ts calls its REST API. |
| Deno | TypeScript runtime for analyser | Runs index.ts. Uses deno.land/std@0.130.0/encoding/toml.ts for TOML I/O. Zero npm deps for the analyser. |
| @cliffy/ansi & @cliffy/table | Terminal UI | Coloured status table in index.ts during analysis runs. |
| GoatCounter | Privacy-friendly analytics | Script tag in templates/base.html. |
| Woodpecker CI | CI/CD | .woodpecker.yml builds site on push, deploys to host volume. |
| Docker | Container runtime | Runs YellowLabTools locally (yarn ylt). |
| Node.js / Yarn | Package manager | Wraps zola and deno commands as npm scripts. |
-
Flat
content/directory, one .md per site. Simple file-based database. No actual Markdown body — all data lives in TOML frontmatter. Zola treats each as a page;_index.mdprovides a paginated listing sorted byweight. -
256 KB threshold based on
contentLength, not total transfer.contentLength= compressed body size (what actually transits the wire). This is the fairest metric for real-world page weight. Checked inindex.tsasREJECT_THRESHOLD = 262144bytes. -
Content ratio as a quality signal. The analyser computes
htmlSize + imageSize + videoSizeas "content" vs. total weight (which includes CSS, JS, fonts, etc.). Higher ratio = less bloat. Displayed as a horizontal bar in the member listing. -
1-week recheck interval (
RECHECK_THRESHOLD). Each page is only re-analysed if its record is older than 7 days. Prevents hammering YLT and member sites on every run. -
TOML frontmatter as the data storage format. Zola natively understands TOML in
.mdfiles. The analyser writes TOML (via Deno std lib), Zola reads it. No database, no JSON files — just Markdown files with metadata headers. -
Inline CSS in
base.html. The entire site's stylesheet lives in a single<style>tag. Eliminates an external CSS request, keeping the site itself lightweight and aligned with the project's philosophy. -
No JavaScript on the production site. (Aside from GoatCounter analytics.) The info popup uses a CSS-only
:targettoggle. The site practices what it preaches. -
pages.txtas the human-editable source of truth. Curators add/remove URLs as plain text lines. Only the analyser script mutatescontent/. GitHub issue templates provide a structured submission process. -
Woodpecker CI (not GitHub Actions) for deployment. Self-hosted CI builds the Zola site and writes directly to a host volume, keeping the pipeline under the operator's control.
-
Badge economy. Member pages offer 88×31 and larger badges in multiple colour schemes. The smallest is a 0.25 KB PNG. Even ASCII text badges are provided (~120–260 bytes). All badge images live in
static/.