Skip to content

feat(bolt-slides): replace the slides template with slides v2 - #124

Open
geotrev wants to merge 12 commits into
mainfrom
geotrev/bou-2538-add-slides-v2-to-starters-repo
Open

feat(bolt-slides): replace the slides template with slides v2#124
geotrev wants to merge 12 commits into
mainfrom
geotrev/bou-2538-add-slides-v2-to-starters-repo

Conversation

@geotrev

@geotrev geotrev commented Aug 19, 2026

Copy link
Copy Markdown

Replaces the bolt-slides template with Donald's slides v2 studio, modernized to match the rest of the repo and made safe to publish.

Part of BOU-2538

What changes for users

v1 was a deck you edited as JSX. v2 is a slide studio: a deck is data — a layout plus JSON props — rendered through a layout registry, with an editor, presenter mode, annotation, and PDF/OG export. The agent authors decks by writing JSON and importing it, never by writing slide components.

Commits, in the order they are meant to be read

  1. Import the prototype as-is — easiest to review as a pure swap, nothing else mixed in.
  2. Re-apply feat(bolt-slides): modernize template toolchain #121's modernization — v2 was written against the older scaffold (React 18, Vite 5, TS 5.5, framer-motion). Brought up to React 19 / Vite 8 / TS 6 / motion, with feat(bolt-slides): modernize template toolchain #121's tsconfig project references and @/ alias.
  3. Alias rewrite — 200 relative specifiers across 56 files.
  4. The publish fix — see below.
  5. Docs — every doc in the prototype said publishing was impossible; that is no longer true.
  6. Build script — plain vite build, matching what Bolt's deploy actually runs.
  7. Tests.
  8. Prettier — mechanical, isolated so it does not bury the rest.

The publish problem

v2's API is Vite dev-server middleware. Publishing produces a static bundle where that middleware does not exist, so a published deck sat on "Loading deck…" forever — publishing a deck, the entire point of making one, would have regressed against v1.

The build now bakes the deck into the bundle as deck-snapshot.json and the client falls back to it, entering read-only present mode. Deciding "there is no API" needs care: Bolt's static deploy writes SPA _redirects, so Netlify answers /api/state with index.html and a 200, while a bare file server 404s. Both fall back; a 401/403 deliberately does not, since that is the API refusing us and falling back there would serve a gated deck to someone it just denied.

The snapshot carries deck meta, slides and speaker notes. Review status, comments, profiles and share rows are stripped, so no share token, password hash or internal review comment can ride along to a public URL. Speaker notes are public on a published deck — making them private needs a backend, which is the cloud follow-up.

configurePreviewServer is also gone, because mounting the API on vite preview made preview the one command that hid this bug.

Verification

  • tsc -b and vite build clean
  • format:check and package:check pass
  • new WebContainer tests pass, repeatedly
  • published behaviour exercised against both host shapes (SPA-rewrite and 404) with a populated deck: snapshot loads, ordering holds, notes survive, nothing sensitive leaks

What this does not cover

The cloud port (docs/cloud-setup.md) is now load-bearing rather than optional: it is the only route to the three things the snapshot deliberately cannot do — editing a published deck, access control, and keeping speaker notes private.

To be clear about what that is: it is not a follow-up PR in this repo. That doc is a spec shipped inside the template and addressed to whoever is working in a scaffolded deck project. The port is per-project by nature — its own Postgres tables, its own edge function holding a service role key, RLS with no policies, an owner secret in an env var — and the checklist ends by deleting server/ and scripts/deck.mjs from that project. A template cannot ship a database. So it happens when a user asks Bolt to make their published deck live, with the agent implementing the doc.

There is a defensible follow-up here, not filed yet: that port is currently ~150 lines of prose every project must reimplement from scratch, including scrypt hashing with constant-time compare, unlock throttling, and per-mode server-side stripping of speaker notes. That is security-sensitive code to re-derive per project. Shipping a ready-made edge function and migration SQL in the template would make it turnkey.

