# Liberate Local

> Liberate a local static site

- Skill: `automattic/liberate-local` (Agent Skill)
- Install (CLI): `npx skillmds@latest add automattic/liberate-local`
- Raw SKILL.md: https://api.skillmd.com/api/skills/automattic/liberate-local/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: Automattic (https://skillmd.com/u/automattic)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/automattic/liberate-local

---


# Liberate a local static site

> **Dispatched by `/liberate`.** This is the local-directory branch of the `/liberate` front door, not a standalone entry point — it is `user-invocable: false` (hidden from the user's autocomplete) and `disable-model-invocation: true` (the Skill tool can't invoke it). Instead, `/liberate <dir>` reads & follows this SKILL.md inline (see `skills/liberate/SKILL.md` → "Step R — Route on input type"). The steps below run in that shared context.

The **owned local-source** path: a directory of HTML/CSS/JS → a live WordPress Studio site running a native block theme that carries the source's own design. Unlike the URL path of `/liberate` (which detects a remote platform and extracts over the network), this path has the source on disk already, so it skips detect/discover/extract entirely and goes straight to: **provision the Studio site → process the source → build the theme + pages → capture → parity compare → deterministic repair.**

`liberate_convert_local_site` is the one command that drives all of it (with `createSite: true` it provisions the Studio target too). This skill is the thin front door that resolves inputs, runs that command, and reports.

---

## Pipeline overview

```
/liberate-local <source-dir>
│
├─ resolve inputs: source dir · site name · theme slug · Studio site path · output dir
│
├─ if JS-data OR static content cards: start these together, then wait for both:
│     ├─ background provision: studio site create --path <studioSitePath> --start --skip-browser --skip-log-details
│     └─ model-local-data subagent → <outputDir>/data-model.json
│
└─ liberate_convert_local_site({ dir, studioSitePath, createSite:true, … })  ── one command, drives everything:
      1 createSite      studio site create (fresh WP+SQLite, started)  — skipped if the site already exists or was already provisioned (idempotent)
      2 ingest          source HTML → native block sidecars (roundtrip-validated) + per-instance lib-i styles
      3 block-fixer     canonicalize each page through @wordpress/blocks (editor-valid markup)
      4 design capture  serve source over HTTP → palette/typography/breakpoints + self-host Google fonts
      5 theme assemble  block theme: nav from the graph, footer, no-title page templates, carried source.css/js + instance-styles.css (+ add_editor_style)
      6 install         write + activate the theme; create WP Pages from sidecars (idempotent via _source_url); front page; page template
      7 compare         capture the WP replica (desktop+mobile) and score parity vs the source
      8 repair          deterministic parity loop: diff → computed-style probe → parity-patch.css → re-compare (bounded, no AI)
```

There is no path checkpoint (unlike `/liberate`): the owned-local path is always carry-to-block-theme — the source IS the design authority. The only decision is the optional editor-fidelity surface (below).

---

## Step-by-step

### Step 0 — Resolve inputs

Ask for the **source directory** if not given. Then derive (let the operator override any):

- `dir` — the absolute source directory (HTML/CSS/JS).
- `themeSlug` — kebab-case, e.g. `basename(dir)` → `maison-clouet`.
- `siteTitle` — the home page `<title>`, or a name the operator gives.
- `studioSitePath` — `~/Studio/<slug>` for a fresh site (must NOT collide with an existing unrelated site unless you intend to reuse it). Confirm the chosen path with the operator before creating.
- `outputDir` — liberation artifacts (sidecars, reports, screenshots). Default `~/Studio/_liberations/<slug>` or a run-local dir.

### Step 0b — WordPress-driven data (conditional)

If the source renders its main content from **JavaScript data** (an empty `<div id="…">` filled at runtime by a mount call over an in-file array, e.g. a catalog/listing/gallery) or from repeated **static-HTML content cards** (post-preview cards on index/archive pages), those grids would be **empty or non-native in the block editor** on a straight carry. In that case, first produce the data model:

- Dispatch the **`model-local-data`** skill via a subagent (so the nested skill runs to completion without interrupting this workflow), pointing it at the source dir + output dir. It runs the scaffold to decide the source: `js-array`, `html-cards`, or `none`. For JS arrays it reads the source JS; for static content cards the scaffold's records come from `discoverHtmlCards`. It writes `<outputDir>/data-model.json` (posts/taxonomy + native query loops + a faithful card render).
- At the same time, start Studio provisioning in the background:

  ```
  studio site create --path <studioSitePath> --start --skip-browser --skip-log-details
  ```

  Start this right after resolving inputs and dispatching the data-model fill. Wait for BOTH the background provisioning command and the `model-local-data` subagent to finish before Step 1.
