Skip to content

cwp content

shipped 1.0.0
cwp content [flags]

cwp content groups the commands below and takes no action of its own; run it alone and it prints its help.

FlagWhat it doesDefault
--with-agentwith —dry-run on a host with no shell: install and remove the PHP agent so the plan is realoff
SubcommandWhat it does
cwp content listList the posts, terms and menus in scope on a site, and whether cwp manages them
cwp content openOpen one page in a browser: the page a status row names
cwp content statusCompare the site against the committed tree, in both directions, without writing
cwp content pullRead named posts down into the committed tree
cwp content pushWrite named posts from the committed tree to an environment

What it does

Moves individual pages and posts between an environment and a committed tree of JSON. This is the command for the work that is most of the work: building a page and getting it live.

pull and push name the direction relative to the tree, not to your laptop. cwp content pull dev brings dev’s pages into content/; cwp content push dev sends them there. Omitting the environment means the local site.

cwp content pull --post about-us       # from the local site into content/
git add content/ && git commit -m "…"
cwp content status dev                 # what differs, both directions, no writes
cwp content open about-us dev          # look at the page a row named
cwp content push dev --post about-us

This is not a hole in the safety model

The database flows downward. That rule protects against a write of unbounded extent: one where nobody can say beforehand what will be different afterwards.

A content push is a named list of posts, printed before anything happens. cwp has shipped a bounded upward database write since 0.4.0. cwp bricks push replaces global classes and templates on a protected environment under the full guard set, and nobody calls that a breach. The only structural difference to a page is the table the row sits in. So:

Bulk database state flows downward. Individually named items may flow upward, under the full guard set, with the affected items listed before the write.

The artifact

content/<type>/<slug>.yml, one file per post, committed. This is half the point of the feature. A Bricks page is database state. Without this file it has no diff, no review and no history: you build one and git status stays empty.

YAML, and bricks/ is not. A content item is mostly strings, and some of them are long. A page’s custom CSS is a stylesheet. On a site with no page builder the whole page is one post_content blob. JSON writes those on a single line with \n escapes. Changing one CSS value then replaces a 200-character line with a 200-character line, and you diff two strings by eye. A YAML block scalar puts the changed line on a line of its own. The design-system tree under bricks/ stays JSON because it is the builder’s own export package: interchange, not a document.

If your tree still holds .json files, cwp refuses to read them and tells you to re-pull once. That re-pull regenerates the files instead of converting them, and it renames every file in the tree. Give it a commit of its own, or a real content change disappears in it.

The file is environment-neutral. It holds the site URL as {{site}} and every attachment id as its upload path. The same page pulled from two environments therefore produces the same bytes. Without that, every push would look like a conflict.

It is canonical: sorted keys throughout, two-space indent, trailing newline. A pull that changes nothing writes nothing. Re-running one does not dirty the tree or move an mtime.

Identity is _cwp_uid, a UUID in post meta. A renamed slug moves the file and keeps the item. cwp adopts a post that has none. It names the post in the output and asks for confirmation. An adoption that guesses wrong marries two unrelated pages.

media describes each attachment by path, with a note field that is yours. cwp reads it, carries it through a round trip and never writes it. The committed file is the only place a remark about an image survives a cwp pull. That command replaces the local database wholesale.

Configuration

Absent means the project does not use the transfer, and cwp content says so. One group inline is the common case:

content:
  dir: content/
  post_types: [page, post]
  meta:
    include: ["_bricks_*", "_thumbnail_id", "_wp_page_template"]
    exclude: ["_edit_lock", "_edit_last"]

…or a map of named groups. A custom post type wants that: its own directory and usually its own meta rules:

content:
  pages:
    post_types: [page]
  event:
    dir: events/
    post_types: [acme_event]
    meta:
      include: ["_bricks_*", "_event_*"]

A group’s dir defaults to its own name. cwp tells the two shapes apart by whether dir/post_types/meta sit at the top. Groups live under content: and not at the top level of cwp.yml. The top level is a reserved namespace: a post type called bricks or pull would collide with a real key.

The meta allowlist is short, and every export names what it skipped, once, with the key to add. Losing a site’s SEO fields silently on the first push is the failure class this project does not ship. Bricks’ own page content and settings never travel as raw meta. They go through Bricks’ abilities. Those normalise them and take the revision.

Bricks templates are content

cwp init scaffolds a second group on a Bricks project:

content:
  content:
    dir: content
    post_types: [page]
  templates:
    dir: templates
    post_types: [bricks_template]
    meta:
      include: ["_bricks_*"]

A Bricks template is a post with an element tree. It keeps its element ids only on this path. In the design-system package it does not: Bricks’ importer regenerates every id each time it upserts a template. Those ids are not bookkeeping. Bricks stores a custom CSS rule written as %root% { … } as #brxe-pcvsjv { … }, so an id is a name other things point at. Pushed as content, the tree goes through bricks/set-page-elements. That ability preserves the ids and takes a revision.

The design system stops carrying templates the moment a group claims them. cwp reads that off this config, not off a flag, so there is no migration day. The next cwp pull --only bricks reports the old bricks/structure/templates/*.json as left behind. You accept the move with git rm. A deletion out of a tracked directory is a commit, not a side effect of a read.

Taxonomy terms

A post carries its terms, by slug, per taxonomy:

"terms": { "category": ["news"], "post_tag": ["workshop"] }

Absent means the post has none. Terms are part of the content hash. Moving a post between categories is therefore a change like any other. A push over someone else’s edit reports the conflict it is.

cwp assigns terms the target already has and creates none. A term that exists on one environment and not the other is a real difference between them. Inventing it would hide that behind a push reporting success. The push writes the post either way, without that term, and the run names what was missing.

A group that declares menus: gets one file per menu: the nav_menu term, its entries nested in order, and its theme locations. See pull for the format and push for what a push does with it.

The one thing worth knowing here: an entry carries its target as an identity, never as the id WordPress stores. The identity is a post type and slug, a taxonomy and slug, or a uid when the target has one. A raw _menu_item_object_id is a different page on the next environment. A menu that arrives pointing at the wrong page is worse than a menu that does not arrive.

What it does not do

It deletes no post on the target and has no --prune. Files are replaceable; content is not. A menu entry the tree stopped describing is the one thing cwp removes outright. The entry is a pointer, and the page it pointed at stays as it was.

It does not merge two versions of a page. It does not transfer post types no group claims. Editorial content that lives and changes on production should keep doing that. This command serves the page you build, a design artifact stored as a post.