WP Manifestindependent plugin directory
manifest / developer / pages-as-code

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

2stars
0forks

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

File-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):

  1. Front matter css: path (relative to wp-content/)
  2. Sibling file with same basename: about.css
  3. 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 filemtime for 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 `