- When that file exists, the convert step below **auto-activates** the data path: it inserts the items, registers any needed custom type/taxonomy, replaces the mounts/card containers with `core/query` loops (editable/analyzable in the editor), and neutralizes the JS data-mounts or static card container + rebinds any modal to per-card DOM islands — while keeping styling/animation/filter JS.
- Step 1 still calls `liberate_convert_local_site` with `createSite: true`. The convert handler is idempotent, so it reuses the already-provisioned site and skips re-provisioning; behavior is unchanged, only the orchestration order and wall-clock time change.

Run the scaffold to decide. If `discovered.source` is `none`, fall through to the pure carry path (today's behavior): do not use a data model, pass `dataModel: false` to the convert call to force the data path off even if a model file is present, and let Step 1 provision or reuse the site idempotently.

### Step 1 — Convert (one command)

Call `liberate_convert_local_site` with `createSite: true`:

```
liberate_convert_local_site({
  dir:            "<source-dir>",
  studioSitePath: "~/Studio/<slug>",
  outputDir:      "<output-dir>",
  themeSlug:      "<slug>",
  siteTitle:      "<title>",
  createSite:     true,     // provision the Studio site if it doesn't exist (idempotent)
})
```

- **createSite** runs `studio site create` only when no WP install exists at `studioSitePath` — a re-run reuses the site (and re-converts idempotently). Admin creds come from env `WP_ADMIN_USER` / `WP_ADMIN_PASS`; omit them and Studio auto-generates (fine for convert-only).
- **carryCss / carryJs** default ON — the source's own stylesheet + scripts are carried into the theme (the parity mechanism). Pass `false` for a tokens-only theme.
- **nativeBehaviors** (opt-in) swaps carried JS for native Interactivity blocks (reveal/sticky/tabs/slider/modal); unmapped behaviors land in `behavior-gaps.json`. Default OFF (maintain the source JS).
- **editableIslands** (opt-in) converts carried `core/html` islands into editable `dla/editable-html` blocks (text+image bindable, static-save, render-anywhere). Default OFF.
- **editorSurface** (opt-in) also scores each page in the live block editor canvas — needs `WP_ADMIN_PASS` in env; warn-only for carry (it does not flip the verdict).

Narrate progress. On `isError`, surface the message (a missing source dir, a Studio create failure, a roundtrip/compose failure per page) and stop.

### Step 2 — Report

From the handler summary + `parity-report.json` in the output dir, report:

- the Studio site (created vs reused) + its live URL,
- pages composed / failed / empty + low-confidence count,
- **parity**: per-page desktop/mobile scores, the averages, `allPass`, and repair rounds/converged,
- any warnings (carried-asset notes, sticky-not-carried, behavior gaps).

Honest visual assessment: itemize real differences; do not oversell a match (see the match-section skill's anti-gaming rules if doing a polish pass).

---

## Idempotency & resume

Re-running `/liberate-local <dir>` on the same `studioSitePath`:

- **skips** site creation (the site exists),
- re-ingests + re-converts deterministically,
- re-installs pages by `_source_url` (no duplicates),
- re-runs the parity loop.

To start completely fresh, point at a new `studioSitePath` (or delete the old site via `studio site delete`).

---

## Notes

- This path needs **Studio installed** and the `studio` CLI on PATH (`studio site create` provisions WP + SQLite and starts the site on a Studio-assigned port; the convert auto-resolves the live URL via `wp option get siteurl`).
- The source is trusted (owned). There is no platform adapter, no network extraction, no products path — it's structure + design carry.
- The theme is a real block theme: pages are block-editable, the carried CSS is the design authority, and per-instance source styles ride fixer-safe `lib-i` classes (so editor saves don't drop them). Carried CSS is also loaded into the editor canvas (`add_editor_style`).
- Measured timing: a fresh convert with provisioning took about 3m36s, while reusing an already-provisioned site took about 2m. On the JS-data path, overlapping the background `studio site create` with the model fill hides roughly 1.5m of fixed provisioning cost.
- Deeper overlap of ingest or design capture would require splitting the convert handler. Keep this workflow to the provisioning overlap unless that handler split exists.

