Skip to content

URL: https://mkdocs.justinsforge.com/memory/handoffs/links-brand-picker-status-2026-07-11/

Links: brand picker + gusoutdoor.co second domain, status 2026-07-11

Follow-on task (worker session session_01TmRrT6BQSfAAYfDKY9aJjz) on top of the merged Links core (reference_links_module). Key finding: most of this scope was already built and merged to creatortrack main on 2026-07-08 (commits 10fc865, cf08e25, 5da670e) but not reflected in the reference doc. This session closed the two real remaining gaps and added one UI polish item.

What was ALREADY on main (verified, not re-done)

  • Brand picker in the editor: lib/links/brands.ts (brandHost, BRAND_OPTIONS, normalizeBrand), a dropdown in BlockEditor.tsx header and the LinkInBioClient.tsx create-modal, writing links.pages.brand.
  • Publish is host-aware: app/api/links/publish/route.ts computes host = brandHost(brand), writes KV page:<host>:<slug> (lib/links/cf.ts upsertPageHtml), calls ensureRoute(cfg, host, slug) against HOST_ZONE (already has both gusthebass.com and gusoutdoor.co).
  • Edge Worker is host-keyed: infra/links-edge/src/page.ts reads page:<host>:<slug> keyed by url.hostname; wrangler.toml already has gusoutdoor.co/{links,c,r/*} routes.
  • "Send as either brand" for captures: email-capture block has a per-block brand override
  • freeform tags field (BlockEditor.tsx); LinkBioPreview.tsx stamps data-brand/data-tags on the form; render.ts's captureScript reads them per-form, falling back to the page brand.
  • Live infra check (read-only, via CF API, 2026-07-11): gusoutdoor.co already has its A record proxied (orange-cloud) AND all 3 Worker routes (/links, /c, /r/*) already deployed pointing at links-edge. No DNS or route work is needed — this was done in a prior session.
  • Michael's per-app Gus scope: unaffected, both brands live in one workspace (gus-the-bass) that the link-in-bio app is already allowlisted for (app/api/links/route.ts); nothing brand-specific to gate further.

What this session actually fixed (2 real gaps + 1 polish)

  1. Edge CORS gap (real bug)infra/links-edge/src/cors.ts ALLOWED_ORIGINS only had gusthebass.com. A gusoutdoor.co-hosted page's capture form would get blocked by the browser's CORS preflight before the POST ever reached /c, even though the route/DNS were live. Added https://gusoutdoor.co + https://www.gusoutdoor.co. forge repo, commit 53ab95b, pushed to main.
  2. Mautic segment misrouting (real bug)scripts/forge_links_sync.py hardcoded MAUTIC_SEGMENT_ID = 4 ("Gus The Bass") for every brand's captures, so a gusoutdoor signup would have landed in the Bass segment. Replaced BRAND_WORKSPACE + the two hardcoded constants with one BRAND_MAUTIC dict (workspace_id, segment_id, welcome_email_id) per brand, so workspace and Mautic routing can't drift apart. gusoutdoor/gusoutdoorco now fan to segment 2 ("Gus Outdoor Co Members", confirmed to exist via GET /api/segments) instead of 4. fan_out_mautic re-checks segment_for(brand) and skips loudly (row stays synced_to_mautic = false, retried next tick) instead of silently misrouting if a brand is ever added to one place and not the other. No dedicated "Welcome - Gus Outdoor" email exists (only a starter template, id 8, not wired for auto-send), so welcome_email_id is None for gusoutdoor: the welcome step is skipped cleanly rather than emailing mis-branded copy. forge repo, commit 53ab95b.
  3. "Make it obvious which brand/host a page publishes as" (UI polish)components/apps/link-in-bio/LinkInBioClient.tsx pages-list card now shows brandHost(brand)/slug (e.g. gusoutdoor.co/links) plus a friendly brand label ("Gus Outdoor Co.") instead of the bare slug + raw brand tag. creatortrack repo, branch brand-picker, commit 96ec705, pushed.

Files changed

creatortrack (branch brand-picker, pushed to origin/brand-picker): - components/apps/link-in-bio/LinkInBioClient.tsx

forge (main, pushed): - infra/links-edge/src/cors.ts - scripts/forge_links_sync.py

Verification (dev only, dry-run, guard enforced)

  • Confirmed NODE_ENV=development in the worktree's .env.local; every publish call below returned "live": false, "dryRun": true and the "LINKS_PUBLISH_LIVE not set" note — nothing was written to Cloudflare KV/routes.
  • Applied all pending CT migrations to the dev clone ct_brand_picker (last: 20260711T214757_links_core_links_captures_tags_column_...), clean.
  • Created a gusoutdoor-brand page (outdoor-verify) with header + link + email-capture (brand override gusoutdoor, tags Trail Guide 2026) blocks via /api/links (dev auth, WORKSPACE_DEV_AUTH=1, Referer-based workspace resolution).
  • GET /api/links/publish dry-render: linkMap = {"gusoutdoor-12": {destUrl, brand: "gusoutdoor", label}} — linkId correctly brand-derived.
  • Rendered HTML: data-brand="gusoutdoor", data-tags="Trail Guide 2026" on the capture <form> — confirms the per-brand capture attribution reaches the deployed page.
  • POST /api/links/publish: {"live": false, "dryRun": true, "linkCount": 1} — safe.
  • By code inspection (not exercised live): brandHost("gusoutdoor") = "gusoutdoor.co", so a live publish would write KV key page:gusoutdoor.co:outdoor-verify, exactly what infra/links-edge/src/page.ts reads for GET gusoutdoor.co/outdoor-verify.
  • Created a gusthebass-brand page (bass-verify) and repeated the same dry-run checks: linkMap = {"gusthebass-14": {...brand: "gusthebass"...}}, dry-run publish safe. No regression on the primary brand.
  • Deleted both test pages after verification (deletePage via API).
  • npx tsc --noEmit on creatortrack: 6 pre-existing errors (missing public/brand/*.png type declarations, LayoutProps — confirmed via git stash/re-run to exist on the unmodified brand-picker branch baseline too, unrelated to Links). No new errors from my changes.
  • npx eslint components/apps/link-in-bio/LinkInBioClient.tsx: clean.
  • forge_links_sync.py: python3 -c "import ast; ast.parse(...)" syntax-clean; workspace_for/segment_for/welcome_email_for unit-verified in isolation (module loaded with forge_workspace_db.admin_connect stubbed so no live DB dependency): gusoutdoor -> workspace 85 / segment 2 / welcome None; gusthebass -> 85 / 4 / 14; unknown brand -> all None (skip). BRAND_WORKSPACE.keys() == BRAND_MAUTIC.keys() asserted. Did NOT run --dry-run against the live sync DB path end-to-end: admin_connect() hit a pre-existing, unrelated schema error (column "tags" does not exist on whatever DB it defaults to) that exists independent of this change — flagged below, not fixed (out of scope / needs Justin's call on which DB forge_links_sync.py should target and whether prod migrations are current).
  • Did not run wrangler deploy (or --dry-run) at all, per the hard limit; confirmed via read-only Cloudflare API calls that gusoutdoor.co's routes/DNS are already live from a prior session, so the CORS fix is the only thing pending a Worker redeploy.

Go-live steps (Justin-only)

  1. Merge brand-picker branch to creatortrack main (1-file UI polish diff, low risk) — PR: https://github.com/JustinWieb/creatortrack/pull/new/brand-picker
  2. Deploy prod (creatortrack) as usual after merge — the brand picker + host-aware publish itself has been live since 2026-07-08; this only ships the "brand+host on card" polish.
  3. Redeploy links-edge to push the CORS fix live (this is the one functional blocker for gusoutdoor.co captures actually working):
    cd forge/infra/links-edge && set -a; source ~/.forge-secrets/cloudflare.env; set +a && npx wrangler deploy
    
  4. No DNS or CF route changes needed — gusoutdoor.co's A record (proxied) and all 3 Worker routes are already live (verified read-only via CF API this session).
  5. Restart/redeploy forge-links-sync timer is not required (script auto-reloads on next tick; systemd timer just re-runs the .py file) — but do investigate the pre-existing admin_connect() / links.captures.tags column error noted above before the next real gusoutdoor capture needs to sync (unrelated to this task, flagged as a blocker for a clean next sync run).
  6. Optional, not built: a dedicated "Welcome - Gus Outdoor" Mautic email + wiring its id into BRAND_MAUTIC["gusoutdoor"]["welcome_email_id"] in forge_links_sync.py, if Justin wants new Outdoor signups to get an automated welcome (currently: segmented + tagged, no welcome email).

Blocker / flag for Justin

forge_links_sync.py --once --dry-run run from a plain shell (no worktree DB env sourced) hit psycopg.errors.UndefinedColumn: column "tags" does not exist on links.captures via admin_connect()'s default target. This is unrelated to this task's code changes (confirmed by tracing the failure point: it happens on the DB query itself, after all of BRAND_MAUTIC resolution already ran correctly) but suggests whichever DB admin_connect() defaults to may be missing the 20260711T214757_links_core_links_captures_tags_column_... migration. Worth a quick check before the next live sync cycle.

[Claude Opus 4.8, brand-picker worker]