Skip to content

Link In Bio: image/hero block kind

Branch: links-hero-image (worktree ~/forge-suite-wt/links-hero-image, dev DB clone ct_links_hero_image). NOT merged, NOT deployed.

What shipped

A new Link In Bio block kind, "image": a full-width hero/cover image for teaser/landing pages (e.g. the merch-teaser signup page). Data shape: { url: string; alt?: string }.

Files changed

  • lib/links-store.tsBlockKind union + BLOCK_KINDS array: added "image".
  • components/apps/link-in-bio/BlockEditor.tsxBLOCK_META["image"] (label "Image", icon image), ADD_ORDER includes it, BlockFields case (URL input + optional alt-text input), defaultData("image") returns { url: "", alt: "" }.
  • components/apps/link-in-bio/LinkBioPreview.tsx — render case for "image": a single <img className="lbp-hero" src={data.url} alt={data.alt}> (renders nothing if no url set).
  • lib/links/bio-css.ts.lbp-hero { display:block; width:100%; height:auto; border-radius:12px; object-fit:cover; margin:0; }.
  • db/schema.ts — updated the kind column comment to list "image" (no schema shape change; data stays jsonb).
  • db/migrations/20260711T203954_links_hero_image_links_blocks_kind_check_widen_image.sqlnew migration, was NOT anticipated in the task brief. links.blocks has a blocks_kind_check CHECK constraint (added 20260710T052433 to admit published-grid); it hard-rejects any kind not in its literal list, so "image" needed the same DROP-IF-EXISTS + ADD widening. Applied to the dev clone; will apply cleanly on merge via the normal migration runner. This is the one thing that deviates from "no DB migration needed" in the task brief — the brief was right that blocks.data (jsonb) needs no migration, but missed the kind CHECK constraint.
  • lib/links/render.ts — confirmed untouched, as specified. buildLinkMap only special-cases link / affiliate-card / published-grid; every other kind (including image) passes through unmodified, and renderPage reuses LinkBioPreview in mode="publish", so the hero renders identically in the deployed HTML with zero render.ts changes.

How it was verified (end-to-end, not hand-waved)

  1. Bootstrapped the slot per the setup script, but port 3064 was already occupied by a stale, already-merged feat/account-deletion worktree's dev server (unrelated to this task). Used port 3065 instead for this slot's .env.local and next dev --webpack -p 3065. No other process was touched.
  2. creatortrack_dev_base clone was missing ~40 migrations (schema drift from the base snapshot). Ran forge_workspace_migrate.py against ct_links_hero_image before the app would boot without 500s — this is expected slot hygiene, not part of the feature change.
  3. Drove the real API with curl under dev auth (WORKSPACE_DEV_AUTH=1), workspace resolved via Referer: http://127.0.0.1:3065/justin/link-in-bio:
  4. createPage → page id 3, brand gus-the-bass, slug merch-teaser-hero-test.
  5. addBlock kind image (url https://picsum.photos/seed/hero/800/450, alt "Merch teaser hero") → 201, block persisted (this is what proved the CHECK-constraint migration was necessary and correct: it failed with a Postgres constraint violation before the migration was applied, succeeded after).
  6. addBlock kind text and email-capture alongside it → both 201.
  7. Fetched the editor preview HTML (GET /link-in-bio/3): confirmed <img class="lbp-hero" src="https://picsum.photos/seed/hero/800/450" alt="Merch teaser hero"/> present, full-width in the .lbp-blocks flow above the text and email-capture form.
  8. Fetched the published-page dry-run render (GET /api/links/publish?pageId=3, LINKS_PUBLISH_LIVE unset in dev so this is render-only, nothing written to Cloudflare): response ok:true dryRun:true linkCount:0; the returned html contains the identical <img class="lbp-hero" ...> tag plus the .lbp-hero CSS rule inlined in <style>, proving the deployed static page will render the hero the same way as the editor. linkCount:0 is correct — image/text/email-capture are not trackable link kinds, so buildLinkMap needed no image handling, as specified.
  9. npx tsc --noEmit — clean (exit 0) after the worktree's dev server had run once (Next.js generates .next/types on first boot; running it before that produces unrelated pre-existing-asset TS2307 errors that are an environment artifact, not this change).

Raw captures saved during verification (scratch, not committed): editor HTML at /tmp/ct-editor.html, published dry-run HTML at /tmp/ct-published.html, publish JSON at /tmp/ct-publish.json.

Exact steps to go live

  1. git checkout main && git merge links-hero-image (or open a PR) — review the migration file above along with the UI diff.
  2. Deploy prod (normal CreatorTrack deploy pipeline). Prod deploy does not run migrations automatically — the account-deletion precedent in reference_creatortrack_account_deletion applies here too. After deploy, run the prod migration explicitly: PGDATABASE=<prod> WORKSPACE_MIGRATIONS_DIR=db/migrations python3 ~/forge/scripts/forge_workspace_migrate.py against prod, applying 20260711T203954_links_hero_image_links_blocks_kind_check_widen_image.sql.
  3. In the Link In Bio editor, open (or create) the merch-teaser page, click "Image" in the Add block bar, paste the hero image URL (+ optional alt text).
  4. Click Publish. On prod with LINKS_PUBLISH_LIVE set, this pushes the rendered HTML (with the hero) to Cloudflare Pages/KV for real; no further code change needed.

No other migration is needed — blocks.data is jsonb and was never schema-constrained.