Move happiness.gf.cx source PDFs to R2 for stable URLs — trigger = voicing

DARE.CO.UK · PARKED SKETCH · 2026-06-07

Mirrored from ~/.claude/.../memory/parked_sketch_happiness_source_pdfs_to_r2_2026-06-06.md. This is a design sketch parked for future build — read for context, not as a current deliverable.

The 138MB Vitality Blueprint PDF (and any future >100MB source) can’t live in git — GitHub’s 100MB pre-receive hook blocks it. Pattern: gitignore locally, upload to R2 with a stable URL, reference that URL from the published article. Trigger to do this is the voicing pass, not the ingest step (no point burning R2 storage until the article is going live).


Sketch: when voicing a happiness.gf.cx article whose source PDF is >100MB (or any PDF where a stable public reference is editorially useful), upload source.pdf from ~/Code/happiness.gf.cx/body/<slug>/ to R2 and reference the R2 URL from the published article instead of carrying the PDF in git.

Why (Dan 2026-06-06):

“I agree — Future: move it to R2 with a stable URL when voicing the article (matches the broader gf.cx asset pattern).”

Today’s exhibit: vitality-blueprint-for-men-50-by-dr-emma-blake/source.pdf at 138MB tripped GitHub’s pre-receive hook → soft-reset + .gitignore was the immediate fix, but the asset is now orphaned in local-only land. The broader portfolio pattern is “stable URLs > carrying binaries in git” (matches snapshots.gf.cx JPEGs, og-card images, etc. — all live in R2).

Pattern this slots into:

Trigger: voicing pass (the editorial step that turns _clean.md + _clean.json into a rendered article). The voicer is the right caller to upload because it’s also the step that decides whether to expose the source link publicly.

Not now because: ingest dumps source.pdf for local reference during voicing; carrying 5 × ~30-138MB PDFs through R2 unconditionally would burn storage on PDFs that may never get cited. Decide at voice-time.

Update 2026-06-06 PM — Vitality Blueprint pilot hit the CDN, security layer, and DNS provider sitting in front of dare.co.uk." data-tip="Cloudflare — the CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages 25 MiB asset cap during the Chapter 1 pilot deploy. Wrangler 4.87 rejects any single asset > 25 MiB regardless of .assetsignore. Workaround applied: relocated body/vitality-blueprint-for-men-50-by-dr-emma-blake/source.pdf (138 MiB) to ~/Documents/happiness-gf-cx-source-pdfs/vitality-blueprint-for-men-50-by-dr-emma-blake.pdf — outside the deploy tree.

SHIPPED 2026-06-06 23:18 UTC during Chapter 2 deploy. R2 pattern is no longer parked — it’s live.

Canonical upload command (for next happiness PDF that needs the same treatment):

npx wrangler r2 object put \
  "dare-images/happiness-sources/<slug>.pdf" \
  --file ~/Documents/happiness-gf-cx-source-pdfs/<slug>.pdf \
  --content-type=application/pdf --remote

URL is then https://images.dare.co.uk/happiness-sources/<slug>.pdf — verify with curl -sI.

When to upload: at first article voicing that cites the source. Lead-magnet PDFs (the 3 small handouts in today’s inventory) don’t need R2 — they’ll fold into chapter articles, no standalone source line.

Build sketch (to fire at voicing):

# inside the voicing pipeline, after substrate render:
if (slug_dir / "source.pdf").exists():
    r2_key = f"happiness/{slug}/source.pdf"
    upload_to_r2(slug_dir / "source.pdf", bucket="gfcx-public", key=r2_key)
    article_meta["source_pdf_url"] = f"https://r2.gf.cx/{r2_key}"

R2 bucket + base URL TBD at build time — likely reuses whatever bucket snapshots.gf.cx + dare social-card publish to.

Sister memories:

Source: parked_sketch_happiness_source_pdfs_to_r2_2026-06-06.md · Rendered 2026-06-07 06:30