Skip to content

docs(skills): reload open pages after the server has the saved content - #676

Closed
tlgimenes wants to merge 1 commit into
mainfrom
docs/v8-content-reload-after-update
Closed

tlgimenes wants to merge 1 commit into
mainfrom
docs/v8-content-reload-after-update

Conversation

@tlgimenes

@tlgimenes tlgimenes commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

The deco-v7-to-v8-migration reference (reference/content.md, "Editing locally") told sites to send full-reload from a Vite hotUpdate hook on .deco/blocks.gen.ts. That hook fires before the worker re-imports the content, so the reloaded page renders the previous save, and the site editor's local preview (Localhost and /site-editor) stays one edit behind.

The bullet now says to send the reload from cms.ts's import.meta.hot.accept callback, after createCMS takes the new module (import.meta.hot.send("site:content-updated")), and to have the plugin listen in configureServer (server.environments.ssr.hot.on(...)) and send full-reload from there.

The same fix is in the demo store (deco-sites/demo-storefront#131, be699ee). With deco serve + vite dev: before, 1 of 4 saves showed on the reload; after, 5/5 single saves and 4/4 save bursts did.

Docs only. bun run skills:check and skills:readme:check pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig


Summary by cubic

Updates the deco-v7-to-v8-migration reference to reload open pages only after the server has the saved content. The old instruction sent full-reload from a hotUpdate hook on .deco/blocks.gen.ts, which fires before the worker re-imports the content and leaves the site editor preview one save behind. The reference now directs sending the reload from cms.ts's import.meta.hot.accept callback after createCMS takes the new module, with the plugin listening in configureServer.

Written for commit fad48d8. Summary will update on new commits.

View guided diff Turn on auto-fix

The v7-to-v8 migration reference told sites to send full-reload from a
hotUpdate hook on .deco/blocks.gen.ts. That fires before the worker
re-imports the content, so the reloaded page renders the previous save and
the site editor's local preview stays one edit behind. Send the reload from
cms.ts's accept callback after createCMS takes the new module instead
(demo-storefront be699ee).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
@tlgimenes

Copy link
Copy Markdown
Contributor Author

Superseded by #690: decoContent() from @decocms/blocks/vite does this reload timing itself, and #690 rewrites the same "Editing locally" section, with this PR's content folded in. Not closing it here; close it when #690 lands.

tlgimenes added a commit that referenced this pull request Oct 14, 2026
…he saved content (#690)

Vite sites no longer need template code to show a block saved with `deco
serve`. Today every v8 Vite site carries two pieces: an
`import.meta.hot.accept("../.deco/blocks.gen", ...)` block in
`src/cms.ts` that calls `createCMS` again and sends
`site:content-updated`, and a `site-content-reload` plugin in
`vite.config.ts` that turns that event into `full-reload`. This PR ships
that as one opt-in Vite plugin:

```ts
// vite.config.ts
import { decoContent } from "@decocms/blocks/vite";

export default defineConfig({ plugins: [/* cloudflare(), tanstackStart(), react(), */ decoContent()] });
```

Only Vite sites add it. Next.js sites never import it, so nothing
changes for them. The generated `.deco/blocks.gen.ts` on disk is
unchanged (`deco content` is untouched).

**Supersedes #676.** That PR fixed only the timing, in the docs (reload
after `createCMS` takes the new module, not on the file change). This PR
makes the plugin do that and rewrites the same "Editing locally"
section, so #676 can be closed.

## How it works

- **`@decocms/blocks/vite`, `decoContent()`**: `apply: "serve"`. In a
server environment only (`this.environment.config.consumer ===
"server"`, or `ssr` before the Environment API), its `transform` hook
finds each module that statically imports the site's
`.deco/blocks.gen.ts`. It reads the module's static imports and
re-exports and resolves each one with `this.resolve`, so any specifier
form counts: `"../.deco/blocks.gen"`, an alias like
`"~/.deco/blocks.gen"`, with or without `.ts`. Modules under
`node_modules`, non-JS files and the content module itself are skipped.
To each importer (usually `src/cms.ts`) it appends the block sites used
to write by hand:
  ```js
import { adoptContent as __decoAdoptContent } from
"@decocms/blocks/vite/hot";
  if (import.meta.hot) {
    import.meta.hot.accept("../.deco/blocks.gen", (next) => {
      if (next) __decoAdoptContent(next.default);
      import.meta.hot?.send("decocms:content-updated");
    });
  }
  ```
The block is appended, so no line moves and the source map holds (`map:
null`). The importer is the update's HMR boundary, so neither it nor
anything above it (the router entry, `routeTree.gen`) is invalidated or
runs again. JSON block edits bubble up through `blocks.gen.ts` to the
same boundary.
- **Already accepted by hand:** if the importer already has an
`….accept(<specifier>)` (string or array) that resolves to the content
module, the plugin adds nothing to it.
- **Pre-bundled hot module:** `configEnvironment` adds
`@decocms/blocks/vite/hot` to `optimizeDeps.include` in the server
environments. The appended import appears only after the transform, so
the dependency scan can't see it. Discovered on the first request
instead, the worker re-optimizes its dependencies and reloads the whole
program once.
- **One reload per save:** `configureServer` listens for the event on
every server environment (the `@cloudflare/vite-plugin` worker whatever
it's named, `rsc`, plain `ssr`) and sends `{ type: "full-reload" }` to
the client environment. It waits 100 ms because a save swaps the module
twice (the block's JSON, then `blocks.gen.ts`), so open pages reload
once.
- **`adoptContent(content)`** (`src/v8/cms.ts`, exposed only on
`@decocms/blocks/vite/hot`): hands the module to every CMS instance
keyed for that content's `.deco` root (`module:<root>`, with or without
`|site:…`), the same adopt path a second `createCMS` call takes. It
doesn't need the site's config: each instance re-adopts with its own
stored config, so a hosted CMS keeps its `site`/`dev`. It returns how
many instances took it. With none (no CMS yet, or a module without
`root`) it does nothing.
- `vite` is an optional peer (`>=6.0.0`, for the Environment API) and a
devDependency. The plugin uses `import type` only, so nothing is
imported from vite at runtime. New `exports` entries are `./vite` and
`./vite/hot` (types/source/default, like the others), and `bun run
build` emits `dist/v8/vite/*`.

**Sites that still have the old code:** they keep working.
- Without the plugin, nothing changes.
- With the plugin and the old `cms.ts` accept block both in place, the
plugin leaves that module alone. The old callback re-creates the CMS and
the old `site-content-reload` plugin reloads the page, as before. If you
delete only the old Vite plugin, pages don't reload, so the migration
reference says to delete both together.
- The older docs recipe's reload plugin is a `hotUpdate` hook on the
file change. That hook fires early, so the reference says to delete it.

## Review fixes

- **`optimizeDeps` (041a9b4):** the server environments pre-bundle
`@decocms/blocks/vite/hot`. Without that, the first request found the
new import and re-optimized the worker, which reloaded the whole program
once.
- **Importer boundary instead of a self-accepting content module
(ea8618c):** the first version made `blocks.gen.ts` accept its own
updates. Vite still invalidates every importer of an updated module
unless the importer accepts it. TanStack Start imports its server entry
again on each request, so with the cloudflare plugin `src/cms.ts` and
the route tree re-ran on every save. On the demo store, bursts of saves
gave 500s (`Cannot read properties of undefined (reading 'method')` in
`handleServerAction`, `Route` undefined in `routeTree.gen`), and the
client then stopped reloading. The plugin now writes the accept that
worked before (the hand-written `cms.ts` block had 14/14 clean bursts),
on the importer.

## Tests

- `src/v8/vite/vite.test.ts` (unit tests):
- `adoptContent`: the same instance, another `.deco` root left alone, a
hosted instance keeps its options, nothing before a CMS exists or for a
root-less module.
- The import reader: static imports and re-exports, not dynamic
`import()` or `import.meta`. It also finds accepted dependencies in
string and array forms.
- The transform: relative, alias, `.ts` and bare-alias specifiers
resolving to the content module get the block with their own specifier.
Nothing is added in the client, to the content module itself, under
`node_modules`, to non-JS files, to modules that don't import it, or to
a module that accepts it by hand. An unrelated accept doesn't count. The
`ssr` flag works without the Environment API.
- `optimizeDeps.include` only in server environments; `configureServer`
listens on every server environment, and two events make one
`full-reload`.
- `src/v8/vite/devServer.test.ts` (integration): a real Vite dev server
(`createServer`, SSR `RunnableDevEnvironment`). Like TanStack Start,
every "request" imports the site entry again (`entry.ts → cms.ts`, which
imports `~/.deco/blocks.gen` through an alias).
- Three saves: each gives a `full-reload`, and a request at that moment
already resolves the new content.
- **Regression:** the test counts module runs. `cms.ts` and the entry
each ran once, and the transformed `cms.ts` has exactly one accept while
`blocks.gen.ts` has none. With the previous self-accepting design the
same test fails with `{ cms: 4, entry: 4 }`.
- A `cms.ts` with its own accept block keeps exactly one accept, its
callback runs, and the plugin sends no reload.
- End to end with `@cloudflare/vite-plugin` (Node `vite dev`; 10 bursts
of 3 saves 120 ms apart, each save writing the block JSON and then
`blocks.gen.ts`; two request loops running throughout; a `vite-hmr`
WebSocket counting reloads; `cms.ts` logging each run):
- **Demo store** (deco-sites/demo-storefront#133 with this build, Vite
7, TanStack Start):
- This PR: 52/52 responses 200, `cms.ts` ran once in all, `hot updated:
/.deco/blocks.gen.ts via /src/cms.ts`, and the page had each burst's
last save 10/10.
- Previous self-accepting design: 12 of 54 responses were 500, `cms.ts`
ran 16 times, and 1 burst's page lacked the last save.
- **`examples/tanstack-smoke`:** 276/276 responses 200, `cms.ts` ran
once, and 10/10 bursts showed the last save. Saves more than 100 ms
apart each get their own reload, so a burst gave 1 to 3 reloads, never
more than its saves.
- `bun run check` (typecheck, biome, knip), `bun run build` and
`skills:check` pass. In `bun run test`, 1926 tests pass. The failures
are not from this PR:
- `delivery.cli.test.ts` DS-2: it greps `protocol/storage/fs/git.ts` for
`"reset"`, which #689 added. It fails on the base too.
- `schema.test.ts` (`is deterministic`, and one more): they hit the 5 s
timeout under full-suite load and pass when run alone (58/58).

## Also in this PR

- `examples/tanstack-smoke` and `examples/tanstack-rsc-smoke` add
`decoContent()`. The `ts-09` guide conformance test now expects that one
`@decocms/blocks/vite` import.
- `.agents/skills/deco-v7-to-v8-migration/reference/content.md`,
"Editing locally": add the plugin; drop the old snippet and plugin
together when you do; and why: the "client is not a function" and
one-edit-behind history from #676, and why the boundary is the importer.
- `README.md` mentions the plugin and the `/vite` subpath.

Follow-ups opened alongside:
- The TanStack guide snippet becomes the plugin line:
deco-sites/docs-tanstack#25. Merge it after the release.
- The demo store drops the template code: deco-sites/demo-storefront#133
(draft; it needs the release containing this fix).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@tlgimenes

Copy link
Copy Markdown
Contributor Author

Superseded by decoContent() in @decocms/blocks/vite (released in 8.2.2).

@tlgimenes tlgimenes closed this Oct 17, 2026
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