Skip to main content

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:

  1. When you save the menu, the admin uploads a single /menu.json file to your Flexweg site root.
  2. Every published page's <head> references a theme-assets/<theme-id>-menu.js runtime script.
  3. 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

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-archives plugin's archive index (only when the plugin is enabled)
  • RSS feed — link to a flexweg-rss feed (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 / _blank toggle
  • 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:

  1. Resolves every reference (post id → post URL, category id → category URL, etc.) into absolute URLs
  2. Runs the menu.json.resolved filter — plugins like flexweg-rss use this to inject footer entries (e.g. an "RSS" link per category feed)
  3. Uploads the resulting /menu.json to Flexweg
  4. 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:

  1. menu.json upload failed — check the Menus page's publish log for an error
  2. CDN / browser cachemenu.json is fetched with cache: no-store headers 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-TTL menu.json.
  3. 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.
  4. 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