og:image pipeline — SHIPPED 2026-05-18 (was: parked sketch)
DARE.CO.UK · PARKED SKETCH · 2026-05-20
Mirrored from ~/.claude/.../memory/project_og_image_pipeline_parked.md. This is a design sketch parked for future build — read for context, not as a current deliverable.
2026-05-18 sketch THEN BUILT same day. Superseded by
project_og_image_standard_shipped.md— see that memory for the live state. This file kept as the audit/design record for retroactive traceability.
STATUS: SHIPPED 2026-05-18. See
project_og_image_standard_shipped.mdfor live surfaces, the script, and the per-renderer meta block. Original audit content below — useful for the design-rationale record.
Dan, 2026-05-18: “v2 seems like the right pipeline for us going forward, sketch/audit it first. It’s a beautiful thing we are building, adding a single line of code, resulting in high-touch experiences.”
Five-attribute definition
| Attribute | Definition |
|---|---|
| Purpose | Every gated portfolio surface gets a social-card preview without exposing private content |
| Goal | One line of HTML per renderer → high-touch share experience; preview reflects ACTUAL page content, refreshes on each deploy |
| Approach | Headless-Chrome capture (existing thumbnailer.py) authenticated via CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Access service token → upload to stable R2 key via existing grab pipeline → each renderer emits <meta property="og:image"> to that URL |
| Editorial voice | N/A (infrastructure layer) |
| Prompt needed | “What does this surface look like right now?” — answered by the most-recent capture |
The single-line promise
Every renderer adds exactly ONE LINE to its <head>:
�STASH6�
{surface-slug} is the slugified domain: dash-dare-co-uk, health-audreyinc-com, etc.
Everything else is infrastructure that runs once + maintains itself.
Architecture — what runs where
┌──────────────────────────────────────────────────────────┐
│ Each renderer's refresh.sh / cron tick │
│ ├─ existing: render index.html, deploy via wrangler │
│ └─ NEW: call dare_og_capture.py <url> <r2-key> │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ dare_og_capture.py (NEW, ~80 LOC) │
│ 1. Read CF Access service-token creds from 1Password │
│ 2. Headless Chrome → URL with CF-Access-Client-Id + │
│ CF-Access-Client-Secret headers │
│ 3. Wait for page render (DOMContentLoaded + 1s buffer) │
│ 4. Capture screenshot @ 1200×630 (OG card dimensions) │
│ 5. Save to /tmp/og-{slug}.png │
│ 6. Upload via ~/bin/grab to images.dare.co.uk/og/... │
│ (force overwrite — stable R2 key) │
│ 7. Print public URL │
└──────────────────────────────────────────────────────────┘
│
▼
https://images.dare.co.uk/og/{slug}.png
(PUBLIC; page stays Access-gated)
│
▼
Social-card scraper sees public preview ✓
Click → real page (Access-gated) ✓
What’s REUSED (load-bearing existing infra)
| Component | Where | Used as |
|---|---|---|
thumbnailer.py |
~/bin/ |
Headless-Chrome wrapper — three-layer error-page guard, page render, screenshot capture. Already battle-tested for dare’s path-hover previews. |
~/bin/grab |
toolkit | R2 upload + public URL. The og/ prefix becomes a new sub-tree under images.dare.co.uk. |
dare-images-root Worker (deployed 2026-05-17) |
~/Code/dare-images-root/ |
Already polite-landing-pages the root of images.dare.co.uk; doesn’t interfere with og/* keys. |
CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Access service token claude-session |
Existing on dashboard.dare.co.uk + devreports + new health/dash apps | Authenticates the headless-Chrome request through Access. ID in token db; secret retrieval via 1P op://. |
| 1P references | op://Code Shared/... |
Reads service token credentials; standard op run re-exec pattern. |
Nothing new at the infrastructure level. All five existing components compose; the new script is the orchestrator.
What’s NEW
~/bin/dare_og_capture.py— the orchestrator. ~80 LOC. Takes--url+--r2-key, returns the public R2 URL. Idempotent (overwriting same key).- 1P entry for the service token secret —
op://Code Shared/CF Access claude-session/{client_id, client_secret}. Likely already exists since the token is referenced in policies; may just need a metadata update. - Surface registry — small mapping in each renderer module:
OG_IMAGE_URL = "https://images.dare.co.uk/og/{slug}.png". Single constant per file. - Capture invocation — appended to each refresh.sh:
python3 ~/bin/dare_og_capture.py --url <surface-url> --r2-key og/{slug}.png. Five lines across five refresh scripts.
Audit dimensions
| Dimension | Read |
|---|---|
| Auth | Service-token authentication is the load-bearing assumption. If thumbnailer.py can’t pass CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF-Access-Client-* headers cleanly through headless Chrome, that’s the blocker. Quick to verify with a 10-line spike before committing to the full build. |
| Capture quality | 1200×630 is the standard OG card dimension. dare’s existing thumbnails are smaller; need to confirm headless-Chrome viewport accepts the larger size. Trivial config change if so. |
| Cache invalidation | R2 serves with CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF cache. If we overwrite the same key, CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF cache may serve stale image for hours. Two options: (a) accept hours of staleness (fine for “preview” UX); (b) version the URL (og/{slug}-{date}.png + update meta tag — defeats the one-line simplicity). Option (a) is cleaner. |
| Cost | Each capture: headless Chrome launch (~5-10 sec) + R2 upload (<1s). Per surface, per refresh. Daily total ≤30 captures × ~10 sec = ~5 min compute/day. Negligible. R2 storage: 5 surfaces × ~100KB per image = 0.5MB. Negligible. |
| Failure mode | Capture fails → no upload → og:image URL 404s on social-card scrape → no preview. Graceful degradation: link still works, just no preview. Acceptable. |
| Portability | Cross-portfolio out of the box. dogwood / audrey / client-engagement renderers add the same one line + the same refresh.sh invocation. No per-brand customization needed. |
| Security | Service token has scoped Access permission (non_identity decision; per-app policy). Compromise blast radius = read-access to the gated dashboards. Acceptable for a non-secret read surface. |
| Maintenance | Single script + 5 one-line additions. If thumbnailer.py upgrades, capture inherits. If grab tool changes, capture follows. Minimal ongoing maintenance. |
What’s elegant about it (Dan’s framing)
“adding a single line of code, resulting in high-touch experiences”
The one-line <meta> per renderer IS the entire visible footprint of the feature. Every other moving part (capture, auth, upload, hosting, caching) is INVISIBLE from the renderer’s perspective. The complexity hides behind a single declarative HTML attribute. That’s the right shape.
This composes with the broader portfolio stance:
- feedback_show_the_future_grey_it_out.md — the og:image is the page’s “show what’s there before the click”
- user_streamlined_purpose_defined_surfaces.md — every surface is purpose-defined; the og:image reflects that purpose visually
- feedback_glide_path_migrations.md — surface rename (dashboard → dash) auto-updates the preview on next refresh, no separate marketing-asset migration
Open design questions
- Capture cadence. Every refresh, or on-demand only? Hourly refreshes × 5 surfaces = 120 captures/day. Daily refresh seems sufficient for og:image freshness; hourly is overkill.
- Should
dash.*anddashboard.*(during glide path) share an og:image? Probably yes — same content. Captures the canonical URL only. - Mobile vs desktop viewport. OG cards display at fixed aspect ratio; desktop capture is the right shape. 1200×630 standard.
- Should the og:image include text overlay? Optional polish layer — render with a brand wordmark + page title overlaid on the screenshot. Adds ~50 LOC. v2.1.
- Per-page og:images for content within a surface? E.g. each report at
reports.dare.co.uk/<slug>could have its own preview. Out of scope for v1; revisit when single-page sharing matters.
Implementation sequence (when unparked)
| Phase | What ships | Effort |
|---|---|---|
| 0. Auth spike | 10-line script to verify thumbnailer.py + headless Chrome can authenticate via CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Access service-token headers | ~30 min |
| 1. Capture script | dare_og_capture.py standalone — manually invoked, single URL → R2 upload + public URL printed |
~2 hours |
| 2. Renderer integration | Add OG_IMAGE_URL constant + meta tag in 5 renderers (dare_health / audrey_health / dogwood_health / dare_cf_analytics / dare_dev_reports_publish) |
~30 min |
| 3. Refresh hooks | Append capture invocation to refresh scripts | ~30 min |
| 4. Verification | Trigger each refresh, scrape with curl to confirm meta tag emits + image URL resolves + image is current |
~30 min |
Total: ~4 hours for the full pattern across 5 surfaces. Most of the time is in the auth-spike + capture-script work; the per-surface integration is trivial.
Sibling memories
feedback_grabs_bucket_vs_thumbs_pipeline.md— the architectural rule: manual embeds ingrabs/, NOT_thumbs/. og:image is auto-generated infrastructure; sits atog/{slug}.png— separate sub-tree from grabs (manual) and_thumbs/(auto-pipeline-managed).feedback_cdn_root_polite_landing_pattern.md—images.dare.co.ukroot pattern. Theog/sub-tree slots cleanly under it.feedback_internal_seo.md— naming discipline;{surface-slug}follows the lowercase-hyphenated convention.feedback_glide_path_migrations.md— surface migrations (dashboard→dash) automatically inherit the new preview when refresh fires.user_streamlined_purpose_defined_surfaces.md— the og:image embodies each surface’s purpose visually.feedback_proactive_preview_deploy_when_asked_to_look.md— sibling stance: when Dan says “look-at this,” preview-deploy it. og:image is the always-on preview version of that pattern.
Resume conditions
- ✅ Auth spike validates that thumbnailer.py can pass CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Access service-token headers through headless Chrome. Without this, the architecture doesn’t work.
- ✅ A surface gets shared externally enough that the lack of preview becomes noticeable friction (currently most sharing is internal Dan-and-collaborators where bookmarks suffice).
- ✅ Client engagement deliverable wants high-touch share previews for stakeholder review.
Earliest qualifying trigger → Phase 0 spike, then linear through Phase 4. Single half-day sprint.