Menus
Menus are the navigation structures that appear in your site's header and footer. Open Menus in the sidebar to manage them.
The CMS supports two named menus out of the box: header and footer. Both share the same shape and operations — you build a list of items, save, and the public site updates instantly.
Dynamic, not baked
Unlike WordPress (which bakes menus into every page's HTML on the next page render), Flexweg CMS uses dynamic menus:
- When you save the menu, the admin uploads a single
/menu.jsonfile to your Flexweg site root. - Every published page's
<head>references atheme-assets/<theme-id>-menu.jsruntime script. - On page load, the script:
- Fetches
/menu.json - Finds every
[data-cms-menu="header"]and[data-cms-menu="footer"]container in the DOM - Fills them with the resolved menu HTML
- Sets
aria-current="page"on the link matching the current URL
- Fetches
So changing menu items doesn't require re-publishing every page. The change is live instantly on every page next time anyone loads it.
Building a menu
The Menus page has two columns side-by-side:
- Header menu — left
- Footer menu — right
Each column shows the current menu as an ordered list of items. Drag rows to reorder. Click an item to expand its detail editor.
Adding an item
Click Add item at the bottom of the header or footer column. A modal opens with a type picker:
- Post — link to a specific post (resolves the post's URL at publish time)
- Page — link to a specific page
- Category — link to a category archive
- Tag — link to a tag (note: tags don't have archive URLs in v1, so this fails to resolve currently)
- Custom URL — paste any URL (relative or absolute)
- Archive — link to the
flexweg-archivesplugin's archive index (only when the plugin is enabled) - RSS feed — link to a
flexweg-rssfeed (only when the plugin is enabled and emitting feeds)
Once you pick the type, the modal shows specific fields:
- For Post / Page / Category / Tag: a searchable picker
- For Custom URL: URL field + label field + target
_self/_blanktoggle - For Archive / RSS feed: pre-filled, just confirm
Click Add — the item appears in the menu list.
Editing an item
Click an existing item → expands inline. Edit the label, target, or pick a different post / page / category. Save closes the editor.
Removing an item
Click the 🗑 icon on an item. Confirms (no, it doesn't — it just deletes immediately). Item gone.
Reordering
Drag the ⠿ handle on the left of an item to reorder within the menu. Items can move freely within their menu (header ↔ header, footer ↔ footer); cross-menu drag isn't supported.
Hierarchy
Menus support one level of nesting. Drag an item's row slightly to the right to nest it under the previous item; drag back left to unnest. Two-level deep nesting isn't supported in v1.
Themes typically render top-level items as horizontal nav, second-level items as a dropdown / disclosure menu. Some themes flatten the hierarchy (default theme); others render dropdowns (magazine, corporate).
Saving
Click Save & publish at the top of the page. The admin:
- Resolves every reference (post id → post URL, category id → category URL, etc.) into absolute URLs
- Runs the
menu.json.resolvedfilter — plugins likeflexweg-rssuse this to inject footer entries (e.g. an "RSS" link per category feed) - Uploads the resulting
/menu.jsonto Flexweg - Toasts "Menus saved and published"
The public site's next page load (visitors and yourself) picks up the new menu.json.
What menu.json looks like
Roughly:
{
"header": [
{ "label": "Home", "href": "/index.html", "current": false },
{ "label": "News", "href": "/news/index.html", "current": false,
"children": [
{ "label": "Sport", "href": "/sport/index.html", "current": false },
{ "label": "Tech", "href": "/tech/index.html", "current": false }
]
},
{ "label": "About", "href": "/about.html", "current": false }
],
"footer": [
{ "label": "Privacy", "href": "/privacy.html", "current": false },
{ "label": "RSS", "href": "/rss.xml", "current": false, "external": true }
],
"branding": {
"logoUrl": "/theme-assets/default-logo.webp?v=1715000000000",
"siteTitle": "My site"
}
}
Theme menu loaders consume this. The branding field is set by the active theme's settings page (logo upload). The ?v=… query string is a cache-bust based on logoUpdatedAt.
What happens when references break
If a menu item references a post that gets deleted, or a category whose slug changes, the next save resolves correctly:
- Deleted post — the item resolves to
404.html(or is omitted, depending on the resolver). The admin's UI shows a warning: "This menu item references a deleted post." - Renamed slug — the item still references the same Firestore id, so the URL is automatically updated to the new slug on next save.
The admin auto-resolves on every publish action that affects content (publishPost, unpublishPost, deletePostAndUnpublish) — so the menu stays consistent without manual intervention. In fact, every post / page publish re-uploads /menu.json automatically, even if the menu structure didn't change, to ensure URLs in the JSON match current slugs.
Plugin contributions to menus
Plugins can add menu items dynamically via the menu.json.resolved filter. The most prominent example: flexweg-rss injects footer entries for each enabled RSS feed when its Show in footer option is on.
The injected items appear at the end of the footer in the resolved JSON. Users wanting to control placement should leave the plugin's "Show in footer" off and add the URL manually via the menu builder.
Themes and menu containers
Themes opt into dynamic menus by including:
<nav data-cms-menu="header">
<ul></ul>
</nav>
<nav data-cms-menu="footer">
<ul></ul>
</nav>
In their BaseLayout and a <script> that loads the theme's runtime menu loader. The runtime loader reads [data-cms-menu] containers and fills their inner <ul>.
Themes without [data-cms-menu] containers won't render the dynamic menu — they'd need to fall back to a static / hardcoded nav. All three built-in themes (default, magazine, corporate) use the dynamic system.
When menus don't update on the public site
Symptoms: you save the menu but visitors still see the old menu.
Possible causes:
menu.jsonupload failed — check the Menus page's publish log for an error- CDN / browser cache —
menu.jsonis fetched withcache: no-storeheaders from the loader, but if you've put a CDN in front that caches aggressively, it might serve a stale version. Configure your CDN to short-TTLmenu.json. - The theme's loader script didn't load — open DevTools → Network → reload, look for
theme-assets/<theme-id>-menu.js. If 404, click Themes → Sync theme assets. - The page is being viewed via a stale browser tab — the loader runs on every page navigation, but if the user has the same tab open, hard-refresh (Cmd+Shift+R / Ctrl+F5).
Continue
- Settings → General — site title (used in menu branding)
- Categories and tags — sources of menu items
- Themes → Sync theme assets — re-upload menu loader