Pages as Code
File-backed Gutenberg pages for WordPress. Author page content as .html files with front matter and block markup, push to WordPress via WP-CLI.
by Lasse Jellum · github.com/nytafar/pages-as-code · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/nytafar/pages-as-code/archive/refs/heads/main.zipFile-backed Gutenberg pages for WordPress. Author page content as .html files with YAML front matter and native block markup, push to WordPress via WP-CLI.
The idea
WordPress is a great runtime. But authoring pages inside the block editor is a bottleneck when you have a coding agent, a deploy pipeline, or a team that thinks in files.
Pages as Code introduces a deliberate authoring layer: write page content as .html files with front matter, push them into WordPress as real pages. WordPress remains the runtime, the editor, and the source of truth for the live site. The file is the source of truth for the intended content at push time.
This is an explicit, file-first workflow. No hidden sync. No filesystem watchers. No background jobs. You push when you're ready, pull when you need the current state, and WordPress does the rest.
File on disk ──wp pac push──> WordPress page (post_content)
|
Block editor <── humans edit normally
|
wp pac pull <── Revisions <── automatic on update
Why this exists
The block editor stores content as serialized block markup -- `comments inside HTML. That means raw block markup is already a valid file format. Pages as Code takes advantage of this: the same markup that lives inpost_content` can live in a file, version-controlled, generated by an agent, or hand-crafted by a developer.
For AI coding agents, this changes the game. An agent can generate complete, valid WordPress pages without touching the database, the REST API, or the admin UI. It writes a file. A single CLI command makes it real. The agent gets a clear success/failure signal. No browser automation, no API authentication dance, no fragile selectors.
For developers, this is the WordPress equivalent of infrastructure-as-code. Pages live in your repo, deploy through your pipeline, and can be regenerated from data or templates. The block editor remains fully functional for human edits after push.
Quick start
# 1. Install and activate
wp plugin activate pages-as-code
# 2. Create a page file
cat > wp-content/pages/about.html << 'EOF'
---
title: About Us
slug: about
status: publish
---
<!-- wp:heading {"level":1} -->
<h1 class="wp-block-heading">About Us</h1>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>Welcome to our company.</p>
<!-- /wp:paragraph -->
EOF
# 3. Push it
wp pac push about.html --user=1
# Success: Created page "About Us" (ID 42, slug: about).
# 4. Push again (no-op — file unchanged)
wp pac push about.html --user=1
# Success: Page "About Us" unchanged, skipping.
File format
Each .html file contains YAML front matter and a Gutenberg block markup body:
---
title: Rites of Passage
slug: rite
status: publish
parent: company
meta:
seo_title: Our ceremonial cacao ritual
---
<!-- wp:cover {"dimRatio":60,"overlayColor":"dark","minHeight":100,"minHeightUnit":"svh","align":"full"} -->
<div class="wp-block-cover alignfull" style="min-height:100svh">
<div class="wp-block-cover__inner-container">
<!-- wp:heading {"level":1} -->
<h1 class="wp-block-heading">Every seed holds a forest.</h1>
<!-- /wp:heading -->
</div>
</div>
<!-- /wp:cover -->
Front matter fields
| Field | Required | Default | Description |
|---|---|---|---|
title |
yes | -- | Post title. The only required field. |
slug |
no | filename | URL slug. Falls back to filename without .html. |
type |
no | page |
Post type. Must be registered via the pac_post_types filter. |
status |
no | draft |
draft, publish, pending, private, future |
template |
no | default | Page template slug. Page-only; ignored for other post types. |
parent |
no | -- | Slug of parent page. Page-only; parent must exist before push. |
css |
no | auto-resolved | CSS asset path relative to wp-content/. Overrides sibling resolution. |
js |
no | auto-resolved | JS asset path relative to wp-content/. Overrides sibling resolution. |
meta |
no | -- | Key-value map written as post meta. |
File location
The default pages root is wp-content/pages/. A theme or plugin can override it with the pac_pages_root filter — e.g. to wp-content/pac/ so the directory name matches the CLI command.
wp-content/
pages/ # default; configurable via pac_pages_root
about.html
contact.html
landing/
product-a.html
product-b.html
products/ # per-type subdir (from pac_post_types config)
cacao-200g.html # type: product
Subdirectories under the pages root are organizational only — they don't affect WordPress page hierarchy, slugs, or URLs. The post type is declared in front matter (type:), not by directory. Per-type config can give each post type a default landing directory for wp pac pull (see Custom post types).
Sibling CSS/JS assets
Each .html file can have optional matching CSS and JS files that are automatically resolved during push:
wp-content/
pages/
about.html # post content (required)
about.css # per-post styles (optional)
about.js # per-post scripts (optional, only when needed)
Resolution order (CSS example, same for JS):
- Front matter
css:path (relative towp-content/) - Sibling file with same basename:
about.css - Shared directory under the pages root:
<pages-root>/css/about.css
Enqueue behavior:
- CSS loads on the frontend and in the block editor (per-post only — applies to every registered post type)
- JS loads on the frontend only (not in the editor)
- Assets use
filemtimefor cache-busting versioning - If no asset file exists, the corresponding meta field is cleared
Why separate files instead of inline styles/scripts?
- Better caching — browsers cache external files independently
- Cleaner diffs — CSS/JS changes don't pollute HTML diffs
- More WordPress-native — uses standard
wp_enqueue_style/wp_enqueue_script - Editor parity — CSS works in the block editor without parsing `