Skip to content

Menus

The menu command group lets you version-control WordPress nav menus (Appearance > Menus) as plain JSON files in Git, one file per menu, plus the active theme’s menu locations.

A menu item’s post_type/taxonomy target is never synced by its raw WordPress id, ids are not the same on two environments. Instead each item is resolved by identity: a post type or taxonomy slug (object) plus the target’s own slug (objectSlug). menu push re-resolves that identity on the target environment every time; if the target doesn’t exist there, the push fails clearly instead of guessing or silently dropping the item.

Menu locations (which menu is assigned to “Primary Navigation”, “Footer”, etc.) are a per-theme setting in WordPress, not a site-wide one. menu pull/menu push read and write them for whichever theme is active at the time.

Terminal window
# 1. Download existing menus and locations from WordPress
lps menu pull
# 2. Edit locally, commit to Git
git add menus/ && git commit -m "feat: add a footer menu"
# 3. Deploy back to WordPress
lps menu push

Download every nav menu and the active theme’s menu locations from WordPress and write them as .json files.

Terminal window
lps menu pull [path]
Argument Default Description
path ./menus (or loopress.json’s menuDir) Local directory where menus are written
Flag Description
--dry-run / -d Show what would be written without touching the filesystem

Local menu files no longer present on WordPress are removed on pull, so the directory always mirrors the site. In a terminal the files are listed and a confirmation is asked first (--yes skips it); in scripts and CI they are removed and reported. menu-locations.json is a single file, not a list, so it’s always overwritten in place.

Example:

Terminal window
lps menu pull --dry-run

Upload local .json files to WordPress.

Terminal window
lps menu push [path]

Each menu is matched by its own slug, which is its permanent identity, like ACF’s key. Pushing a menu replaces every one of its items on the target with a freshly built tree from the file: there’s no stable identity for a menu item itself to diff against, so a menu syncs as one atomic unit. Every post_type/taxonomy item is re-resolved by object/objectSlug on the target environment first; if any item can’t be resolved there, the whole push for that menu fails before anything is written, rather than partially rebuilding it.

A custom item whose URL is absolute and points at a different domain than the target environment produces a warning, not a failure, menus can legitimately link off-site. It’s a nudge to double check before syncing the same menu to another environment.

Argument Default Description
path ./menus (or loopress.json’s menuDir) Local directory to read .json files from
Flag Description
--dry-run / -d Show what would be pushed without making any changes

Example:

Terminal window
lps menu push ./menus

Print every nav menu and the active theme’s menu locations currently on WordPress.

Terminal window
lps menu list
Flag Description
--json Output raw JSON instead of formatted text

Example output:

main (Main Menu): 4 item(s)
footer (Footer Menu): 2 item(s)
locations:
primary: main
footer: footer
social: (unassigned)
menus/
main.json
footer.json
menu-locations.json

Each menu file holds { "slug", "name", "items", "warnings" }. items is a tree (children nested under their parent, not a flat list with parent ids) for a readable git diff:

{
"slug": "main",
"name": "Main Menu",
"items": [
{
"type": "post_type",
"object": "page",
"objectSlug": "about",
"title": "",
"url": null,
"classes": [],
"target": "",
"xfn": "",
"description": "",
"children": [
{
"type": "custom",
"object": null,
"objectSlug": null,
"title": "External docs",
"url": "https://docs.example.com",
"classes": [],
"target": "_blank",
"xfn": "",
"description": "",
"children": []
}
]
}
],
"warnings": []
}

warnings is diagnostic only, a dangling item whose target was deleted, an item type Loopress doesn’t sync (only post_type, taxonomy, and custom are supported), or an off-environment custom URL. It’s never part of the tracked configuration and is ignored by lps menu diff.

menu-locations.json holds { "<location>": "<menu slug>" | null } for every location the active theme registers:

{
"primary": "main",
"footer": "footer",
"social": null
}