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.md for 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

  1. ~/bin/dare_og_capture.py — the orchestrator. ~80 LOC. Takes --url + --r2-key, returns the public R2 URL. Idempotent (overwriting same key).
  2. 1P entry for the service token secretop://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.
  3. Surface registry — small mapping in each renderer module: OG_IMAGE_URL = "https://images.dare.co.uk/og/{slug}.png". Single constant per file.
  4. 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

  1. 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.
  2. Should dash.* and dashboard.* (during glide path) share an og:image? Probably yes — same content. Captures the canonical URL only.
  3. Mobile vs desktop viewport. OG cards display at fixed aspect ratio; desktop capture is the right shape. 1200×630 standard.
  4. 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.
  5. 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

Resume conditions

Earliest qualifying trigger → Phase 0 spike, then linear through Phase 4. Single half-day sprint.

Source: parked_sketch_og_image_pipeline_2026-05-18.md · Rendered 2026-05-20 18:23