2026-05-01 — Copilot rounds on 13, typecheck cleanup, stack ready to merge

Summary

Worked the Copilot review on the two open PRs from yesterday’s stack (sitemap/404 and Payload-driven footer). Fixed everything that was a real bug or sensible hardening, declined-with-reasoning the items that fell under existing project policy (e2e in CI, scale-driven pagination). Took the opportunity to clear the pre-existing typecheck errors that have been sitting at the top of the stack — they weren’t from these PRs but they were noise and they’re gone now.

Stack is healthy and intended for merge today.

What changed

PR #12 — sitemap/404 (feat/sitemap-and-404)

  • Revalidate cadence: fetchJson hard-coded next.revalidate = 60, which silently overrode sitemap.ts’s export const revalidate = 3600. Threaded an optional revalidate arg through fetchJson, exposed it on listPages/listBlogPosts, and the sitemap calls now pass 3600 explicitly. Page renderers still get the 60s default — sitemap is the only caller that needs the slower cadence.
  • Robots disallow: disallow: ['/account/'] doesn’t block /account itself because robots.txt matches by prefix. Added bare /account and /order alongside the trailing-slash variants. Applied Copilot’s suggested patch verbatim.
  • URL sanitization: external link URLs (footer affiliates + social buttons) were rendered straight into href from CMS content. Lifted the existing sanitizeUrl helper out of lexical-render.tsx into lib/util/sanitize-url.ts and reused it in both spots. Entries that fail the scheme whitelist (http/https/mailto/tel + relative //#) get dropped on the storefront. Belt-and-suspenders: also added a Payload-side validateExternalUrl (http(s)-only regex) on social and affiliate url fields, so unsafe URLs get rejected at edit time too.
  • Hours validation + render fallback: openTime/closeTime were untyped text fields with no validation, and the storefront blindly interpolated them into ${open} – ${close}. Now: a Payload validateOptionalTime enforces HH:mm and is required when the day isn’t isClosed; the storefront also renders Closed if either time is missing, so even bad legacy data doesn’t leak ' – '.
  • LinkedIn parity: SocialButtons had a LinkedIn icon mapping but Payload’s SOCIAL_PLATFORMS didn’t list it. Added it so the dropdown matches the icon map.
  • Cleanup: removed unused LocalizedClientLink import, unused social destructure on ShopInfo, and the void countryCode no-op on Footer (dropped the prop entirely from the component signature and both call sites).
  • phoneHref hardening: returned tel: for any non-empty input, even strings with no digits. Now strips digits first and returns "" if nothing’s left. formatPhone docstring corrected to match its actual empty-string return on null/empty input.

Typecheck cleanup (same PR, separate commit)

pnpm exec tsc --noEmit was failing with six errors at the top of the stack — none from today, all inherited from the Solace port. Cleared them in 4217469:

  • Two stale [countryCode] route segments in import paths and prop reads (order-overview/index.tsx, categories/templates/index.tsx) — leftover from the route-segment refactor.
  • react-markdown’s img.src is string | Blob; was passed straight to next/image. Added a typeof === 'string' guard.
  • An @ts-expect-error on the SDK’s array-handle quirk in related-products no longer reported an error (the underlying SDK type loosened). Replaced with a typed cast and a comment.
  • ExploreBlog was importing BlogPost from types/strapi while everything else in the blog module imports from @lib/payload-client. Repointed the import.

Storefront tsc is now clean.

Copilot scoreboard

  • #12: 5 comments. 3 fixed (revalidate × 2 threads, robots disallow). 2 deferred-with-reasoning: pagination beyond the 1000 limit (catalog is well under it; staff-driven growth) and e2e specs (covered by existing project policy — see 2026-04-29 e2e thresholds note).
  • #13: 13 comments. 12 fixed. 1 acknowledged-and-actioned: PR description was stale (“no icons yet”) — updated to reflect that social buttons render icons.

All threads replied to in-line with the resolving SHA. Same pattern as last week.

Decisions worth remembering

  • sanitizeUrl is now a shared util. Any new place that renders an external href from CMS or user input should pull from @lib/util/sanitize-url, not roll its own. It already covers Lexical link nodes, footer affiliates, and social buttons.
  • CMS content gets validated twice. Schema-side validators on Payload (regex/required-when), and a defensive sanitizer on the render side. Either alone is enough for the obvious cases, but the combo means a misconfigured collection or a stale doc can’t bypass either layer.
  • Pre-existing TS errors get fixed when the stack is at the top. Don’t merge a green PR on top of a red baseline if the baseline is small enough to clear in one sitting. Footer PR was the trigger today — six errors, all small, all gone in one commit.

Deferred / not done

Most of these were already on the followup list from 2026-04-30. Re-listed here so today’s deferrals are in one place.

  • Sitemap pagination beyond the per-collection limit: 1000 cap. Add when any of {Payload pages, blog posts, products, categories} crosses ~500.
  • E2e specs for sitemap/robots/404. Covered by the broader policy from 2026-04-29 (no e2e in CI yet; write 5 fresh tests at the threshold). Will land alongside the checkout happy-path spec.
  • Newsletter backend wiring (Beehiiv / Buttondown / etc.) — own PR.
  • Catch-all /[slug]/page.tsx so arbitrary Payload Page slugs render — sitemap currently advertises slugs that 404 on the storefront.
  • NotFoundPage Payload global so 404 copy is staff-editable.
  • Staff Picks page from brief 2.7 — only original ask not yet shipped.
  • Google Maps embed on Footer.
  • Filling real shop data (address, hours, social, affiliate URLs, newsletter blurb, copyright) via Payload admin — content task, ideally Carrie does it herself since that’s the whole point of the Payload-driven footer.

Tomorrow

Stack should be merged by EOD today. Next session: pick up from main with the deferred list above. Staff Picks is the most user-visible remaining item from the original brief.

Also today

  • Research sprint: Donald Rubin / Rubin Museum. Saved donald-rubin — career and wealth summary. Founder-archetype study (union-household kid, infrastructure middleman business, sole ownership 34 years, single 22M) parallels the Noguchi precedent — needs arm’s-length lease + disinterested board approval; (3) plan for founder subsidy, museum ran 50% deficit even with 1B exit first; we don’t. Foundation as wedge earns the right to launch the museum later.
  • Withfriends sunset completed. Decision logged 2026-04-23, executed today. dungeon.club is now the sole membership system. Existing paid terms honored through expiry. See platform-strategy PMF Status section for the activity snapshot at sunset.
  • Strategy session — produced platform-survey-2026-05-01 (state-of-the-world snapshot) and platform-strategy (durable strategic frame: gaming/nerd-culture third-spaces beachhead, platform-vs-applications taxonomy, hybrid tier model, engineering priority reorder, Pync reframed as salary+visa not strategic). Vault backports: Authentik POC live (discord-integration, ecosystem-architecture); tier-naming reconciled hybrid in membership-platform + guild-multitenancy; Saleor → Medusa v2 in projects.md; multi-location non-goal narrowed in membership-platform.

Pitch sharpened + substrate empirical

Refined the partner-shop pitch with human-copy discipline. Final version in platform-strategy:

Guild is built for bookstores, game stores, comic shops, and other geek-adjacent rooms whose regulars already use your Discord. It turns those regulars into a membership: they earn XP for showing up and buying things, work through quests you set, and redeem points for things you actually stock. When a new shipment lands or Free RPG Day is coming up, write a quest for it and your members see it the next time they open the app.

What this collapses: three braided theses (cassette-futurism RPG voice, OutsideRPG metaverse, network coordination) into one shape — shop owner + Discord-native community + own inventory + Quest primitive on top. The earlier pitch required all three theses to land. The new pitch works at one shop with one Discord and one shelf, and the others become optional layers.

Discord-as-substrate validated empirically (sample of 9/12 DB members, 25% not on Discord):

  • 5/9 (~56%) in Victory Point’s server. The cross-shop substrate thesis with Phil holds at pilot resolution.
  • 2/9 in JC Socials. Wide-and-shallow; reads as acquisition channel, not graph.
  • 1/9 in Hudson County Tabletop. HCT is rotting (spam, bots, weak custodian, Panat left). The earlier “I met my wife on HCT” example doesn’t hold today; HCT is a worked example of how Discord substrate decays without good custodianship.
  • 1/9 in TTRPG HQ (~70 members, 18+, JC-local, mission-stated, run by Jordan). Different shape than HCT — small, curated, healthy.

Three substrate layers, not one:

  1. Broadcast servers (JCS): metro-scale, low curation. Acquisition channel.
  2. External curator-run servers (TTRPG HQ): on-thesis, healthy, but not ours. Partnership opportunistic, not near-term lever.
  3. In-shop DM-led groups (DMs at DB, DMs at VP): first-party, smallest unit, highest leverage. Each DM is a curator with a player roster on a recurring schedule, in our space.

DMs are super nodes in network terms — high degree, recurring schedule, taste filter, retention coupling, cross-shop edge embodied (a DM running at DB Tuesdays + VP Thursdays is the cross-merchant graph in one person). Anti-extractive design: recognition over piece-rate, tools over paychecks, comped membership over per-session payment. Connects directly to research-lab coordination-dynamics framing.

Nathaniel Holden flagged as the canonical example: best DM at DB, organizes Daggerheart and Magic nights, data engineer, has contributed to Marty Discord bot. Worth a people note when there’s time.

Build audit and priority reorder

The published priority list in roadmap and platform-strategy needs to flip. Indie ingest tool was #2; pilot-onboarding was #4. After the pitch refinement and DM super-node insight, pilot-onboarding (and what it requires) becomes the load-bearing path.

Promote to top of stack:

  1. Emporium launch close-out (in flight, non-negotiable). Railway staging, prod cutover, real Payload content (Carrie), Staff Picks, catch-all /[slug]/page.tsx. 1–2 weeks. Phil judges the platform partly by what runs at DB.
  2. Member portal redesign close-out (in flight, guild-portal-ui-implementation). Stay the course.
  3. Quest primitive (MVP). The pitch sells “write a quest yourself” and Quest Log is placeholder. Without it, pitch retracts on first Phil demo. MVP scope: Quest model (author, scope, requirements, reward, window), auto-completion via existing webhook events, manual claim flow, Payload admin authoring surface, Quest Log UI. ~2 weeks.
  4. Domain routing for multi-tenant. Designed in guild-multitenancy, not built. Phil cannot pilot meaningfully on dungeon.club/victory-point. Next.js middleware host → shop → org plus per-shop branding pull from Payload. ~3–5 days.

Add (emerged from DM super-node thread):

  1. DM role + DM-authored quests (V2 of Quest primitive). Once MVP Quest exists, layer DM-authored scoping. DM tier mechanics: comped membership, venue priority, badge surface, attendance ledger. ~1 week on top of Quest MVP. Concrete demo-able feature that ports cleanly to Phil’s game-store DM model. Nathaniel as first user.

Drop:

  1. Indie ingest tool Phase 1. Was #2 in the strategy doc. Real and durable wedge against Bookmanager/Edelweiss long-term, but Phil isn’t asking for it. 2–4 weeks of internal-tool build is too expensive against the pilot window. Move to after Phil pilots, unless Carrie says shelf-stocking is the operational bottleneck right now (worth checking).

Stays:

  1. Observability hardening before pilot 3+. Sentry, log aggregation, cross-tenant alerting. Pi-crash precedent.
  2. Check-in V2 (NFC daemon + SSE) partial — at minimum ship the systemd daemon and the lite kiosk (phone-as-reader) so Phil can start without a Pi.
  3. Auto-discount, eval infra, cross-merchant identity all correctly deferred.

Risk if this doesn’t reorder: 4 weeks on ingest tool, no pilot motion, Phil window closes or he commits and we onboard him on a Guild that doesn’t have the verb the pitch sold. Pitch credibility is hard to recover from a retraction.

Decision pending: update platform-strategy priority list and sketch a rough 6-week build calendar, or hold and iterate further.