Replacing a component across a CMS-driven website sounds like a frontend task until you encounter a published page with someone else's unpublished edits.
Now the operation has two consequences: changing the component and deciding which content becomes public.
While working on the CometChat website, I handled a Storyblok migration from an older pre-footer block to a newer form section. The replacement already existed on another page, including nested content and form configuration. The job was to reuse it across the relevant pages while preserving placement and respecting each page's publication state.
The interesting engineering problem was controlling what else could change along the way.
1. Define the migration contract before touching content
A component name is only one part of a CMS block's contract. Its fields, nested blocks, references, and position in the page all affect behaviour.
For this migration, recreating the replacement from its visible appearance would have been a weak approach. The source included configuration behind the rendered section: form settings, nested points, an anchor, and a logo grid.
Copying the approved source block preserved that configuration more reliably than rebuilding it field by field.
That choice has a boundary, though. Reusing an entire block is appropriate when the destination pages are meant to receive the same configuration. If they need different tracking values, copy, or form destinations, those differences need an explicit mapping. A visually correct replacement can still submit to the wrong workflow.
Before running a similar migration, I want four invariants written down:
- Every targeted occurrence is replaced in its original parent field and position.
- Replacement configuration matches the approved source, with only documented per-page overrides.
- Unrelated page content remains unchanged.
- Publication follows the agreed policy for that page's starting state.
These give the review something stronger than “the new section looks right.”
2. Publication state belongs in the migration logic
Our scope distinguished three cases: published pages without pending changes, drafts, and published pages with unpublished changes.
The authorized published pages were updated and republished. Drafts were updated and saved as drafts. Pages with pending changes were left untouched.
That last distinction matters. A page being live does not mean its latest saved content is identical to the live version. Storyblok exposes publication and unpublished-change information separately. Publishing a modified story can also release unrelated edits already present in that saved version.
For this scope, “publish everything that was already published” would therefore have been too broad.
The policy was effectively:
| Starting state | Migration action |
|---|---|
| Published, no pending changes | Replace, verify, then publish within the approved scope |
| Draft | Replace, verify, then save without publishing |
| Published with pending changes | Skip for separate review |
This is a decision about editorial ownership as much as implementation. The migration should not silently decide that someone else's work is ready to ship.
Storyblok's story object documentation describes the separate state fields; its update documentation distinguishes saving from publishing.
3. Count occurrences, not just pages
Some pages contained the old block more than once. A page-level checklist could record success after replacing the first occurrence while leaving another behind.
The unit of work needs to include the block's location: page, parent field, and occurrence. In a script, I would also capture a stable block identifier where available; array indices alone can shift as blocks are removed.
For each page, the audit should reconcile:
- old-component count before and after;
- new-component count before and after;
- original placement of each replacement;
- configuration of the inserted blocks.
If a page starts with two old blocks and one existing new block, replacing both old blocks should leave zero old blocks and three new ones. Merely checking that the new component exists would miss a partial migration.
Even that arithmetic is insufficient on its own: an extra block in the wrong section can satisfy a count check. Structure and values need to agree.
4. Design automation for interruption and concurrent edits
The CometChat work used the CMS editing workflow with per-page verification. If I were turning the same process into a reusable migration runner, I would separate planning, mutation, and publication.
The planning step would record the target occurrences, starting state, source revision, and intended replacements. Immediately before writing, the runner would re-read the page and compare it with that plan.
If content or publication state had changed, it would stop processing that page and flag the conflict. A fresh read reduces the risk of overwriting an editor's work, although it does not eliminate the race between reading and writing. Where the API lacks an atomic version precondition, an agreed editing pause or another coordination mechanism is still needed.
Retries also need a defined outcome. Running the transformation twice should not insert the replacement twice:
migrate(migrate(content)) = migrate(content)
That property applies to the content transformation. Publication is a separate side effect and needs its own recorded outcome.
After a timeout, I would read back the stored content before retrying. “No response received” does not establish that the write failed.
5. Rollback spans both code and content
A CMS migration creates a compatibility window: some pages may use the old component while others use the new one.
For a migration that also requires renderer changes, I would deploy support for the new shape while retaining support for the old shape, migrate and verify content, then remove the old renderer only after checking the remaining dependencies.
This makes partial completion manageable. It also avoids a rollback that restores old content into an application that can no longer render it.
Before changing content, preserve the original blocks and their locations. For published pages, account for both the live version and any saved draft. Restoring a whole historical story can overwrite legitimate edits made after the migration, so rollback needs the same conflict checks as the forward operation.
6. Verify data and rendered behaviour separately
The content audit answers whether the intended transformation was stored. Browser verification answers whether that content behaves correctly in the application.
For a form section, my verification plan would cover field configuration and placement first, then preview rendering, responsive layout, anchor behaviour, and the intended submission destination. Any submission test should use an agreed test path so verification does not create real sales leads.
For pages approved for publication, I would check the live delivery path as well. A correct preview does not establish that the published version, build, or cache has updated.
The CometChat migration made the scope of these changes much clearer to me. A reusable CMS component connects frontend code, stored content, and an editorial workflow. A sound migration has to preserve the contracts between all three.
Top comments (0)