What changed, and why
The drop-and-share site is the last static workload left on the cluster. glassbox went first, then 2ls.tech. Most of this PR is the same shape they had: the container path goes (Dockerfile, nginx.conf, .dockerignore, an image-only CI workflow) and Vercel's git integration serves public/ straight from the repo.
One thing gives this round extra weight. Pushing this repo is how every read-thru guide publishes. Making the push itself the deploy doesn't just simplify hosting; it deletes the separate cluster-rollout step from that whole workflow.
Two pieces are genuinely new. cleanUrls is load-bearing, since every shared link (/read-thru/<repo>/pr-<n>-<slug>, no extension) works today only because nginx tried $uri.html. And autoindex has no Vercel equivalent at all, so a small build script now writes the directory listings. Those two carry the review weight below.
vercel.json: config for everything nginx did
Five nginx behaviors, five destinations — two of them into files in this PR:
Serve public/, clean URLs, and run the index generator at build.
| nginx rule | where it lives now |
|---|---|
try_files $uri $uri.html $uri/ (extension-less URLs) | cleanUrls: true — every shared read-thru link depends on this |
autoindex on (directory listings, root included) | generate-index.mjs, run as the buildCommand (next section) |
Cache-Control: no-cache on content | Vercel's static default (public, max-age=0, must-revalidate), right for files that mutate in place under stable names |
gzip on + types | edge compression, no config |
/healthz for the k8s probes | dropped — no pod, no probes |
generate-index.mjs: autoindex, rebuilt at build time
Vercel serves files. It will not list a directory, ever, and the root of this site is a directory listing. So the generator walks public/ on every deploy and writes an index.html (entries, sizes, dates) into each directory that doesn't already own one. / keeps working. So does /read-thru/. A freshly dropped file shows up in the listings with nobody maintaining an index by hand, which is the property autoindex actually provided.
The shallow-clone handling and date helper, the ownership rule, and the descend loop; the HTML-emitting middle is folded.
generate-index.mjs · 119 lines
⋯ 22 lines hidden (lines 1–22)
⋯ 9 lines hidden (lines 62–70)
⋯ 30 lines hidden (lines 81–110)
What's gone, and the README
The deleted CI workflow was image-only, the same shape 2ls.tech had: one job building ghcr.io/mseeks/static for the cluster pod to pull, nothing to gate on. It goes whole. Dockerfile, nginx.conf (42 lines), and .dockerignore go with it, and the README's local preview drops its docker run for the generator plus a plain Python file server.
The whole README: what the site is, how it serves, local preview.
README.md · 22 lines
How to verify it
The generator ran locally against the real tree, twice. The root listing came out with all four top-level directories and their last-commit dates (froot at 2026-06-08, straight from git). A file added between runs showed up in the regenerated listing, proving the marker-based idempotence, and a probe named tmp probe#1.txt produced a correctly encoded href. vibe-themer's page stayed exactly as committed, and git status showed nothing generated, which is the gitignore doing its job.
Three requests settle it after cutover. The root / should render a listing rather than a 404 — that is the autoindex replacement working. An existing shared link like /read-thru/zo/pr-21-2ls-to-vercel should serve its page with no extension, which proves cleanUrls. And the response should say server: Vercel. One bonus: the guide refreshes that sat in main waiting for a cluster rollout go live the moment Vercel first deploys. Ordering (project first, DNS second, namespace deletion last) lives in mseeks/zo#23.