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 inBlockEditor.tsxheader and theLinkInBioClient.tsxcreate-modal, writinglinks.pages.brand. - Publish is host-aware:
app/api/links/publish/route.tscomputeshost = brandHost(brand), writes KVpage:<host>:<slug>(lib/links/cf.ts upsertPageHtml), callsensureRoute(cfg, host, slug)againstHOST_ZONE(already has bothgusthebass.comandgusoutdoor.co). - Edge Worker is host-keyed:
infra/links-edge/src/page.tsreadspage:<host>:<slug>keyed byurl.hostname;wrangler.tomlalready hasgusoutdoor.co/{links,c,r/*}routes. - "Send as either brand" for captures:
email-captureblock has a per-blockbrandoverride - freeform
tagsfield (BlockEditor.tsx);LinkBioPreview.tsxstampsdata-brand/data-tagson the form;render.ts'scaptureScriptreads 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 atlinks-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 thelink-in-bioapp 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)¶
- Edge CORS gap (real bug) —
infra/links-edge/src/cors.tsALLOWED_ORIGINSonly hadgusthebass.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. Addedhttps://gusoutdoor.co+https://www.gusoutdoor.co. forge repo, commit53ab95b, pushed to main. - Mautic segment misrouting (real bug) —
scripts/forge_links_sync.pyhardcodedMAUTIC_SEGMENT_ID = 4("Gus The Bass") for every brand's captures, so a gusoutdoor signup would have landed in the Bass segment. ReplacedBRAND_WORKSPACE+ the two hardcoded constants with oneBRAND_MAUTICdict (workspace_id,segment_id,welcome_email_id) per brand, so workspace and Mautic routing can't drift apart.gusoutdoor/gusoutdoorconow fan to segment 2 ("Gus Outdoor Co Members", confirmed to exist viaGET /api/segments) instead of 4.fan_out_mauticre-checkssegment_for(brand)and skips loudly (row stayssynced_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), sowelcome_email_idisNonefor gusoutdoor: the welcome step is skipped cleanly rather than emailing mis-branded copy. forge repo, commit53ab95b. - "Make it obvious which brand/host a page publishes as" (UI polish) —
components/apps/link-in-bio/LinkInBioClient.tsxpages-list card now showsbrandHost(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, branchbrand-picker, commit96ec705, 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=developmentin the worktree's.env.local; every publish call below returned"live": false, "dryRun": trueand 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 overridegusoutdoor, tagsTrail Guide 2026) blocks via/api/links(dev auth,WORKSPACE_DEV_AUTH=1, Referer-based workspace resolution). GET /api/links/publishdry-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 keypage:gusoutdoor.co:outdoor-verify, exactly whatinfra/links-edge/src/page.tsreads forGET 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 (
deletePagevia API). npx tsc --noEmiton creatortrack: 6 pre-existing errors (missingpublic/brand/*.pngtype declarations,LayoutProps— confirmed viagit stash/re-run to exist on the unmodifiedbrand-pickerbranch 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_forunit-verified in isolation (module loaded withforge_workspace_db.admin_connectstubbed so no live DB dependency):gusoutdoor-> workspace 85 / segment 2 / welcome None;gusthebass-> 85 / 4 / 14; unknown brand -> allNone(skip).BRAND_WORKSPACE.keys() == BRAND_MAUTIC.keys()asserted. Did NOT run--dry-runagainst the live sync DB path end-to-end:admin_connect()hit a pre-existing, unrelated schema error (column "tags" does not existon whatever DB it defaults to) that exists independent of this change — flagged below, not fixed (out of scope / needs Justin's call on which DBforge_links_sync.pyshould 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)¶
- Merge
brand-pickerbranch tocreatortrackmain (1-file UI polish diff, low risk) — PR: https://github.com/JustinWieb/creatortrack/pull/new/brand-picker - 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.
- Redeploy
links-edgeto push the CORS fix live (this is the one functional blocker for gusoutdoor.co captures actually working): - 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).
- Restart/redeploy
forge-links-synctimer is not required (script auto-reloads on next tick; systemd timer just re-runs the.pyfile) — but do investigate the pre-existingadmin_connect()/links.captures.tagscolumn 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). - Optional, not built: a dedicated "Welcome - Gus Outdoor" Mautic email + wiring its id into
BRAND_MAUTIC["gusoutdoor"]["welcome_email_id"]inforge_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]