Chapter 17Operate
Migrating an existing site
Bringing an already-live static site under inline, without rebuilding it. Annotate the HTML, extract the content, render the page — page by page, nothing forces you to convert everything at once.
What this is not
This is not an automatic conversion. The tool can extract values from an annotated page; it cannot decide what the client is allowed to change. That decision is the real work, and it does not automate:
- a section title, yes; the company name in the footer, probably not;
- a testimonial, yes; the structure of the grid displaying them, no;
- an illustration photo, yes; the logo, to be discussed.
Count one hour per page for that sorting, and a few minutes for the rest. The operation goes page by page: a migrated page and a non-migrated page coexist without interfering.
1. Annotate the page
On the existing HTML, add attributes. Nothing else changes: not a tag moved, not a class removed.
<h1 data-cms="blocks.hero.title">Sourdough bread, every morning</h1>
<p data-cms="blocks.about.body" data-cms-type="richtext">
We <strong>knead</strong> every night.
</p>
<img data-cms="blocks.about.photo" data-cms-type="media"
src="/img/bakehouse.jpg" alt="The bakehouse at dawn"
width="1600" height="900" />
<figure data-cms="blocks.about.film" data-cms-type="media">
<iframe src="https://www.youtube.com/embed/aqz-KE-bpKQ" title="A night in the bakehouse"></iframe>
</figure>
<ul data-cms-list="collections.reviews">
<li data-cms-item="a-001">
<blockquote data-cms="collections.reviews.a-001.quote">The best bread in town.</blockquote>
<cite data-cms="collections.reviews.a-001.author">Claire D.</cite>
</li>
</ul>Three rules, and they are enough:
- a simple field lives under
blocks.; a list field undercollections.{list}.{id}.; - each item carries an id that is stable and never reassigned (
a-001,a-002) — it is the link between the page and the content; - an image needs an
alt, awidthand aheight. Without them, bootstrapping refuses to write.
If the old HTML already carries cms-* classes — because it was built with this
convention — bootstrapping derives the style tokens from them. Otherwise defaults apply and
styling comes from your CSS.
2. Extract the content
# Dry run: writes nothing, shows what would be produced
npm run bootstrap -- --html old-site/home.html --page home --langue en --essai
# Writes src/content/pages/en/home.json
npm run bootstrap -- --html old-site/home.html --page home --langue en| Option | Default | Role |
|---|---|---|
--html | — | The file to read. Required. |
--page | home | Name of the produced content file. |
--langue | fr | Destination language folder. |
--essai | — | Writes nothing; shows the result and what could not be derived. |
--sortie | — | Write somewhere other than the standard location. |
What is picked up automatically
- texts and their style variants;
- richtext, with its allowed markup — the rest is stripped;
- images, with their description and dimensions;
- videos, converted into provider + id;
- lists, with their ids;
- the page title and meta description.
If an image has no description, bootstrapping fails instead of writing an empty string. A plausible value invented at this stage ends up in production, invisible in review, and nobody ever fixes it. The schema applied here is the one used by the build and by publishing: what passes here passes everywhere.
An existing file is never overwritten. Bootstrapping run again by mistake must not erase what the client has already changed.
3. Render the page with Astro
The annotated HTML acts as the template. Each data-cms becomes a component that
reads the content:
<Editable data={data} path="blocks.hero.title" as="h1" />
<Media data={data} path="blocks.about.photo" />
<Collection data={data} name="reviews" item={Review} blank={{ … }} />Two things not to miss:
-
no
client:*directive on a component that displays content. The text must be in the raw HTML, otherwise it leaves the index of engines and assistants; -
every list needs its
<template>in the page, otherwise adding an item would require reimplementing a rendering engine in the browser. TheCollectioncomponent takes care of it.
npm run check verifies both.
4. The images
Copy the files into src/media/, lowercase, without accents or spaces —
bootstrapping already normalised the names in the JSON, and the files must follow.
Do not put them in public/: <Image /> would not process them,
and the site would lose AVIF, WebP and the width set it may already have had.
5. Verify
npm run build && npm run checkThen, on the deployed site:
curl -s https://the-site.com/en/ | grep -c "a page title" # must return 1
If it returns 0, the content is not in the raw HTML: look for a client:*
directive.
What does not migrate
| Case | Why |
|---|---|
| Forms | Out of scope. They stay what they were — a third-party service, a mailto:, whatever already existed. |
| Browser-generated content | If it is not in the HTML, there is nothing to extract. That is an SEO problem before it is a migration problem. |
| Pages whose structure changes on every visit | A carousel must contain all its items in the markup; JavaScript only scrolls them. |
If the site relies heavily on those three, migration is not the right tool: it is a sign that it should be made static first.
A migration order that works
-
The home page, alone
It changes most often, and it proves the whole chain.
-
Have the client validate it
Before migrating ten pages on the same convention. That is when you find out they also wanted to edit the opening hours in the footer.
-
The pages whose content changes
Services, prices, news. The ones that motivated the migration.
-
Frozen pages last — or never
Legal notice, 404 page: making them editable brings nothing and adds zones to watch.
scripts/fixtures/site-existant.html holds an annotated page covering every case
— text, richtext, image, video, list. It is also what npm run test:bootstrap
reads.