/promote — gated investment-opportunity surface, scroll-deck prototype, memo rendering
A private surface for sharing investment opportunities with a small circle of co-investors — passcode-gated in middleware so a locked deck never assembles, with the opportunity content itself gitignored out of this public repo.
New surface: /promote
Added a multi-tenant opportunity-promotion surface for sharing investment opportunities and syndicates with a small circle of co-investors.
Three-surface architecture: public index at
/promote, gated opportunity hub at/promote/[slug], gated materials at/promote/[slug]/{type}/{format?}/{version?}.URL grammar: decks carry both
format(scroll | static | interactive) andversiondimensions; memos carry onlyversion. Computed bylib/promote/urls.tsso YAML never has to declare hrefs.Public index renders OG + Twitter card meta so iMessage / WhatsApp DMs unfurl with a brand image and headline.
Hard gate
New Astro middleware at
src/middleware.tsreads a signedpromote_sessioncookie and gates every path matching^/promote/[^/]+/.+$. Locked requests are rewritten to the hub's locked-state render — deck and memo content never assemble for unauthenticated callers.Server-validated passcode flow at
src/pages/api/promote/unlock.ts. Codes are scrypt-hashed against an environment-configured master hash; an optionalPROMOTE_OVERRIDE_CODES_JSONmap enables per-opportunity codes without code changes.Cookie shape: HMAC-SHA256-signed payload
{ unlocked, exp, scope }, HttpOnly, SameSite=Lax, Secure in production, scoped to/promote. TTL: 7 days, sliding renewal with a polished "signed in for 7 days on this device" toast on first unlock.PROMOTE_DEV_BYPASS=1accepts any code locally for development.Env reads use
process.envfirst withimport.meta.envfallback — Astro 6 doesn't reliably surface non-PUBLIC_*vars viaimport.meta.envat runtime.
Materials chooser
MaterialsChoosercomponent conditionally renders by material count: 0 → 404, 1 → auto-redirect (hub becomes a router), 2-3 → vertical card list, 4+ → 2-column grid.Each
Materialdeclarestype, optionalformat,title,description,primary,status,default_version. Drafts hidden in production.Sibling formats of the same type (e.g.
deck.scroll+deck.static) render as separate chooser cards.
Scroll-deck rendering
Copied
PageAsDeckWrapper.astropattern fromsites/calmstorm-decksand adapted to mpstaton-site's two-tier semantic tokens. Provides scroll-snap container, keyboard nav (↑ ↓ Home End PgUp PgDn), double-click upper/lower half navigation, fixed{n}/{N}counter bottom-right, andIntersectionObserverreveal-on-intersect for.reveal-itemchildren.New
PromotionDeckLayout.astrowraps the deck wrapper with minimal chrome (back-link to hub + session toast).Section components live under
src/layouts/sections/promote/{slug}/deck/{format}/v{N}/so a single opportunity can host multiple formats and multiple versions side-by-side.Loader at
lib/promote/sections.tsusesimport.meta.globto discover sections at build time per(slug, format, version)triple.
Memo route
Memos render through
@lossless-group/lfm(parseMarkdown→ MDAST → existingAstroMarkdown.astro+Sources.astro). Reuses the citations pipeline already wired for context-v document rendering.Versions resolve from
memo/v{N}.md(or legacymemo.mdfor v1) —lib/promote/memos.tshandles fallback.
Content model
Each opportunity is a directory at
src/content/promote/{slug}/:opportunity.yaml— metadata: company name, status (active | closing-soon | closed | paused), logo, accent color, materials list, gate config.variants.yaml— registry of(format, version)deck pairs and per-format default version.data.yaml— shared structured data the deck composes against.narratives/{NN}-{slug}.md— one markdown per slide with frontmatter + prose ("what this slide is", "why it's here", "visual hierarchy").memo/v{N}.md— versioned memo content.
Loader at
lib/promote/opportunities.tsreads viafs, parses YAML with the existingyamldep, caches in-process.
Privacy convention (this is a public repo)
New
.gitignorepatterns atsites/mpstaton-site/.gitignore: opportunity content undersrc/content/promote/,src/layouts/sections/promote/, andpublic/promote/is local-only, except for directories prefixed with_which are committed.Pattern:
_demo,_template,_scaffoldetc. become committable scaffolding seeds that future opportunities copy from. Real opportunity slugs (anything not starting with_) stay on-disk only.Verified with
git check-ignore: real opportunity content is invisible togit add;_demoremains stageable.
Index card system
OpportunityCard.astrorenders a card per active opportunity. Anchored on the symbolic mark (appIcon / wordmark fragment), not the full logo — full logo is reserved for the OG share image.StatusPill.astroshowsActive(default),Closing soon(emphasised),Closed(kept on the index below active opportunities so prospects can see what they missed), orPaused.Index sorts:
Closing soonfirst, thenActive, thenPaused, withClosedrendered separately at the bottom.
Variant iteration loop (the speed motion)
Workflow targeted at 3-5 minute cycles to ship a new deck variant once narratives + data are sharp.
Recipe lifted from calmstorm-decks: pre-sharpened narrative copy → shared YAML data → established theme tokens → single rendering primitive (
PageAsDeckWrapper).Net effect: brand-new opportunities take an hour or two (narrative is the slow step). Additional variants of an already-modelled opportunity approach single-session economics.
Demo opportunity
_demoopportunity scaffold atsrc/content/promote/_demo/with three placeholder section components, a placeholder memo, andlisted_in_index: false. Hidden from the public index but reachable at/promote/_demofor end-to-end smoke testing of the gate, chooser, deck, and memo flow.
Blueprint
New blueprint at
astro-knots/context-v/blueprints/Build-a-Promotion-Surface-for-Investment-Opportunities.md. Frames the architecture, hard-gate posture, materials chooser, variant iteration loop, content model, and decisions audit trail. Sibling toBuild-a-Fundraise-Deck-Workspace.mdandConfidential-Content-Access-Control-Blueprint.md.
Environment additions
.env.exampleextended withPROMOTE_SESSION_SECRET(HMAC signing key, hex 32 bytes),PROMOTE_MASTER_CODE_HASH(scrypt of master code),PROMOTE_OVERRIDE_CODES_JSON(per-slug overrides),PROMOTE_DEV_BYPASS(local development convenience).src/env.d.tsdeclaresApp.Locals.promotetyping for the middleware-set unlock flag.
