← All changelog entries
April 29, 2026 · Feature

Web-native slides — Reveal.js + Astro components-as-slides + GSAP, all mode-aware via tier-2 tokens

Stood up a slides surface from zero in one evening — Reveal.js layout that lives inside BaseThemeLayout (header stays, theme/mode toggle stays, fullscreen via a Present button), markdown-deck and Astro-deck routes side by side, auto-discovery of decks via import.meta.glob (no registry file), a tone-setter deck for tomorrow's launch session that composes the existing Section__Webinar* infographic components as slides, two seed decks lifted and adapted from hypernova-site, a markdown primer deck, and a small GSAP timeline registry pattern keyed off data-gsap attributes — nine animated timelines on the launch deck, including a number-counter on the survey-stats tiles. Decks read tier-2 semantic tokens only, so the same deck repaints automatically when the user toggles light / dark / vibrant from the site header.

Authors
Michael Staton
Augmented with
Claude Code on Opus 4.7
Tags
#Slides#Reveal-JS#GSAP#Astro-Components-as-Slides#Markdown-Slides#Theme-Tokens#Modes#Tone-Setting#Pattern-Reuse#Pseudo-Monorepo

Changelog — 2026-04-29 (05)

Slides as a first-class content type

Stood up a complete slides surface for fullstack-vc in one evening — primarily so tomorrow’s launch session can open with a tone-setter deck composed from existing site components, secondarily because Reveal-based decks have shipped on three sister sites (lossless.group, hypernova-site, twf_site) and were overdue here. The blueprint at context-v/blueprints/Maintain-Embeddable-Slides.md framed the work; this entry is the receipts.

Why Care?

The launch session opens at 11:00 EST with a 5-minute framing block. The framing wants to show the room rather than describe it: who’s on the registrant list, what they say they’re working on, what the survey signaled about tool-stack maturity. All four of those infographics already exist as Section__Webinar* components on /sessions/2026-04-29_agentic-vc-dojo-launch. The goal: render them as slides without rebuilding them.

That goal turned out to land cleanly: an Astro page can import a section component, drop it inside a <section> element in a Reveal deck, and Reveal sees it as static DOM at build time. No special slide-version of the component, no port. The webinar components don’t know they’re slides. The deck doesn’t know they’re page sections. Astro’s component model just collapses the distinction.

Beyond tomorrow: this gives the dojo a place to drop slides going forward. Workshop materials, retro decks, presentations, primers — all live next to the rest of the site, all repaint with theme/mode toggles, all noindex by default.

What Got Built

OneSlideDeck — the layout

src/layouts/OneSlideDeck.astro wraps a deck inside BaseThemeLayout, so the site header (with theme + mode switchers) stays visible while the deck renders inside the main content area. A small control bar above the deck has ← All slides · deck title · Restart · Present. Present triggers requestFullscreen() on the stage element only — the user breaks out of the page chrome into a true presentation surface, and breaks back in with Esc. Reveal.layout() re-fits on enter/exit so the canvas resizes cleanly.

The deck reads only tier-2 semantic tokens (--color-text, --color-primary, --fx-headline-gradient, --fx-card-bg, --font-display, etc.), so the same deck repaints when the user toggles light / dark / vibrant from the header. No mode is forced. A vibrant-loving colleague hits the toggle, the deck flips to neon. Old-school folks who think dark mode is “trivial hacker BS” stay on light.

Reveal config: embedded: true, center: true, width: 1600, height: 900, margin: 0.04, transition: 'slide', plus the markdown / highlight / notes plugins. :first-child { margin-top: 0 !important } inside slide sections neutralizes the page-flow margin-top: 3rem that section components carry, so V+H centering reads true. Reveal’s controls are colored with var(--color-primary) (which resolves to --color__violet-electric) at full opacity, with a violet drop-shadow + scale-up on hover — visible across all three modes.

MarkdownSlideDeck — Reveal’s markdown plugin, frontmatter stripped

src/layouts/MarkdownSlideDeck.astro wraps OneSlideDeck and feeds raw markdown through Reveal’s official markdown plugin via <textarea data-template>. Slides separate on --- (h-rule), nested on --, speaker notes on Note:. YAML frontmatter is stripped before handoff so it doesn’t render as a literal ”---” slide. ~25 lines.

Slides index + dynamic markdown route

