cwp content pull
shipped 1.0.0cwp content pull [env] [flags]
This name is an alias. The work moved to cwp pull --only content, and that page documents it. This spelling still runs for one minor cycle.
Acts on the local site. Name an environment to act there instead.
| Argument | What it is | Default |
|---|---|---|
[env] | environment to read (default: default_environment in cwp.yml) |
| Flag | What it does | Default |
|---|---|---|
--post <selector...> | slug, post id or URL, repeatable | |
--type <post_type> | restrict to one post type | |
--all | every post in scope | off |
--force | override the protected-environment refusal | off |
--no-backup | skip the remote backup taken before the write | on |
--yes | skip the confirmation prompt | off |
--with-agent | with —dry-run on a host with no shell: install and remove the PHP agent so the plan is real | off |
Plus the shared flags --json, -v, --verbose, -q, --quiet and --dry-run.
What it does
Brings named posts from a site into the committed tree.
cwp refuses a bare cwp content pull and does not read it as --all. The
command is one word from cwp pull, and that one replaces your
whole local database. A command that guessed here would guess about the wrong
one.
Selection is explicit: --post <slug|id|url> repeatable, --type <post_type>,
or --all.
cwp resolves a selector against every configured group, not against one of
them. A Bricks project scaffolded by cwp init has two: content
for pages and templates for bricks_template. Naming a page there works
without saying which group it is in. cwp refuses a slug that two groups both
hold rather than guessing at it. --type says which one you meant.
A pull writes to the site it reads
Two things go up. Against a remote that means the full guard set: the
confirmation, the restore point, --force on a protected environment.
- the uid of anything adopted, so the item keeps its identity across a rename;
_cwp_sync, the hash of what cwp took. The next push compares against it.
A local pull writes to no remote and asks nothing. This is the one place in cwp
where a command named pull has an upward effect. The command declares it as
one rather than excusing it.
Example
cwp content pull --post about-us # from the local site into content/
cwp content pull dev --type page
cwp content pull dev --all
Terms travel too, when a group declares them
A group that names taxonomies gets a file per term:
content:
post_types: [page]
taxonomies: [category, post_tag]
content/terms/post_tag/workshop.yml
Declared, never discovered. This is the load-bearing decision, not a
preference. A live project’s fourteen taxonomies include three that store
visitor IP addresses and user agents as terms. pull.truncate_taxonomies exists
for them. A feature that discovered taxonomies would commit personal data to a
git repository. Absent means none. A project that says nothing behaves exactly
as it did.
A uid identifies a term, not its slug. If the slug were the identity,
renaming veranstaltungen to events would look the same as deleting one term
and creating another. A consolidation is mostly renames. The slug is the file
name; the uid inside the file is the term.
The tree never records the count. The posts derive it, and writing it would dirty every term file whenever a post gained or lost a tag.
The pull names and skips a declared taxonomy the source does not register. A plugin switched off on one environment is a real difference between two sites.
Menus travel as menus, not as posts
A group that says menus: gets a file per navigation:
content:
post_types: [page]
menus: true # or: [primary, footer]
content/menus/primary.yml
One file per menu, not one per entry. A menu entry has no slug to name a
file after: WordPress leaves post_name empty or numeric. And a menu is the
unit anybody reviews. Reordering two entries is one change to one file, and the
nesting is structure in it:
{
"uid": "8f1c…",
"slug": "primary",
"name": "Hauptmenü",
"locations": ["primary"],
"items": [
{
"uid": "3a20…",
"label": "Über uns",
"target": { "kind": "post", "type": "page", "slug": "ueber-uns", "uid": "7c1e…" },
"children": [
{ "uid": "b904…", "label": "Kontakt",
"target": { "kind": "custom", "url": "{{site}}/kontakt" } }
]
}
]
}
The target makes an entry portable. WordPress stores it as a raw post id.
That id is a different page on the next environment. The file carries an
identity instead: a post type and slug, a taxonomy and slug, a post type’s
archive, or a custom URL. The custom URL goes through the same {{site}}
placeholder a page payload uses. The uid sits beside the slug when the target has one. A push resolves
by identity first and by slug second: the uid survives a rename, the slug
survives a target no group manages.
An entry whose page was deleted on the source is already broken there. The pull names it and leaves it out rather than committing a reference to nothing:
! primary: the entry "Termine" points at something dev no longer has — it was
left out of the file rather than carried as a reference to nothing
The element that displays a menu is portable too. A navigation element stores which menu it shows as that menu’s term id. The file carries the slug instead, the same substitution one level in:
settings:
menu:
kind: menu
slug: primary
This happens whatever the selection, so a page pulled on its own commits the
same bytes as the same page pulled with --all. A menu that no longer exists on
the source keeps its number: there is no slug to write, and inventing one would
commit a reference to nothing.
Menus travel with --all; a run naming individual posts says so and skips them.
The pull reports a menu named under menus: that the source does not have the
way it reports an unregistered taxonomy.
cwp refuses two configurations when it reads cwp.yml. Both would manage one
thing twice: nav_menu under taxonomies: beside menus:, and
nav_menu_item under post_types:.
A narrowed run carries posts, and only posts
--post and --type name posts. Everything that belongs to the group as a
whole travels with --all, and a narrowed run says which:
· menus menus travel with --all
· terms terms travel with --all
· content/authors.yml the people travel with --all
The reason is the same one that stops a narrowed run from deleting. A run that
knows about five posts cannot tell a term somebody removed from the tree on
purpose from one nobody ever pulled. content/authors.yml describes the whole
tree. Rewriting it from one environment’s accounts would flip it between the
scrub’s placeholders and real names according to the side of the last pull.
Two posts cannot share one file
The tree names a file after the post type and the slug:
content/page/kontakt.yml. WordPress keeps a slug unique among siblings,
not site-wide. A top-level kontakt and a child of hilfe called kontakt are
both legal, and both want that file.
The pull refuses and names them:
! content/page/kontakt.yml — page/kontakt (#30) and page/kontakt (#31)
✗ 1 post(s) would be written into a file another post already has
→ WordPress keeps a slug unique among siblings, and the tree names a file
after the type and the slug — rename one of the slugs on the site and pull
again
It refuses the same shape against the tree: a file that already describes a different post, recognised by the uid inside it.
A parent the tree does not manage
A child post carries its parent’s identity, not its id. Where the parent is in no managed group there is no identity to carry. The item then records no parent and the run says so:
! page/kind: its parent (#30) is in no managed group, so the item carries no
parent and a push would put it at the top level
The site’s own URL leaves the file
A link an editor writes is an absolute URL, because the browser hands them one.
The pull replaces this environment’s own address with {{site}} in
post_content and post_excerpt. The push writes the target’s address back:
-<a href="https://project.ddev.site/kontakt/">write to us</a>
+<a href="{{site}}/kontakt/">write to us</a>
Without the placeholder a page pushed to another environment would serve a link back to the machine of its author. The same page read from two environments would commit different bytes.
The same holds for a term description, the introduction above its archive, and for a widget’s text. Those are the other two places where prose a person wrote travels in the tree.
The pull names a link to a different environment and does not rewrite it. cwp cannot know whether you meant that other site, so it says so and leaves the address alone:
! page/ueber-uns: links to another environment in its own text —
https://old.example.com — that address travels unchanged and points there
from every environment
What the tree holds and the source does not
A pull that covers a whole group names the items the source no longer has:
! 1 item(s) in the tree are not on dev (page/alte-aktion) — they were either
deleted there or never pushed there, and cwp cannot tell which. Nothing was
removed: delete them yourself if they are gone for good
Reported, never removed, exactly as the design-system read reports a stale package file. Deleting out of a git-tracked directory is not a side effect of a read. In a repository a deletion belongs in a commit.
The pull cannot say more than that. The evidence that would tell a deletion
from a page nobody ever pushed is _cwp_sync, the marker cwp leaves on the
post. That marker lived on the post that is gone.
It ends by pointing at the diff
A pull that wrote something closes with the step people skip:
what to do next:
3 file(s) changed in the tree — the diff is the review
→ git diff -- content
→ commit them: a page built in a builder has no history anywhere else
Those two lines hold the whole argument for the content tree. A page built in a page builder is database state. Without these files there is nothing to read, nothing to review and nothing to revert to. A run that rewrote nothing prints no block, because there is nothing to look at.
Menus are read from the database
Not through wp_get_nav_menu_items(). That call is WordPress’s display path
and applies a filter. A plugin that shows account links only to logged-out
visitors hides entries from that call. A membership, translation or
role-visibility plugin does the same. The pull would then commit a menu with
those entries missing. The next push would delete them, because a menu file
describes the whole menu.
The rule generalises, and so does the fix. cwp is a storage transport, and
anything WordPress renders through a filter has two answers. The shim stands
those filters down before it reads: menus, terms, post meta, the options a
settings: group carries, the roles table. What lands in the tree is what the
database holds.
A key it asked for and cannot carry stops the run
A committed item holds one value per meta key. WordPress does not: a key can carry several rows, and a duplicated post carries a duplicate of every row it had.
Where the rows say the same thing, cwp collapses them and says nothing. Two rows
holding 9 are the value 9, and nothing is lost. Where they say different
things and the key is one your meta.include names, the run stops before it
writes a single file:
! example_thing/some-slug: _thumbnail_id has 2 values on staging, and a committed item carries one
✖ 1 meta key(s) the tree asks for cannot travel — nothing was written
→ decide which value stands — `cwp wp --env staging -- post meta list 1234 --keys=_thumbnail_id`
— or name the key under content.things.meta.exclude
It stops rather than writing what it could. An item written without a key
it declares is an item the next push writes back without it, and cwp push
deletes what the tree does not carry. The tree stays exactly as it was. The
choice is yours: repair the rows on the source, or say in cwp.yml that the
key does not travel.
A key your rules never asked for is a different matter and stops nothing. The run reports it once for the group, with the number of items that carry it:
! things: 2 meta key(s) not carried on 98 item(s) — _wp_old_date, _wp_old_slug
— add a pattern under content.things.meta.include to keep them
A key that only some of the items carry gets its own count, _legacy (3). One
line then answers the question a hundred lines used to.
What it does not do
It does not delete files in the tree that the site no longer has. A pull that changes nothing writes nothing, so re-running one does not dirty the tree or move an mtime.
It does not download media. The committed file describes each attachment by path;
the files themselves travel with cwp pull or the uploads proxy.