# ADR 058: Project Pitch Decks

- HTML version: https://robbiepalmer.me/projects/personal-site/adrs/058-project-pitch-decks
- Project: Personal Site (https://robbiepalmer.me/projects/personal-site.md)
- Status: Proposed
- Date: 2026-08-30

## Summary

Add an optional, project-local pitch deck to project pages. Authors will write each deck in MDX and render it with
[reveal.js 6](https://revealjs.com/). The spike will start with the official
[`@revealjs/react` package](https://revealjs.com/react/) and connect it to the site's components and Tailwind tokens.
If the published package gets in the way, vendor and cut down the relevant source.

The project page will embed the deck as a first-class tab. A separate deck route will support focused viewing,
deep links, speaker notes, fullscreen presentation, scroll view, and browser PDF export. The same source will
also produce a plain-Markdown transcript.

Take [Slidev](https://github.com/slidevjs/slidev) as the product reference for Markdown-first authoring, layouts,
framework components, presenter tools, and static hosting. Implement that experience inside the existing React and
Next.js application instead of adding Vue, UnoCSS, and a second Vite application.

Accept this ADR only after a one-deck spike proves reveal.js and its React integration against the site's Next.js
static export, React 19 lifecycle, MDX renderer, responsive layout, and Markdown-twin generation.

## Context

Project pages currently combine different kinds of document:

* product requirements and user stories;
* market and competitor analysis;
* implementation status;
* interactive design prototypes; and
* linked architecture decisions.

That material works as a reference, but it makes a poor opening explanation. A project pitch deck can tell a
shorter story: the problem, the audience, the insight, the proposed product, evidence, design, technical choices,
and current result. Keep the detailed project overview and ADRs available instead of compressing them into slides.

The recipe-site project already proves that project MDX can embed an interactive React component. Its
`DesignEmbed` uses an iframe because the design is a self-contained prototype. A deck is different. Its text,
links, diagrams, and images belong to the project's content and should remain readable, indexable, themeable, and
available in the site's `.md` twin.

### Existing constraints

The chosen design must fit these existing decisions and implementation details:

* The site uses Next.js 15, React 19, TypeScript, and static export to Cloudflare Pages.
* Project content is MDX loaded and validated at build time.
* The MDX renderer already supports React components, Mermaid, Shiki-backed code blocks, responsive images, and
  GitHub Flavored Markdown.
* Major pages have agent-readable Markdown twins and appear in `/llms.txt`.
* The visual system uses Tailwind CSS and local components. A deck should look like part of the site.
* Embla Carousel is already installed, but the site does not yet have presentation state, speaker notes, overview
  mode, deep linking, or print-to-PDF behavior.
* A project can have no deck. Existing project URLs and content files must remain valid.

### What Slidev gets right

Slidev is the clearest reference implementation for the desired authoring experience. Its documented feature set
includes Markdown, themes, embedded framework components, presenter mode, drawing, Mermaid, code highlighting,
recording, and PDF, PNG, and PPTX export. It can also build one or more decks as static SPAs under a configured
base path. See the [Slidev repository](https://github.com/slidevjs/slidev),
[hosting guide](https://sli.dev/guide/hosting.html), and [export guide](https://sli.dev/guide/exporting.html).

Slidev's documented runtime uses Vue 3, Vite, UnoCSS, VueUse, and Vue components. Running that runtime here would
create a second client framework, build pipeline, theme system, component model, and development server. Embedding
the resulting SPA in an iframe would also separate the deck from the page DOM and the existing Markdown-twin
pipeline. Its source remains useful, especially the parser and presentation behavior that do not depend on Vue.

## Decision

### Use reveal.js core and try its official React package first

Use `reveal.js` 6 as the browser presentation engine. Start the spike with `@revealjs/react` as a normal dependency.
Keep the site's deck components thin so they can wrap the package without exposing its API throughout the content.
Vendor and prune the wrapper only if the published package cannot meet the requirements cleanly.

reveal.js already implements the version-one browser behavior:

* embedded decks with keyboard handling limited to a focused deck;
* touch and keyboard navigation;
* URL hashes and direct slide links;
* fragments and code line stepping;
* overview, fullscreen, and speaker views;
* a mobile-oriented scroll view;
* browser PDF export; and
* a plugin API if a later deck needs math or another specialist feature.

The [official React integration](https://revealjs.com/react/) provides tested lifecycle handling and typed `Deck`,
`Slide`, `Stack`, `Fragment`, and `Code` components. Its production source is about 1,400 lines, with another 1,300
lines of tests. Try that implementation unchanged before taking ownership of it. If its generic Markdown, code,
stack, configuration, or styling paths cause integration problems, the source is small enough to copy, prune, and
adapt to the project's component API.

The embedded deck will use at least this configuration:

```tsx
<ProjectPitchDeck
  config={{
    embedded: true,
    keyboardCondition: "focused",
    hash: false,
    history: false,
    autoSlide: false,
    transition: prefersReducedMotion ? "none" : "slide",
  }}
>
  {slides}
</ProjectPitchDeck>
```

The [reveal.js configuration reference](https://revealjs.com/config/) explicitly supports embedded mode,
focus-scoped keyboard shortcuts, touch navigation, URL state, media autoplay control, and PDF settings.

Do not use the reveal.js Markdown plugin. The site will continue to compile MDX with its existing remark and
rehype pipeline, then map the generated slide groups to the React integration. The existing pipeline will continue
to handle links, image processing, Mermaid, Shiki themes, and agent-readable conversion.

### Keep one optional deck beside each project

A project with a pitch deck will add this file:

```text
ui/content/projects/<project-slug>/pitch.mdx
```

The file will have deck metadata in frontmatter and use root-level thematic breaks as slide boundaries:

```mdx
---
title: "Recipe Site pitch"
description: "A cooking product built around trusted use, not star ratings"
---

# Recipe Site

Recipes improve when cooking behavior can travel with them.

---

## The problem

Collections, planning, shopping, and cooking history live in separate tools.

---

## The product

<RecipeProductFlow />
```

A pitch-only remark plugin will group root nodes between thematic breaks into standard `mdxJsxFlowElement` nodes
named `PitchSlide`. It will run after MDX parsing and only in the pitch-deck compilation pipeline. Ordinary project
MDX will not register it. Using standard MDX nodes avoids adding a custom mdast node type that another compiler or
serializer would need to understand.

The plugin will split only on root-level `thematicBreak` nodes. A `---` inside a fenced code block or JSX child
stays inside its existing node and cannot become a slide boundary. Inside `pitch.mdx`, a root-level thematic break
always separates slides. Use another visual separator inside a slide.

Transcript conversion takes a separate branch before slide grouping. It removes each complete `PitchNotes` node,
then passes the remaining, ungrouped MDX through `mdxToAgentMarkdown`. Neither `replaceMdxNodes` nor the Markdown
serializer will receive deck-only slide nodes.

MDX components may opt into layout or presentation behavior where plain Markdown is insufficient:

```mdx
<PitchColumns>
  <PitchColumn>
    ## Evidence

    The detailed analysis remains linked from the slide.
  </PitchColumn>
  <PitchColumn>
    <MarketChart />
  </PitchColumn>
</PitchColumns>

<PitchNotes>
  Explain why repeated cooking is stronger evidence than a saved recipe.
</PitchNotes>
```

`PitchNotes` will render an `<aside className="notes">` inside its containing `PitchSlide`. The reveal.js notes
plugin reads that element for speaker view. The pitch transcript processor will remove the entire `PitchNotes` node
before calling the public Markdown converter, so it cannot unwrap the note children into `deck.md`, the project
Markdown twin, or `/llms-full.txt`.

Notes must not contain secrets. The focused deck HTML still contains the hidden `<aside>`, so speaker notes remain
public even though the audience view and public Markdown outputs omit them.

The project loader will treat `pitch.mdx` as an optional one-to-one child of the project. It will parse and validate
frontmatter at build time and expose a typed `PitchDeck` value through the project detail view. A missing file means
the project has no pitch-deck tab. No new project frontmatter switch is needed, so the file itself is the source of
truth.

### Embed the deck without an iframe

Projects with a deck will have three tabs:

1. Pitch deck
2. Overview
3. Architecture decisions, when present

The pitch deck will be the default tab for those projects. The long overview remains the default for projects
without a deck. The embedded deck will have a fixed aspect-ratio viewport, visible previous and next controls,
slide position, and an "Open deck" link.

The focused route will be:

```text
/projects/<project-slug>/deck
```

That route will render the same source and theme without the normal project chrome. It will enable reveal.js hash
navigation so a slide can be shared directly. It will also load the notes plugin and expose scroll, overview,
fullscreen, and speaker views. The project-page embed will keep hash navigation off so slide state cannot collide
with heading anchors in the overview.

An iframe is not needed for the chosen engine. Direct rendering gives the deck the same origin, component registry,
font loading, theme variables, analytics boundary, and accessible document as the rest of the project page.

### Treat mobile reading and presenting as different layouts

On wide screens, the embedded deck will use a 16:9 stage. On narrow screens, it will use reveal.js scroll view or
offer the transcript directly rather than shrinking a desktop slide until its text becomes unreadable. Animations
and fragments continue to work in scroll view, which lists slides in reading order. It can activate
automatically at a configured mobile width. See the
[scroll-view documentation](https://revealjs.com/scroll-view/).

The dedicated route may keep conventional slide navigation in landscape fullscreen. The embed should favor
self-guided reading.

### Make the transcript part of the feature

The deck source will generate:

```text
/projects/<project-slug>/deck.md
```

The transcript will contain deck metadata and every public slide in order, with slide numbers and headings.
Interactive components will use the existing Markdown converter rules: unwrap meaningful child content, convert
Mermaid to a code block, convert images to absolute URLs, and use a labeled placeholder only when no text
alternative exists.

The project Markdown twin will link to the deck transcript near the start of the page. `/llms.txt` and
`/llms-full.txt` will index each deck. The concise account of each project will remain available to search, screen
readers that prefer linear content, and agents.

### Make accessibility a release requirement

The deck will not auto-advance. Navigation must work through visible buttons and the keyboard, and the embedded deck
must capture arrow and space keys only while focused. Slide changes need an announced position such as "Slide 3 of
10" without moving focus on every transition. Focus must return predictably after leaving fullscreen or speaker
view.

Every deck must meet the same content rules as the rest of the site:

* one useful heading per slide;
* semantic text rather than screenshots of text;
* alternative text for informative images;
* sufficient contrast in both themes;
* no meaning conveyed only by color or animation; and
* a complete linear transcript.

The [W3C carousel guidance](https://www.w3.org/WAI/tutorials/carousels/) requires keyboard operation, user control
over movement, communicated slide changes, and comprehensible focus management. Automated checks do not prove
these behaviors, so the spike and later deck changes need keyboard and screen-reader smoke tests.

### Export PDF from the focused route

Version one will support the web deck and PDF. The focused route will use reveal.js's print stylesheet and the
`?print-pdf` mode documented in its [PDF export guide](https://revealjs.com/pdf-export/). A mise task may later run
Chromium to produce a reproducible PDF artifact, but generated PDFs will not be committed until there is a clear
download or archival need.

PPTX export is not a version-one requirement. Slidev and Marp can generate PPTX, but a rasterized or partially
editable PowerPoint is not the source of truth for these project pages. If editable office files become a real
distribution requirement, this decision should be revisited rather than adding a fragile conversion step.

### Load the presentation code only where needed

The reveal.js client and presentation CSS will load only on project pages that have a deck and on focused deck
routes. Project cards, projects without decks, blog posts, and ordinary ADR pages must not pay the client-bundle
cost.

The spike will record compressed JavaScript and CSS deltas for the embedded project page. Acceptance requires no
regression to the current project page before the deck enters the viewport. If a dynamic import is required, the
server-rendered transcript link and a stable aspect-ratio placeholder must remain available before hydration.

## Research snapshot

Popularity is a weak proxy for suitability, but it helps distinguish maintained tools from abandoned examples.
I recorded the following figures on 2026-08-30. Weekly downloads cover 2026-08-23 through 2026-08-29 and come
from the [npm downloads API](https://github.com/npm/registry/blob/main/docs/download-counts.md). The raw counts are
available for [Slidev](https://api.npmjs.org/downloads/point/2026-08-23:2026-08-29/%40slidev%2Fcli),
[reveal.js](https://api.npmjs.org/downloads/point/2026-08-23:2026-08-29/reveal.js),
[`@revealjs/react`](https://api.npmjs.org/downloads/point/2026-08-23:2026-08-29/%40revealjs%2Freact),
[Spectacle](https://api.npmjs.org/downloads/point/2026-08-23:2026-08-29/spectacle),
[Marp CLI](https://api.npmjs.org/downloads/point/2026-08-23:2026-08-29/%40marp-team%2Fmarp-cli),
[Marpit](https://api.npmjs.org/downloads/point/2026-08-23:2026-08-29/%40marp-team%2Fmarpit), and
[MDX Deck](https://api.npmjs.org/downloads/point/2026-08-23:2026-08-29/mdx-deck).

| Option                                                                                                  | Popularity and activity                                                                                | Relevant functionality                                                                                                              | Fit here                                                                                                                                                                  | Estimated production effort                                                                                   |
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| [Slidev](https://github.com/slidevjs/slidev)                                                            | 48.3k GitHub stars, about 68k weekly `@slidev/cli` downloads, active in August 2026, MIT               | Best Markdown authoring, Vue components, themes, presenter mode, recording, drawing, diagrams, static SPA and PDF, PNG, PPTX export | Strong product reference, poor runtime fit because it adds Vue, Vite, UnoCSS, a separate app, and an iframe for isolation                                                 | 6 to 10 engineering days for a supported build, theme, embed, transcript, and CI path                         |
| [reveal.js](https://github.com/hakimel/reveal.js) with [`@revealjs/react`](https://revealjs.com/react/) | 72.2k stars, about 109k weekly core downloads, about 11k wrapper downloads, active in August 2026, MIT | Embedded mode, keyboard and touch, fragments, overview, scroll view, speaker notes, fullscreen, plugins, PDF                        | Best engine fit. Try the published React package first; its small source makes vendoring and pruning a practical fallback                                                 | 4 to 7 engineering days for the spike, content model, routes, theme, transcript, tests, and first deck        |
| [Spectacle](https://github.com/FormidableLabs/spectacle)                                                | 10.2k stars, about 49k weekly downloads, last repository push in April 2026, MIT                       | React and Markdown slides, components, presenter mode, notes, keyboard controls, browser PDF                                        | Viable, but it adds styled-components, react-spring, its own Markdown stack, and 20-plus direct dependencies. Its docs also require a client component in Next App Router | 6 to 9 engineering days, with more design-system and dependency integration than reveal.js                    |
| [Marp CLI](https://github.com/marp-team/marp-cli) and [Marpit](https://github.com/marp-team/marpit)     | 3.8k and 1.4k stars, about 60k and 95k weekly downloads, active in July and August 2026, MIT           | Plain Markdown, CSS themes, static HTML, images, PDF, PPTX, presenter notes                                                         | Strong for generated documents. Weak for direct MDX and React-component reuse; an HTML build would need an iframe or sanitised injection                                  | 4 to 7 engineering days for a static generation and embed pipeline, with lower interactivity                  |
| [MDX Deck](https://github.com/jxnblk/mdx-deck)                                                          | 11.5k stars but about 550 weekly downloads; repository last pushed in January 2023, MIT                | MDX, React components, themes, notes, presenter and overview modes                                                                  | Conceptually close, but tied to Gatsby, Theme UI, and Emotion, and no longer active enough for a new dependency                                                           | 5 to 8 engineering days plus immediate ownership of aging dependencies                                        |
| Local engine using existing Embla and vendored reference code                                           | Embla is already installed; no new presentation dependency                                             | Build slide state locally and borrow focused pieces for deep links, fullscreen, notes, print layout, and accessibility              | Full control, but more browser behavior remains ours than with reveal.js core                                                                                             | 6 to 10 engineering days for the version-one feature set, before advanced export or presenter synchronization |

The estimates include implementation, tests, and one representative recipe-site deck. They do not include the
editorial work of rewriting every project. They are planning estimates based on repository inspection, not vendor
claims.

### Why reveal.js over Slidev

Slidev should influence the authoring experience, but using it directly would split the site into two applications.
Its static SPA can be hosted under a subpath, so direct use is technically possible. The cost is continued ownership
of both stacks, cross-app theming, duplicate Markdown behavior, iframe sizing and focus, and a separate transcript
pipeline.

reveal.js supplies browser presentation behavior without choosing the content compiler or component library. Its
official React package handles the lifecycle while local project components control the authoring API. If that
split proves awkward, the wrapper is small enough to bring into the repository and simplify. Slidev's parser
remains available as permissively licensed reference code if the remark slide grouping becomes more complicated,
but its Vue renderer offers little reusable value here.

### Why reveal.js over Spectacle

Spectacle is a credible fallback. Its [current documentation](https://github.com/FormidableLabs/spectacle/blob/main/docs/index.mdx)
supports React or Markdown, presenter mode, and Next App Router through a client component. Its
[presentation controls](https://github.com/FormidableLabs/spectacle/blob/main/docs/presenting-controls.mdx) include
PDF, print, and presenter query modes.

Its component and theme system would overlap more with this site. The current package brings its own styling,
animation, syntax-highlighting, Markdown, history, keyboard, and command-palette dependencies. reveal.js can remain
a presentation engine under local MDX and local visual components.

### Why not use Marp as the main renderer

[Marpit](https://github.com/marp-team/marpit) is deliberately a small Markdown-to-static-HTML framework, and
[Marp CLI](https://github.com/marp-team/marp-cli) has the strongest document export story in this comparison. It can
emit HTML, PDF, PPTX, and images from plain Markdown.

That strength does not match the primary requirement. Project decks need to reuse the site's MDX components and sit
inside an existing React page. Marp output would either run as another iframe document or require a second HTML and
CSS integration path. Marp remains worth reconsidering if office-file distribution becomes more important than live
web components.

### Why not adopt MDX Deck

MDX Deck proves that the desired authoring model works. It has MDX, React components, themes, presenter notes, steps,
overview mode, and keyboard shortcuts. The last push to its [repository](https://github.com/jxnblk/mdx-deck) was in
January 2023, its weekly usage is now small, and its Gatsby, Theme UI, and Emotion architecture does not match this
site. Treat it as a reference, not a new dependency.

## Vendoring boundary and effort

All shortlisted projects use the MIT license. A few thousand lines of TypeScript are small enough for this
repository to own, especially when coding agents can trace call sites, port tests, and compare later upstream
changes. Vendoring also lets the implementation remove framework abstractions and expose the exact MDX and React
components the site needs.

The repository inspection behind this ADR found:

* the Slidev parser is about 1,200 lines of TypeScript and includes frontmatter, notes, file offsets, and some
  Vue-oriented asset detection;
* the official reveal.js React wrapper has about 1,400 production lines and 1,300 test lines, while its `Deck`
  component is about 250 lines;
* Marpit has about 4,300 source lines and uses markdown-it and PostCSS;
* Spectacle's core has about 7,400 TypeScript lines before tests; and
* reveal.js core has about 11,900 JavaScript lines, with about 2,400 more across its bundled plugins.

None of these sizes rules out vendoring, including reveal.js core. Size becomes a concern when the required code
reaches tens or hundreds of thousands of lines. Upstream activity is a separate concern: a compact, stable module
is easy to own, while a fast-changing module may be worth consuming as a dependency or syncing selectively.

Start with both reveal.js packages as pinned dependencies. Their upstream browser and React testing has value, and
the spike should find out whether their public APIs already fit. Their source size is manageable if either package
later proves awkward to integrate.

For the spike:

1. Add `reveal.js` core as a pinned dependency.
2. Add `@revealjs/react` as a pinned dependency and use its `Deck`, `Slide`, `Fragment`, and notes support.
3. Put the project-facing names `ProjectPitchDeck`, `PitchSlide`, `PitchStep`, and `PitchNotes` in a thin local
   component layer. This layer contains site-specific props and styling; it does not copy library internals.
4. Write the MDX slide-grouping plugin and Tailwind theme locally.
5. Test static export, React 19 lifecycle, bundle loading, keyboard focus, speaker notes, scroll view, and PDF.

If those tests pass, keep the dependencies. Do not vendor code without a concrete integration problem.

If the wrapper fails those tests or forces unwanted Markdown, code-rendering, configuration, or styling behavior,
copy its relevant source and tests into `ui/components/projects/pitch-deck/reveal`. Remove the unused paths and
record the upstream repository, file paths, commit, copyright, and MIT license beside the copied code. Review later
upstream changes only when they affect retained code.

The same rule applies to reveal.js core. Try the package first. If its package boundary causes integration or bundle
problems, measure the required module closure and vendor it if that produces a cleaner implementation. The current
source count does not prohibit that choice.

Vendoring and pruning the wrapper should take less than a day if the spike finds a concrete reason to do it. The
four-to-seven-day estimate already includes that contingency.

## Validation before acceptance

This ADR can move to Accepted after one recipe-site pitch deck proves the following:

1. `next build` completes with static export and no client-only code leaking into server evaluation.
2. The project page embeds the deck without an iframe and the focused deck route works from the exported files.
3. React 19 development mode mounts, unmounts, and remounts the deck without duplicate listeners or broken controls.
4. A slide can contain prose, an internal link, a responsive image, a Mermaid diagram, a Shiki code block, and one
   existing interactive React component.
5. Wide-screen slide view and narrow-screen scroll view remain readable without clipped content.
6. Keyboard navigation is active only while the embed is focused; controls have accessible names; reduced-motion
   mode removes transitions; and a screen-reader smoke test announces position without trapping focus.
7. The focused route supports a direct slide URL, fullscreen, speaker notes, overview mode, and Chrome PDF output.
8. `/projects/recipe-site/deck.md` contains the complete public transcript, omits speaker notes, and is indexed in
   `/llms.txt` and `/llms-full.txt`.
9. Projects without `pitch.mdx` render exactly as they do now.
10. Unit tests cover deck discovery, frontmatter validation, slide grouping, transcript conversion, and tabs. A
    slide-grouping fixture covers ordinary project MDX plus `---` inside fenced code and JSX. A browser test covers
    navigation, focus-scoped keys, the focused route, and the mobile reading mode.
11. The production bundle report records the compressed JS and CSS delta and confirms that non-deck pages do not
    load reveal.js.
12. Tests cover the React integration lifecycle. Any vendored source retains the relevant upstream tests and has an
    MIT notice tied to a commit.
13. A `PitchNotes` fixture contains a unique sentinel. The browser test finds it in the focused route's
    `aside.notes` and in speaker view. Transcript tests prove that it does not appear in `deck.md`, the project
    Markdown twin, or `/llms-full.txt`.

If reveal.js core resists the static export or local component model, the spike will compare vendoring its required
modules with using Embla plus selected reveal.js or Slidev logic. A second application stack is a worse trade than
owning a modest amount of TypeScript.

## Consequences

### Positive

* Projects gain a concise entry point without deleting their detailed product and architecture record.
* Decks remain content as code, reviewed and versioned beside the project they explain.
* Authors can reuse the site's React components, diagrams, code renderer, images, fonts, and theme tokens.
* reveal.js covers presentation behavior that would otherwise take much more local code and browser testing.
* A thin local component layer keeps the project's authoring API independent of the chosen React integration.
* Vendoring remains available if either published package creates avoidable integration problems.
* Focused, embedded, scroll, presenter, and PDF views come from one source.
* The transcript keeps the content searchable, accessible in linear form, and useful to agents.

### Negative

* reveal.js adds client JavaScript and CSS to deck pages.
* The first implementation adds both reveal.js and its React wrapper as dependencies.
* If the spike leads to vendoring, the repository must pull in relevant upstream fixes deliberately.
* Slide layouts introduce another visual system that needs design review and responsive testing.
* `pitch.mdx` and the long project overview can drift or repeat claims. Deck slides should link to the detailed
  section and state dates for market facts rather than copying large passages.
* PDF output depends on browser print behavior unless a later CI task pins Chromium and the export command.
* The audience view hides presenter notes, but the notes remain public data.

### Follow-up work after acceptance

After the recipe-site spike, convert projects one at a time. Prefer projects where a deck changes how quickly a
reader can understand the work. Do not create empty decks only to make every project page look uniform.

Later decisions may add a shared deck theme, automated PDF artifacts, slide-level analytics, or editable office
exports. None of those are required to introduce project pitch decks.

---

Markdown index of this site: https://robbiepalmer.me/llms.txt