Paired with a draft PR in bolt that pins this branch for testing — merging here needs that pin updated to the merge commit before it reaches users.

Swaps the JSX-authored deck template for Donald's slides v2 studio
(github.com/inkko44/bolt-slides-starter), where a deck is data — layout
plus JSON props rendered through a layout registry — with an editor,
presenter mode, and PDF/OG export.

Imported as-is; the toolchain modernization from #121 is re-applied in
the following commit.

Excluded from the import:
- .bolt/mcp.json — prototype-local MCP servers (linear, notion, mobbin)
  that would be enabled for every user scaffolding this template
- data/allhands.json — an internal Bolt all-hands deck, unreferenced by
  any code and not appropriate for a public repo
- data/deck.json — generated at runtime from data/deck.seed.json
Carries the modernization Chris landed on the v1 template (#121) forward
onto slides v2, which was written against the older scaffold.

- React 18.3 -> 19.2, Vite 5.4 -> 8.2, TypeScript 5.5 -> 6.0,
  @vitejs/plugin-react 4 -> 6, framer-motion ^11 -> motion ^12
- build script typechecks first: tsc -b && vite build
- tsconfig split into project references, with the @/* -> src/* path
  alias and vite's resolve.tsconfigPaths to match

tsconfig.node.json additionally sets allowJs, which #121 did not need:
v2's vite.config.ts imports server/api.mjs, and without it tsc -b fails
with TS7016 on that untyped import.

The v2-only dependencies (@dnd-kit, zustand, jspdf, html-to-image) stay
at their current versions -- all are latest and declare open React peer
ranges, so React 19 needed no changes there.

tsc -b and vite build are both clean.
Rewrites all 200 relative specifiers under src/ (56 files) to the @/
alias configured in the previous commit. 133 of them were ../-style.

This is the change #116 made to bolt-vite-react-ts for a reason: deep
relative paths are a known source of miscounted-depth import errors in
agent-generated code, and v2's tree (data/, deck/, edit/, export/,
layouts/, present/, slide/) is deeper than v1's was.

Specifiers resolving outside src/ were left alone. tsc -b and vite build
are clean.
v2's API is Vite dev-server middleware (server/api.mjs). Publishing a deck
produces a static bundle where that middleware does not exist, so the app
sat on "Loading deck…" forever waiting on /api/state. v1 had no API and no
such problem; without this, publishing a deck — the whole point of making
one — regresses.

The build now bakes the deck into the bundle as deck-snapshot.json, and the
client falls back to it when there is no API, entering read-only present
mode. EditorApp already redirects to /present whenever mode !== 'edit', so
no new UI is needed.

Deciding "there is no API" needs care, because hosts disagree about paths
that do not exist: Bolt's static deploy writes SPA _redirects, so Netlify
answers /api/state with index.html and a 200, while a bare file server 404s.
Both fall back. A 401/403 explicitly does not — that is the API answering,
and falling back there would serve a gated deck to someone it just refused.

The snapshot carries only what renders: deck meta, slides, and speaker
notes. Review state (status, assignee), comments, profiles, and share rows
are stripped, so no share token, password hash, or internal review comment
can ride along to an unauthenticated public URL.

Speaker notes are public in a published deck. Making them private needs a
backend that can enforce it — that is the cloud follow-up, docs/cloud-setup.md.

Also drops the API from the preview server: `npm run preview` serves the
production build, and mounting the API there made preview the one command
that hid this bug.

Verified against both host behaviours (SPA-rewrite and 404) with a populated
deck: snapshot loads, ordering holds, notes survive, nothing sensitive leaks.
Every doc in the prototype told the reader that publishing is impossible
until they port to a cloud backend. That was true of the prototype and is
no longer true, and the agent reads SKILL.md on every deck request — left
alone it would talk users out of publishing a deck that now works.

- SKILL.md: publishing works read-only; publish after authoring, and
  speaker notes are public once published
- README: replace the "sits on a loading screen" section with what a
  published deck actually does, and note preview now shows the same
- cloud-setup.md: reframe from "required before you publish" to "required
  when a published deck must be live" — editing, comments, share links,
  passwords, private notes. Its checklist also said to delete server/*.mjs
  after porting, which now breaks the build unless the vite plugins that
  import it go too.

Restores .bolt/prompt, which v2 did not carry. It keeps #121's @/ alias and
Vite 8 notes and adds two v2-specific ones: author decks as JSON rather than
JSX, and do not "fix" the absent published API by deleting the snapshot
fallback.
The modernization commit made build `tsc -b && vite build`, copying
vite-shadcn. That is the wrong model for this template: templates.json
gives bolt-slides `"build": "vite"`, and framework-detection resolves
vite-react to `npx vite build`, so Bolt's deploy never runs the npm
script. Gating it on tsc only risks a stray type error blocking a local
build while changing nothing about publishing.

Matches bolt-vite-react-ts, which builds with plain `vite build` and keeps
typechecking as its own script. `npm run typecheck` still runs `tsc -b`,
which covers vite.config.ts as well as src.

cloud-setup.md's checklist said to build with the npm script "because that
is what the deploy runs" — it is not; corrected to `npx vite build`.
Two WebContainer tests, matching the shape of the other template tests.

The build test is the one that matters: it imports a deck through the CLI,
builds, and asserts the published snapshot actually carries that deck —
right slides, notes intact for presenter view, and no status, comments or
profiles, which must never reach an unauthenticated URL. Publishing breaks
silently when the snapshot goes missing, so it is worth a test.

The second test covers the authoring loop end to end: CLI import, dev
server, slide rendered in the editor.

Both live in one file with a single build, deliberately. A second
WebContainer in the same file cannot spawn binaries out of node_modules/.bin
once the first has built — `jsh: spawn vite EACCES` — which is why the
sibling test files also build exactly once.
The prototype was written to its own style (no semicolons, 100 columns).
Mechanical prettier pass so `npm run format:check` passes in CI — no
behaviour change. Kept as its own commit so it does not bury the rest.
@bolt-new-by-stackblitz

Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

Importing is how a deck gets authored, and it happens while the editor is
open. Nothing in the bundle imports data/deck.json, so the import produced
no HMR and the page kept rendering the old slides — the agent would report
slides it had added and none of them appeared.

The dev server now watches the deck file and asks the client to re-fetch.
Writes it made itself are skipped: the editor updates local state before
the request lands, so re-fetching on its own saves would race whatever the
user is still typing.
Two files were both called deck.json: the portable deck handed to the CLI
and the live database the app reads. The skill said the deck "persists to
data/deck.json" and then said to "write deck.json", so the agent treated
them as one thing and kept both in sync by hand — editing the database
directly, which skips the row fields the app depends on.

The authored file is now deck.draft.json, and a hard rule states that
data/deck.json is generated and reachable only through the CLI. The import
step is also spelled out as the thing that changes the deck: writing the
draft alone left decks empty while the agent reported slides as added.
A fresh scaffold rendered nothing until an import ran, so any hiccup in
authoring was indistinguishable from a broken template — the user saw a
blank presentation and no way to tell why.

The seed now explains what the project is and how to ask for a real deck,
so the first paint is always a working deck. It doubles as the fallback for
a build with no import, which previously snapshotted zero slides.
persistNow already claimed a stale copy must never clobber a newer write
from another process, but nothing enforced it. With an editor save queued
on its 250ms debounce, an import landing in that window was flushed away:
the CLI reported success, data/deck.json reverted to the server's cached
deck, and the slides never appeared. openDb made it worse by refusing to
reload while anything was unsaved, so the API kept serving the stale deck.

Both paths now yield to a newer file. An import is an explicit replace, so
it outranks a cached copy — at the cost of an edit made in the last 250ms,
which beats discarding the whole imported deck.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant