Render local · push static · serve at the edge
DARE.CO.UK · ARCHITECTURE APPROACH · 2026-05-16
A declaration of how the dare portfolio renders, deploys, and serves. The shape compounds across dare.co.uk, dogwood.house, audreyinc.com, and any client engagement — different scripts, same rhythm.
The principle in one line
Render on the developer’s laptop. Push the rendered output as static files. Serve from Cloudflare’s edge. Reserve cloud compute (Cloud Run, Workers) for the narrow problems where local rendering genuinely can’t.
That’s the whole stack. It’s small on purpose. Each layer does one thing well and stays out of the others’ way.
The shape, by surface
| Surface | Render | Source-of-truth | Deploy → Serve |
|---|---|---|---|
dashboard.dare.co.uk |
~/bin/dare_cf_analytics.py — Python on Dan’s Mac, fetches Cloudflare Analytics GraphQL, renders one large HTML file |
rendered artefact at ~/Downloads/dare_dashboard.html; no git mirror — regenerated from source data each run |
wrangler pages deploy → CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages project dare-dashboard → CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF edge |
devreports.dare.co.uk |
~/bin/dare_dev_reports_publish.py — bundles markdown reports written by every other tool, renders to HTML |
git repo xlab-co/devreports-content — mirror of synced ~/Downloads/ reports, pushed every 30 min by dare_devreports_sync.sh |
(a) local wrangler pages deploy, OR (b) Cloud Run Job devreports-publish daily 06:00 ET as redundancy. CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages project dare-dev-reports → CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF edge |
dare.co.uk (main site) |
None — already-static HTML pre-built and committed | git repo xlab-nyc/dare-co-uk on main |
CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages auto-builds on push → CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF edge |
dare-time-poc.pages.dev |
~/bin/dare_time_page.py — static generator using Python’s zoneinfo stdlib, no external API |
none — generator output is ephemeral and regenerable from script | wrangler pages deploy → CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages → CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF edge |
dare-dashboard-v2-poc.pages.dev |
Observable Framework via npm run build — local Node |
git repo (local-only commit while POC) | wrangler pages deploy dist → CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages → CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF edge |
Why this shape
Rendering on your laptop means you have full debugger access. No log-export rituals when something breaks. Print statements are real-time. The headless-Chrome thumbnail capture runs against the same Chrome you have open. Iteration is seconds, not minutes.
Static files mean Cloudflare Pages can do the one thing it’s best at: global edge delivery. Cloudflare doesn’t try to be your build system; it’s a CDN with HTTPS, Brotli compression, and roughly free pricing at this traffic shape. The render layer and the serve layer are decoupled — change the renderer, the serve layer doesn’t notice.
Source-of-truth as git, not as the renderer’s working directory. When today’s session shipped 41 backlogged devreports in one wave, that worked because the markdown files are mirrored to xlab-co/devreports-content — a git repo any other render path (Cloud Run, a colleague’s laptop, a future CI worker) can clone and re-render against. The renderer becomes interchangeable; the data doesn’t.
Cloud compute reserved for the narrow case. GCP touches the pipeline at exactly one point: the daily 06:00 ET Cloud Run Job that re-renders devreports as a redundancy. It exists for the “Dan’s Mac is asleep” case, not as the primary path. Cost: dollars-per-month. Mental footprint: a single Job we can read end-to-end in one screen.
What compounds across the portfolio
The same shape works for every portfolio site:
- dogwood.house —
~/bin/dogwood_*.pywould render booking-page HTML, push to the dogwood CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages project, serve from CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF edge. Same rhythm; different content + cities (NYC pickup, Hamptons drop-off, Greenwich layovers). - audreyinc.com — Shopify hosts the storefront, but the editorial layer (gift guides, content marketing, agent-discovery surfaces) renders the same way: local Python → static HTML → CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages → edge.
- Client work — every client deliverable that’s “a small reporting / dashboard / content surface” inherits the shape from day one. CLI params, dated outputs, consistent markdown format. Per
project_hygiene_toolkit.md: design site-portable from the first commit.
The toolkit ships across surfaces because the rendering layer is just Python that writes HTML. No framework lock-in. No vendor lock-in beyond Cloudflare’s edge (and CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF is interchangeable with Vercel, Netlify, or any other static host in an afternoon).
What changes when a surface needs more
The architecture is honest about its boundaries:
- Live data on a static page — escape hatch is a small Cloudflare Worker that fetches at request time. The static page does 99% of the work; the Worker handles the 1% that needs freshness. (Today’s dashboard uses this implicitly via the CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Analytics GraphQL query running at render time, but a “Live” view toggle could lift that to client-side.)
- User accounts / per-visitor state — moves to a separate service (CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Workers, a small backend); the static surface stays static. The boundary between “everyone sees the same page” and “this user sees their page” is also the boundary between static and dynamic in the stack.
- Real-time interactivity — client-side JavaScript in the static page handles most of it (see today’s time-poc: rotation, click-to-Google-Calendar, NOW strip ticker — all browser-native, zero round-trips). When that’s insufficient, a Worker stands in.
The pattern: static by default, dynamic by exception, justified by the user-facing win, not by the engineering excitement.
Today’s session as a stress test
Eleven hours of iterations shipped through this pipeline today:
- Seven dashboard refreshes (
dare_dashboard_refresh.sh→ wrangler → CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages) - Three devreports republishes (sync → publish → wrangler → CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages)
- Eight time-poc redeploys (regenerate → wrangler → CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages)
- Two dare.co.uk commits (
robots.txtSitemap directives, About sitemap link, H1 break removal, contact-page intro-aside link) - One A/B preview ship for the alert-indicator placement
- One narrator regenerate (after the snapshot-window + HTML-tag-leak fixes)
Each one was: edit local file → run a script that wrote new HTML → wrangler upload → live in <10 seconds. No CI queue. No build farm. No 30-minute pipeline wait. The friction surface — the place where iteration slows down — was the decision to ship, not the act of shipping.
What this tells us
Render-local + push-static + edge-serve is a compounding choice. Each tool we build here drops cleanly into every other portfolio surface. The cost of building a new tool against this pattern is the cost of writing the Python script — there’s no infra setup, no service to provision, no API gateway to configure. Every hour spent on a render-local tool is an hour of permanent capability across the portfolio.
The architecture isn’t aspirational; it’s load-bearing. Today shipped fixes for two narrator bugs (phantom 0% cache; HTML-tag leak), a new dashboard section, a new tool (time-poc), a daily-hygiene runner, a robots.txt fix, an A/B preview, and the matching dev-reports session report — all using exactly this pipeline. No friction in the shipping; the only constraint was design judgement.
Watch items
- Cloud Run Job as a single point of redundancy — if the daily 06:00 ET job silently fails, devreports drift behind without a loud signal. Worth instrumenting with a “did the catalog timestamp move today?” hygiene check (small follow-up to
dare_daily_hygiene.py). - The thumb cache is laptop-local — wiping the Mac wipes the cache; the next dashboard refresh regenerates from zero (a few minutes of headless Chrome). Acceptable cost; worth knowing.
- The CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages project list grows as POCs ship (today added:
dare-time-poc,dare-ab-edge-health,dare-dashboard-v2-poc,dare-ab-edge-healthre-used for the indicator A/B). Worth a quarterly sweep to delete decided-against POCs. The dashboard for tracking them is justwrangler pages project list.
What’s next
- Co-locate the time-poc under
devreports.dare.co.uk/time/— one less CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages project, cleaner URL, same architecture. - Audit which tools in
~/bin/are still living in~/Downloads/-adjacent paths — TCC restrictions have bitten three times this week. The render-local principle requires the scripts to actually be readable by the runner; launchd processes can’t always read user-protected dirs (perfeedback_downloads_is_least_favourite.md+feedback_sync_no_new_files_is_suspicious.md). - Apply this shape to the first audrey editorial gift-guide piece when it lands — same generator pattern, different content axis. Per
project_audrey_agent_discoverability.md.
The aphorism
The cheapest infrastructure is the kind your laptop already runs. The cheapest CDN is the kind that charges in tens of dollars a year. Build at the intersection and you spend your time on the thing that compounds — the design judgement — instead of on the plumbing.
Source: ~/Downloads/dare_strategy_render_pipeline_approach_2026-05-16.md. Companion to project_hygiene_toolkit.md (the generator pattern), feedback_upstream_fix_downstream_compounding.md (the compounding rule), feedback_stdlib_over_external_api.md (the “stdlib first” principle that keeps the local renderer light). Written 2026-05-16 in conversation with Dan, in the voice of dare.