cwp content
shipped 1.0.0cwp content [flags]
cwp content groups the commands below and takes no action of its own; run it alone and it prints its help.
| Flag | What it does | Default |
|---|---|---|
--with-agent | with —dry-run on a host with no shell: install and remove the PHP agent so the plan is real | off |
| Subcommand | What it does |
|---|---|
cwp content list | List the posts, terms and menus in scope on a site, and whether cwp manages them |
cwp content open | Open one page in a browser: the page a status row names |
cwp content status | Compare the site against the committed tree, in both directions, without writing |
cwp content pull | Read named posts down into the committed tree |
cwp content push | Write 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.
Navigation menus
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.