Skip to content

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

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

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

Conversation

@geotrev

@geotrev geotrev commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

POC 🚧

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.
…port

Authoring a deck is two steps — write deck.draft.json, then import it — and
the second is the one that gets dropped, leaving the app rendering a deck the
draft has moved on from. Most visibly on the first prompt, where the result is
a deck that looks empty, but it is not specific to it.

A draft is now applied on its own: from `npm run predev` before the server
boots, and from the dev server's watcher if it appears after. Which one fires
is a race with npm install, so both are needed.

The deck records which draft content it already reflects, so a draft applies
once per distinct content and a restart never re-applies one. Editing in the
app does not change the draft, so work done since is what survives. That
record is content, not an mtime: mtimes do not survive a project being stored
and materialized again, so comparing them could re-apply a stale draft on
every open. Everything that brings deck and draft into agreement — importing
it, exporting over it — records it too, as does replacing the deck with
something else, since a restart must not overrule that choice.

Applying a draft repeatedly is only safe because importing is no longer a
wholesale replace: slide ids now travel through the draft format, so a slide
that carries one keeps its identity and its comments. Comments are dropped
only when the slide they were written on is actually gone.
Editing a slide did nothing: the typed text showed for a frame and snapped
back, with nothing logged. Slide text is contenteditable, so keystrokes only
reach the deck on blur and are committed by slide id — and the browser was
left holding ids the deck no longer had.

Two things put it there, both on the path the first prompt takes. The watcher
ignored any write landing within a second of the server's own save, which is
exactly where the skill's import lands, so the browser was never told. And
every import minted fresh ids for slides authored without them, which is
every slide the skill writes, so a missed import was fatal rather than
cosmetic.

The watcher now compares the file it last wrote instead of guessing from
elapsed time, a slide authored without an id inherits the one already in its
position, and a write to an unknown slide says so and re-reads the deck
rather than returning in silence.

The dev-server test moves to its own file: a second Vite process in the same
file cannot start.
Visual editing did nothing: a character appeared and was gone a millisecond
later, and formatting the same. The field hands its content to React as an
object, and React 19 re-applies it whenever that object is not the one it saw
last — by identity, not by comparing the HTML inside it. React 18 compared the
string, so building the object inline each render used to be free; now it
rewrites the field on every render, and typing renders constantly because the
caret moves and the format bar follows it.

Holding the object still until the value really changes is what lets the
browser own the field while it is being edited. Correct under either version:
19 skips the write because the reference is unchanged, 18 because the string
is.
Present and the presenter console opened a second tab, which a WebContainer
preview URL cannot survive: only the tab connected to the project can resolve
it, so presenting from a Bolt preview landed on "connect to project" and then
nothing (webcontainer-core#1087, open since 2023 and broken in every browser).
Slides v1 never left the page, which is why this is a regression.

A window is still right when the app owns its tab — the deck goes up on the
projector while the editor stays put — so the choice lives in one place and the
frame decides. Presenting in place needs a way back, hence the dock's exit for
anyone who has an editor to return to, and a flush of pending saves before
leaving, since text fields commit on blur into a debounce that the navigation
was racing.
A share link built from the address the editor runs on opens for nobody
else: a preview URL belongs to the one tab connected to the project, and
localhost belongs to one machine. So the deck records where it was
published and every link is built on that, with the Share button disabled
until it knows.

What a published deck can enforce is nothing — it is a static build with no
server — so the dialog offers a public presentation link and a presenter
link that says outright that the notes in it are public, rather than the
per-mode links and passwords only a backend can check.
@geotrev geotrev closed this Aug 21, 2026
@geotrev
geotrev deleted the geotrev/bou-2538-add-slides-v2-to-starters-repo branch August 21, 2026 17:15
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