Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.frankenpress.com/llms.txt

Use this file to discover all available pages before exploring further.

This is the opt-in variant of the designer flow. Same goal — designer customizations ship through git, content stays live — but design state lives as theme files (parts/*.html, templates/*.html, theme.json) in the site repo rather than as DB rows in a snapshot sidecar.

When to use this variant

Stick with DB-row mode

Default. Snapshots are the canonical channel. Sidecar carries wp_template / wp_template_part / wp_global_styles / wp_navigation. Fine for most sites. No tenant changes required.

Switch to theme-files mode

Designer customizations are diff-able HTML in code review. Snapshot shrinks to just navigation + options + attachments. No DB-row taxonomy juggling at apply time.
Theme-files mode trades DB-row authority for filesystem authority. Both modes are supported in parallel. The migration is per-tenant and reversible.

What changes vs the default

Default (DB-row mode)Theme-files mode
wp_template, wp_template_part, wp_global_stylesShip in templates.json sidecarLive in web/app/themes/<child>/ files
Code review diffOpaque JSON blobClean HTML + theme.json diff
Apply-time DB writes for design stateUpsert per row, set wp_theme taxonomyNone — files load from disk on render
Synced patterns (wp_block)DB-resident, capturedStill DB-resident (no file equivalent yet)
wp_navigation, options, attachmentsDB sidecarDB sidecar (unchanged)
Snapshot footprintFull design state in JSONSlim — only the items still in the DB
Designer toolWP Site Editor → save customization → DB rowWP Site Editor → save customization → DB row → “Save to theme” → file write + DB clear

Tenant bootstrap (one-time per site)

1

Bump mu-plugin

Tenant composer.json requires frankenpress/mu-plugin: ^0.13.10 or later. The drift linter at this version accepts site-tracked themes; the reaper correctly handles “captured set is empty → reap all rows of this type”.
2

Add Create Block Theme as a designer dep

site-template v0.7.0+ already adds wpackagist-plugin/create-block-theme to require-dev. On a fresh fork: nothing to do. On an older fork:
composer require --dev wpackagist-plugin/create-block-theme:^2.9
--dev is load-bearing: the plugin is a designer tool and must never reach the production image. The Dockerfile’s composer install --no-dev already excludes it.
3

Create + activate a child theme

On the designer’s local docker-compose stack (lockdown off, plugin active):
docker compose exec site wp --allow-root --path=/app/web/wp eval '
  wp_set_current_user(1);
  $req = new WP_REST_Request("POST", "/create-block-theme/v1/create-child");
  $req->set_body_params([
      "name" => "mysite-design",
      "description" => "Design overlay",
      "uri" => "", "author" => "Mysite", "author_uri" => "",
      "subfolder" => "", "tags" => "",
  ]);
  echo json_encode(rest_do_request($req)->get_data());
'
Creates web/app/themes/mysite-design/ with style.css declaring Template: twentytwentyfive (or whatever the active parent is) and auto-activates the child.
The child theme must be active before any save-user-changes call. With the parent active, the plugin writes INTO the composer-managed parent directory — silently overwritten on the next composer install locally; fails outright on the read-only production filesystem.
4

Commit the child theme tree

Unignore the child theme dir in the site repo’s .gitignore so composer’s web/app/themes/* ignore rule doesn’t swallow it:
web/app/themes/*
!web/app/themes/mysite-design/
Then git add web/app/themes/mysite-design/ && git commit. The Dockerfile’s existing COPY for web/app/ will bake the child theme into every image build.
5

Bump the chart's active-theme value

In gitops-fp (or wherever the chart values live for the tenant):
siteInstall:
  activeTheme: mysite-design
Before: twentytwentyfive (composer-installed parent). After: the child theme. The install Job’s wp theme activate {{ .activeTheme }} runs on every release, so new pods on a fresh DB activate the correct theme.

Designer workflow per release

1

Edit in Site Editor

Designer’s local docker-compose stack, lockdown off, child theme active. Edit parts/header.html via Site Editor like normal. WordPress creates wp_template_part DB rows attached to the active child theme as customizations land.
2

Save to theme files

Once the designer is happy, invoke POST /create-block-theme/v1/save:
docker compose exec site wp --allow-root --path=/app/web/wp eval '
  wp_set_current_user(1);
  $req = new WP_REST_Request("POST", "/create-block-theme/v1/save");
  $req->set_body_params([
      "saveTemplates"             => true,
      "processOnlySavedTemplates" => false,
      "saveStyle"                 => true,
      "saveFonts"                 => false,
      "savePatterns"              => false,
  ]);
  echo json_encode(rest_do_request($req)->get_data());
'
Writes web/app/themes/mysite-design/parts/header.html, web/app/themes/mysite-design/theme.json, etc. Then clears the corresponding DB rows.
3

Snapshot + commit

fp snapshot --slug=launch --note="Designer iteration N"
The snapshot’s templates.json is now slim — no wp_template / wp_template_part / wp_global_styles entries because the DB rows were cleared. The reaper picks up the “captured set is empty” signal for those types and trashes any orphan rows on apply (e.g. left over from earlier DB-row-mode releases).Commit both:
  • The theme files at web/app/themes/mysite-design/
  • The slim snapshot at web/imports/launch/
4

Tag + ship

git tag vX.Y.Z && git push origin vX.Y.Z
CI builds the image with the theme files baked in. ArgoCD (or Kargo, when present) reconciles. The install Job re-runs wp fp apply against the slim snapshot.On render, the FSE renderer resolves the customized header from the file (source=theme, wp_id=null), not a DB row.

What wp fp apply does in theme-files mode

The apply path is identical to the default — the slim snapshot just gives it less to do:
  1. Stage 1 — WXR import: no-op (no additive post types).
  2. Stage 2 — Attachments: ships the design-asset binaries that templates reference, same as before.
  3. Stage 3 — Owned posts: upserts wp_navigation (and any wp_block synced patterns that haven’t been migrated). No wp_template_part / wp_template / wp_global_styles work because the snapshot doesn’t carry any.
  4. Stage 3b — Reaper: trashes orphan rows for owned types whose captured set is empty. This is what cleans up the previous-release DB-row design state on the first theme-files-mode apply.
  5. Stage 4 — Options: same as before (site-identity options, theme_mods, page_on_front slug remap).
  6. Stage 5 — URL retarget: same as before.

Tradeoffs to know about

When the plugin writes parts/*.html, image blocks that had "id":33 and class="wp-image-33" in their captured form get stripped down to URL-only:
<!-- BEFORE (DB row) -->
<!-- wp:image {"id":33,"sizeSlug":"full"} -->
<figure><img src="..." class="wp-image-33"/></figure>

<!-- AFTER (theme file) -->
<!-- wp:image {"sizeSlug":"full"} -->
<figure><img src="..."/></figure>
The URL still gets search-replaced on apply (so cross-environment URL rewriting still works), but the bidirectional WP-attachment-post linkage is gone. Blocks that read the attachment ID at render time (lightboxes, view-original-size buttons) won’t find one to read.Acceptable for most sites. Consider sticking with DB-row mode if you have heavy image-interactive UX.
Create Block Theme’s savePatterns flag can write synced patterns into the theme, but FrankenPress doesn’t recommend it yet (not empirically validated end-to-end). For now, leave synced patterns as DB-resident — they’re captured by the snapshot’s templates.json and applied via the standard upsert path.
Common gotcha: if the designer accidentally activates the composer-managed parent (not the child) and runs save-user-changes, the plugin writes into the parent dir. On the next composer install locally, those files get clobbered.Recovery: re-activate the child, re-run save-user-changes. The plugin re-reads from DB rows; if those are gone, restore from a recent snapshot.
wp fp snapshot runs a drift check at capture time:
  • Plugins active but not composer-installed AND not site-tracked (in web/app/plugins/<slug>/) → drift error.
  • Theme active but not composer-installed AND not site-tracked (in web/app/themes/<slug>/) → drift error.
Theme-files mode relies on the site-tracked branch — the child theme lives in web/app/themes/mysite-design/ but isn’t composer-installed. mu-plugin v0.13.10+ recognises that as valid. Older mu-plugin versions will trip the linter; bump first.
Per-tenant migration is reversible:
  1. Delete web/app/themes/mysite-design/parts/*.html from the site repo (keep style.css + theme.json if you want).
  2. Revert siteInstall.activeTheme to the composer-managed parent (twentytwentyfive or whichever).
  3. Re-capture customizations as DB rows: open Site Editor on local, recreate the customization in admin, fp snapshot.
Apply on the target then re-establishes the DB-row design state via the normal upsert path. The reaper trashes orphan wp_template_part rows under the child theme (since it’s no longer active).

Per-repo versions

RepoMinimum version
mu-pluginv0.13.10 (drift linter site-tracked + reaper empty-set semantics)
site-templatev0.7.0 (Create Block Theme require-dev)
chartsv0.11.0+ (any post-v0.11.0; chart logic unchanged for theme-files mode)
Create Block Theme pluginv2.9.0+

Companion docs