src/pages/slides/index.astro auto-discovers two kinds of decks at build time:

  1. Astro deckssrc/pages/slides/<slug>.astro files that export const presentation = { title, description, date, ... }. Discovered via import.meta.glob('./*.astro', { eager: false }) so their CSS doesn’t bleed into the index page.
  2. Markdown deckssrc/content/slides/*.md files with a frontmatter title/description. Discovered via import.meta.glob('../../content/slides/*.md', { query: '?raw', import: 'default' }) so we can read frontmatter without a content-collection schema.

No componentDecks.ts registry file. Adding a new Astro deck is just dropping a file with export const presentation = { ... }. Adding a markdown deck is just dropping <slug>.md in src/content/slides/. The index picks them up on next build.

Cards on the index render with a type pill (Astro / Markdown), title, description, author, date, and a hover state that uses --fx-card-shadow-hover from the effect tokens.

src/pages/slides/[...slug].astro is the dynamic route for markdown decks only. Astro’s file-based router gives concrete *.astro files in src/pages/slides/ priority over the catch-all, so the catch-all only fires when there’s no matching .astro file. getStaticPaths reads frontmatter from each markdown file’s raw glob result and feeds title/description/content to MarkdownSlideDeck.

Tone-setter deck for tomorrow’s launch session

src/pages/slides/agentic-vc-dojo-launch.astro. Nine slides:

  1. Title — “Agentic VC Dojo · Launch Session” with the violet→cyan gradient headline and the byline.
  2. Why we’re here — three big-list bullets framing the dojo’s premise.
  3. Who’s in the room<Section__WebinarSurveyStats totals={survey.totals} />. Same component used at the bottom of the live session page; here it’s the third slide.
  4. What this room is working on<Section__WebinarSurveyPolls experience toolStack surveyed />. Same component, slide context.
  5. Firms represented<Section__WebinarFirmsRepresented firms={survey.firms} />. 37 firm pills in a flexbox grid.
  6. Voices<Section__WebinarSurveyVoices quotes={survey.quotes} ... /> with the “what this room is actually working on” framing. 6b. Asks — same component reused for the quotesWanted slice (“what this room is hoping to take home”), conditional on data presence.
  7. Format — five-row agenda, one row per agenda block.
  8. Speed-demo presenters<Section__WebinarPresenters presenters hosts />. Pulled from the session frontmatter via getEntry('sessions', '2026-04-29_agentic-vc-dojo-launch').
  9. Closer — “A few new superpowers, before we log off.”

Each slide carries a data-gsap="<key>" attribute. Six of the nine keys have GSAP timelines registered (see next section).

GSAP timeline registry

The deck registers timelines via a global registry the layout looks up on slide change:

// In OneSlideDeck.astro:
window.__slideAnimations = window.__slideAnimations || {};
Reveal.on('ready',        e => fireSlideAnim(e.currentSlide));
Reveal.on('slidechanged', e => fireSlideAnim(e.currentSlide));
function fireSlideAnim(slide) {
  const key = slide?.dataset?.gsap;
  if (key && window.__slideAnimations[key]) {
    window.__slideAnimations[key](slide, Reveal);
  }
}

Decks register from their own <script is:inline>. The launch deck has nine timelines:

window.__slideAnimations.title = (slide) => {
  const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
  tl.from(slide.querySelector('.eyebrow'),          { opacity: 0, y: 12, duration: 0.45 })
    .from(slide.querySelector('.gradient-headline'), { opacity: 0, y: 28, duration: 0.7 }, '-=0.15')
    .from(slide.querySelector('.subtitle'),          { opacity: 0, y: 18, duration: 0.55 }, '-=0.35')
    .from(slide.querySelector('.byline'),            { opacity: 0, duration: 0.6 }, '-=0.2');
};

The most fun timeline is on the stats slide — a number-counter that animates each .wsstats__num from 0 → its survey value over 1.1s with power2.out easing, suffixes preserved. The firms slide bursts the 37 pills in with a 15ms stagger and back.out(1.4) so they pop on entry. Voices and agenda use stagger-from-left. Title and closer fade-in with the gradient headline scaled up subtly.

Adding flare to a new slide is: drop data-gsap="my-key" on the section, register window.__slideAnimations["my-key"] in a script block in the deck. No layout changes, no GSAP-aware components.

Seed decks lifted from hypernova

src/pages/slides/global-cvc-for-multinationals.astro and the-future-of-cpg.astro ported from astro-knots/sites/hypernova-site/src/pages/slides/. Adapted to use fullstack-vc’s OneSlideDeck and the site’s tier-2 tokens. Retained the structure (title slide, executive summary, vertical-nested slides for deep-dives, two-col / three-col grid layouts), dropped the inline hardcoded color overrides that referenced hypernova’s water-theme tokens.

Markdown primer

src/content/slides/markdown-slides-primer.md doubles as a sample deck and as authoring documentation: separators, nested slides, fragments, code blocks, blockquotes, speaker notes. Drop a markdown file in src/content/slides/<slug>.md, hit /slides/<slug>, done.

Architectural Decisions

  • Embed in BaseThemeLayout, don’t take over the document. Earlier draft made the slide its own <html> root with no header/footer. Two problems: lost the theme/mode toggle (so the user couldn’t pick their preferred mode mid-deck), and broke the convention used on every other site where the slide sits inside the chrome and a Present button breaks it out. Restored to BaseThemeLayout flow with the footer hidden via body:has(.slide-deck-page) and the stage owning fullscreen via requestFullscreen() on the stage element.
  • Don’t force a mode. First draft forced mode="dark" and the launch deck overrode to mode="vibrant". That stripped user agency over the visual mode in slides and ignored the Tier-1/Tier-2 architecture’s whole point — clients (and audiences) pick. Removed forcing entirely; decks now inherit mode from the user’s stored preference (default dark per BoilerPlateHTML’s pre-paint script), and the toggle in the header repaints the deck in real time.
  • Tier-2 semantic tokens only inside slide CSS. No hardcoded hex. Every deck and every layout reads --color-*, --fx-*, --font-*. The same architecture that lets us swap a brand color in one line at the named-token tier means we can swap the deck’s primary color in one line — no slide is exempt.
  • Auto-discovery, no registry file. Hypernova has a src/data/componentDecks.ts registry mapping slugs to component class names. We didn’t replicate that. import.meta.glob does the same job at build time without the indirection. Adding a deck is dropping a file; deleting one is deleting the file. The registry-file pattern made sense when each deck exported a class component; here every deck is just an Astro page, so the file is the registration.
  • Reveal’s markdown plugin, not a hand-rolled converter. Hypernova’s MarkdownSlideDeck does a regex-based markdown→HTML pass that has known edge-case bugs (doesn’t open the first section cleanly, splits incorrectly on h2). Reveal’s own markdown plugin is mature, handles separators / nesting / notes correctly, and ships in the same CDN bundle. We just needed to strip the YAML frontmatter so it doesn’t render as a literal ”---” slide.
  • GSAP via a registry, not via per-component imports. The registry pattern (window.__slideAnimations[key] = fn) keeps GSAP optional (one prop on OneSlideDeck toggles whether the CDN is loaded), keeps the layout pure, and lets each deck author its own animations in a single <script is:inline> block. No tree of GSAP-aware Slide components, no registration hooks. Simpler than the alternatives, and animations are local to the file that uses them — easy to read, easy to delete.
  • Shelved the :::slides directive. The blueprint also defines a markdown directive for embedding decks inside content pages (:::slides\n- [[essays/intro.md|Intro]]\n:::). Not on tomorrow’s critical path. Deferred.
  • Footer hidden on slide pages, header retained. body:has(.slide-deck-page) .site-footer { display: none } — the deck wants vertical room, but the header is the only place to flip mode mid-presentation, so it stays. Trade keeps the page-level chrome consistent with non-slide pages while giving the stage room to render at 16:9 without pushing the controls below the fold.

Behavior

  • Visit /slides → cards for the launch deck, the two seed decks, and the markdown primer. Each card has a type pill that tells you whether it’s an Astro deck or a markdown deck.
  • Visit /slides/agentic-vc-dojo-launch → header on top, deck control bar (← All slides · “Agentic VC Dojo — Launch Session” · Restart · Present), 16:9 stage with the title slide V+H centered. Right arrow advances. The next slide brings up <Section__WebinarSurveyStats /> with a number-counter animation on the four tiles. Subsequent slides cycle through the survey infographics, presenters, agenda, and closer — each with its registered GSAP timeline firing on entry.
  • Click Present (or hit F) → the stage element goes fullscreen, header + control bar hide, Reveal recomputes layout. Esc returns. The mode you picked persists — vibrant deck stays vibrant, dark stays dark.
  • Open the header’s mode switcher mid-deck → the deck repaints instantly because every visual is reading --color-* and --fx-* semantic tokens. The gradient headline switches between violet→cyan (dark/vibrant) and ink→violet-deep (light) without a re-render.
  • Visit /slides/markdown-slides-primer → Reveal’s markdown plugin parses the file, slides render with proper headings / lists / code blocks. Press S for speaker notes; the Note: block in the source shows in the speaker view only.

What’s Deferred

  • :::slides directive — markdown-page-to-embedded-deck pipeline per the blueprint §Embedding Component. The infrastructure on the deck side is ready; the directive parsing and SlidesEmbed.astro iframe wrapper aren’t.
  • Per-deck noindex / share-image overrides — currently every deck inherits BaseThemeLayout’s defaults. A presentation.shareImage field could feed the OG meta on the deck route specifically.
  • /design-system catalog entry — per the blueprint convention every component lands in the design-system catalog when introduced. The slide layouts and the GSAP registry pattern qualify; haven’t added them yet.
  • GSAP timeline scrubbing on Reveal fragments — right now timelines fire on slidechanged. Reveal also emits fragmentshown / fragmenthidden for <li class="fragment"> reveals; could hang scoped timelines off those events. Not needed for tomorrow.
  • Speaker-notes pre-fill on the launch deck — useful for me at 11:00am EST given the 4 hours of sleep ahead. May add inline.

Pattern Reuse Note

The deck infrastructure here doesn’t depend on anything fullstack-vc-specific beyond the BaseThemeLayout import and the tier-2 tokens. Sister sites in the pseudo-monorepo (twf_site, hypernova-site, future client sites) can copy OneSlideDeck.astro + MarkdownSlideDeck.astro + the /slides/ route pair as a unit. The tier-2 token references mean each site gets its own brand colors automatically without any code change in the deck files. This is the copy-pattern motion the project’s CLAUDE.md describes — and a candidate for eventual extraction into a @knots/slides reference once a third site adopts it.

Verification

  • pnpm devhttp://localhost:4324/slides lists the four decks with correct type pills.
  • /slides/agentic-vc-dojo-launch → SSR renders the Reveal canvas with all nine data-gsap sections present in DOM, all five Section__Webinar* components hydrate correctly with the survey JSON data, GSAP CDN loads, the registry has nine timelines registered.
  • Toggle theme/mode in the header during a deck → semantic tokens update, deck repaints, gradient headline switches without re-render.
  • Click Present → stage fullscreens, header+bar hide, Reveal re-fits. Hit Esc → returns to embedded view, header reappears.
  • Hard-refresh after CSS-only edits → Vite HMR hot-swaps; only Reveal config changes (e.g., center: true) require a full reload because Reveal initializes once at DOM ready.
  • /slides/markdown-slides-primer → Reveal markdown plugin parses the file; nested slides accessible via down-arrow; speaker notes (Note:) visible in S window only.
  • Reveal navigation arrows visible in dark and vibrant mode at full primary-violet opacity, hover adds a violet glow + scale.

Time-to-Ship

Empty src/pages/slides/ directory at start of session. Eight files later (two layouts, two routes, three Astro decks, one markdown deck), the full surface is live with GSAP flare, mode-aware theming, header-retained chrome, and a Present button. ~1,400 lines of code. Started Sunday morning on the broader fullstack-vc site; this is Tuesday evening.

Whole teams used to spend months on a presentation system this complete. The tone-setter deck for tomorrow’s room is on rails. Going to bed.

Files modified (8)
  • sites/fullstack-vc/src/layouts/OneSlideDeck.astro
  • sites/fullstack-vc/src/layouts/MarkdownSlideDeck.astro
  • sites/fullstack-vc/src/pages/slides/index.astro
  • sites/fullstack-vc/src/pages/slides/[...slug].astro
  • sites/fullstack-vc/src/pages/slides/agentic-vc-dojo-launch.astro
  • sites/fullstack-vc/src/pages/slides/global-cvc-for-multinationals.astro
  • sites/fullstack-vc/src/pages/slides/the-future-of-cpg.astro
  • sites/fullstack-vc/src/content/slides/markdown-slides-primer.md