EN FR

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.

4 min read9 sectionsChapter 17 / 22

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.

old-site/home.html html
<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:

  1. a simple field lives under blocks.; a list field under collections.{list}.{id}.;
  2. 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;
  3. an image needs an alt, a width and a height. Without them, bootstrapping refuses to write.
Style variants are read back

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

bash bash
# 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
OptionDefaultRole
--htmlThe file to read. Required.
--pagehomeName of the produced content file.
--languefrDestination language folder.
--essaiWrites nothing; shows the result and what could not be derived.
--sortieWrite 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.
Why it refuses to write

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:

astro astro
<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. The Collection component 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

bash bash
npm run build && npm run check

Then, on the deployed site:

bash bash
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

CaseWhy
FormsOut of scope. They stay what they were — a third-party service, a mailto:, whatever already existed.
Browser-generated contentIf 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 visitA 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

  1. The home page, alone

    It changes most often, and it proves the whole chain.

  2. 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.

  3. The pages whose content changes

    Services, prices, news. The ones that motivated the migration.

  4. Frozen pages last — or never

    Legal notice, 404 page: making them editable brings nothing and adds zones to watch.

A complete example is versioned

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.