A snippet sequence · after the eighteen steps

The Site, Travelling

A snippet sequence in three steps: the whole site written down as a legible archive, and read back by another instance — a pair of doors, not a procedure.

The wish: an admin control that writes the entire site — every page, every image, all the metadata — into a zip file that a person can unzip and read, and that another instance of this program can swallow whole. Extract, transform, load; except the transform is chosen so gently that the whole procedure is a pair of buttons.

A snippet sequence obeys the three rules of the original, plus three of its own. It must attach at points in the existing structure you can name — if it has to be smeared across the system, it belongs to a different sequence. It must leave a live datastore untouched, or touch it only by addition — existing entities never learn that anything happened. And it must be the least of all sufficient changes: where the organism already has an organ for the job, the graft uses that organ rather than growing its own.

Step B1

The written form

The whole site can be spoken, the way the feed speaks — but to two audiences at once.

Export is a projection: it stores nothing, changes nothing, reads everything and speaks it once. The step's real work is the shape of what is spoken, because two audiences will read it. A person, unzipping, finds every page body as an ordinary file named by its own path — pages/notes/first-post.html, a raw stylesheet as pages/demo/style.css — and every image under media/. Another instance of the program finds one manifest.json carrying the full record of every entity, all the step-eight metadata, pointing at the body files by name.

Legibility and losslessness pull apart unless the shape serves both: bodies as files (for the person), records in the manifest (for the machine), and each pointing at the other.

def body_filename(page):
    name = page.path or "_home"
    ext = EXT.get(page.content_type, ".html")
    if name.endswith(ext):      # raw companions carry their own
        ext = ""
    return "pages/" + name + ext

record["body_file"] = body_filename(p)
z.writestr(record["body_file"], p.body)

Every page becomes a file named by its own path.

Step B2

The reading back

Import adds and replaces by name; it never deletes. A live datastore can only gain from it.

The reverse projection: read the manifest, pour each record into a Page, fill its body from the named file, and put. The verb is deliberately the least damaging one available — an upsert by path. Pages the archive knows are added or replaced; pages it does not know are left standing. There is no "wipe and load," because a door that can only add is a door you can open without holding your breath.

The reconstruction is tolerant on purpose: records are filtered through the dataclass's own field list, so an archive and a model that have drifted apart still meet politely. And invalidation is, as always, one call in the one place where writing happens.

known = {f.name for f in fields(store.Page)}
for record in manifest["pages"]:
    body_file = record.pop("body_file", None)
    record = {k: v for k, v in record.items() if k in known}
    page = store.Page(**record)
    if body_file:
        page.body = z.read(body_file).decode("utf-8")
    store.put_page(page)        # add or replace; never delete

Tolerant reconstruction through the model's own field list.

Step B3

A pair of doors

Migration is not a procedure; it is an exhale here and an inhale there.

Two controls join the admin surface, side by side: Download site zip, and an upload field with an Import button that states its covenant before it acts — adds and replaces, never deletes. Moving a site is now: press the first button here, press the second button there.

The verification that closes the snippet: a second, empty instance swallowed the archive and stood up whole — the managed pages, the raw stylesheet with its content type, the media, the feed, and (proving both snippets in one trip) the file mount from snippet A. Importing the same archive twice changed nothing, which is the quiet test that the verb was chosen correctly.

<h2>The site, travelling</h2>
<p><a class="button" href="/admin/export.zip">Download site zip</a></p>
<form method="post" action="/admin/import"
      enctype="multipart/form-data"
      onsubmit="return confirm('Import adds and replaces pages
                by path. It never deletes. Continue?')">
  <label>Upload site zip <input type="file" name="file"></label>
  <button type="submit">Import</button>
</form>

The entire procedure, as the writer sees it.

What it cost, and what it proved

One boundary, stated plainly: mounts travel, files do not. A file page's pointer crosses in the archive, but the filepages/ folder itself travels with the deployment, not the datastore — it was never in the database to begin with, which is snippet A's whole point. Move the folder the way you move the code, because it is code's neighbor.

And the deeper observation: the archive format was designed to unfold. The import reconstructs pages through the dataclass's own field list — fields the archive lacks take their defaults, fields the model has since forgotten are ignored. An archive written today will still pour into the organism several sequences from now. The written form of a growing thing must itself be able to grow.