WP Manifestindependent plugin directory
manifest / admin / wp-nonstop-nav-toolkit

NonStop Nav Toolkit

Build, organize, and manage navigation faster. Enhances the classic Appearance → Menus editor with tree tools, bulk actions, menu trash, import/export, visibility rules, revisions, and link health.

★ 0stars

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/nonstopmarketing/wp-nonstop-nav-toolkit/archive/refs/heads/main.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

Build, organize, and manage navigation faster.

A WordPress plugin by NonStopDev that makes the classic Appearance → Menus editor fast and safe for large menus, and adds a toolkit page at Appearance → Nav Toolkit.

  • Plugin slug / text domain: nonstop-nav-toolkit
  • Requires: WordPress 6.4+, PHP 7.4+ (tested in CI on PHP 7.4 – 8.4)
  • License: GPL-2.0-or-later
  • Paid edition (planned): NonStop Nav Toolkit Pro

Build status: every feature (Free, Pro, Agency) is built and unlocked in this single plugin. Each feature is tagged with its future plan in includes/Features.php, so the paid split is a licensing change only. See Licensing and the Pro split.


Installing

  1. Download the zip from the latest CI run (Actions → CI → Artifacts → nonstop-nav-toolkit), or build it yourself:
    mkdir -p dist && git archive --format=zip --prefix=nonstop-nav-toolkit/ -o dist/nonstop-nav-toolkit.zip HEAD
  2. In WordPress: Plugins → Add New → Upload Plugin, choose the zip, and activate.
  3. Open Appearance → Menus. The toolbar appears above the menu structure.
  4. Open Appearance → Nav Toolkit for trash, import/export, revisions, health, preview, contextual menus, activity, and settings.

Activation only schedules two background jobs. It does not create or change any menu data.

Where things live

Location What's there
Appearance → Menus Collapse/expand tree, hierarchy guides and child counts, search with ancestors kept, type/status filters, mouse selection (click, Shift+click range, Ctrl+click toggle, lasso drag across empty space, drag one selected row to move the whole selection, select branches), "Move to" parent picker, top/up/down/bottom/indent/outdent, remove with branch-or-promote choice, quick inline edit, open destination, undo/redo, unsaved-changes indicator, conflict check on save, bulk field edit, bulk URL replace with preview, duplicate branch/menu, link check, compact/comfortable rows. Menu-level tools (special items, Replace URLs, Check links, Duplicate menu, trash) live in a compact Tools menu; density and URL display in View. Each item's settings panel gains Visibility and Display sections. A side box (and the toolbar) adds label-only headings/parents, log in/out, and account links.
Appearance → Nav Toolkit Overview (menus + theme locations), Menu trash, Import/Export (JSON, CSV), Templates, Revisions (with comparison), Health (links, content status, duplicates), Preview (visibility + structure), Contextual menus, Network (multisite), Activity, Settings.

Mouse

Gesture Action
Click a row Select only that row (click it again to deselect)
Shift + click Select the range from the last clicked row
Ctrl/⌘ + click Add or remove one row
Drag across empty space beside the rows Lasso-select (hold Ctrl/Shift to add to the selection)
Click empty space / Esc Clear the selection
Drag a selected row Move it; the other selected items follow and land next to it
Double-click a row Quick edit label and URL

Prefer checkboxes? Turn on View → Checkboxes (or set the site default in Settings). Clicking and Shift+clicking the boxes selects just like clicking rows.

Adding items from the left column

Gesture Action
Click a checkbox Check / uncheck as usual
Shift + click Check (or uncheck) the range from the last clicked box
Drag a box across a list Check every item it touches (hold Alt to uncheck)
Esc or "Deselect all" Uncheck everything in that box
Ctrl/⌘ + A Check the whole list (after clicking in it)
Drag a checked item into the menu structure Add all checked items at the drop line; point further right to nest under the row above

Keyboard (click a row first, or Tab to it)

Keys Action
↑ / ↓ Previous / next visible item (Shift extends selection)
← / → Collapse / expand, or go to parent / first child
Alt + ↑ / ↓ Move selected items up / down
Alt + ← / → Outdent / indent
Enter or F2 Quick edit label and URL
Delete or Backspace Delete all selected items (to menu trash on save; Undo restores)
Ctrl/⌘ + A Select all visible
Ctrl/⌘ + Z, Ctrl/⌘ + Shift + Z (or Y) Undo / redo

Core's own arrow-key moves on the "Edit" link keep working and are recorded in undo history.

Feature map

All spec IDs are implemented. Plan tags are for the future split; today everything is enabled.

ID Feature Plan Where
P1-01 – P1-04 Collapse, expand/collapse all, remembered state (per user/menu), hierarchy guides & counts, visible drop target Free Menus toolbar/rows
P1-05 Search label/URL, ancestors kept, filtering clearly shown, drag paused Free Menus toolbar
P1-06 – P1-10 Multi-select, group move/nest, parent selector, quick moves Free Menus toolbar
P1-11 – P1-14 Bulk remove, menu trash, restore with hierarchy, permanent delete/empty Free Menus + Nav Toolkit → Menu trash
P1-15 Label-only headings/parents (truly empty URL) Free Side box / toolbar "Add"
P1-16 – P1-20 Inline edit, undo/redo, dirty state, keyboard, open destination Free Menus
P1-21 / P1-22 JSON export / validated import into a new menu with preview Free Import / Export
P1-23 / P1-24 Duplicate menu / branch Pro Toolbar, Overview
P1-25 / P1-26 Bulk field edit / bulk URL replace with preview Pro Toolbar
P1-27 Merge (append/replace), branch selection, duplicate flags Pro Import preview
P1-28 / P1-33 Logged-in/out visibility; hidden parent hides branch Free Item → Visibility
P1-29 Role visibility (any role matches) Pro Item → Visibility
P1-30 / P1-31 Login/logout link with validated redirect / profile, dashboard, register, WooCommerce account Free / Pro Side box
P1-32 Visibility preview (simulated, no impersonation) Pro Preview
P2-01 – P2-04 Type/status filters, location overview, density, content status Free Menus, Overview
P2-05 / P2-06 / P2-15 Link health (SSRF-safe, batched), duplicate URLs, scheduled checks Pro Health, toolbar
P2-07 / P2-08 Saved revisions, comparison, restore Pro Revisions
P2-09 / P2-10 Cross-site mapping by path/slug/type/taxonomy, mapping report with manual resolution Pro Import preview
P2-11 CSV transfer Pro Import / Export
P2-12 Custom-field adapters (nsnt_meta_adapters) Pro Filter
P2-13 / P2-14 Templates, structure preview Pro Templates, Preview
P2-16 – P2-20 Agency presets, feature permissions, activity history, multisite copy, white label Agency Settings, Activity, Network
P2-21 Contextual menus (explicit precedence + fallback) Pro Contextual menus
P2-22 – P2-25 Membership visibility, scheduled visibility, dynamic labels, icons/badges/descriptions Pro Item → Visibility / Display

The optional front-end renderer (mega menus, off-canvas, etc.) is out of scope for this plugin, per the spec.

Architecture

nonstop-nav-toolkit.php      Bootstrap, version gate, hooks
includes/
  Plugin.php                 Wires modules
  Features.php               Feature → plan registry (the Pro split lives here)
  Licensing/                 LicenseAdapter interface + UnlockedLicense (replace via filter)
  Capabilities.php           Meta caps for every action (P2-17)
  Settings.php, Meta.php     Settings option; registry of plugin-owned item meta + adapters
  PostTypes.php              Private storage types: trash, revisions, activity, templates
  Tree/Tree.php              Server-side tree validation/ordering (authoritative)
  Menu/                      MenuReader (normalize + fingerprint), ItemWriter (validate → write → rollback), Duplicator
  Trash/TrashStore.php       Versioned recovery snapshots captured before core deletes items
  Transfer/                  Schema (limits/validation), Exporter, Importer (plan/preview/apply), Mapper, Csv
  Visibility/                Evaluator (pure rules), Membership adapters
  Frontend/Renderer.php      wp_nav_menu filters: rules, label-only output, dynamic items, icons/badges, contextual menus
  Revisions/                 Snapshot on save, Diff, restore
  Health/                    LinkScanner (cron batches), ContentStatus, UrlTools
  Templates/, Contextual/, Multisite/, Activity/
  Admin/MenuEditor.php       Menus screen: fields, save validation, conflict check, assets
  Admin/Page.php + Tabs/     Appearance → Nav Toolkit
  Rest/RestController.php    Editor endpoints (nonstop-nav-toolkit/v1)
assets/js/tree-core.js       Shared pure tree operations (tested in Node)
assets/js/menu-editor.js     Menus screen enhancements

Key decisions

  • Native storage only. Menus remain nav_menu terms and nav_menu_item posts; IDs and location assignments are untouched. Plugin data is extra post meta (_nsnt_*) and private post types. Deactivating the plugin leaves every menu working.
  • The DOM stays the source of truth on the Menus screen. Moves reorder the native list items and depth classes, then call core's own parent-ID and accessibility refresh, so core saving, native fields, and third-party fields keep working. Every structural change is computed by tree-core.js (cycle-safe, preserves tree order, never splits a selected parent from its child).
  • Server is authoritative. Before core saves, MenuEditor::validate_save() rejects missing parents, self-parents, and cycles without writing anything, and compares a content fingerprint to block silent overwrites (409 with a back link; the editor offers "Save anyway" first).
  • Trash without touching linked content. before_delete_post captures a versioned snapshot of any published menu item WordPress is about to delete (from the toolbar, core's "Remove" link, or core cleanup after linked content is deleted). Deleting a whole menu keeps no orphan records. Restore allocates new IDs, rebuilds hierarchy, and reports items placed at the top level because their parent is gone.
  • No atomicity claims. Batch writes (import, duplicate, restore) validate first, then write; created items are removed if a later write fails. Replacements and revision restores take a snapshot first.
  • Visibility is evaluated on the server in wp_nav_menu_objects. Hidden ancestors hide descendants; "has children" classes are corrected. Membership rules fail closed when their plugin is missing and an admin notice explains it. There is no administrator bypass unless enabled in Settings. Stored rules keep being enforced regardless of license state.

Security

  • Every write: capability check + nonce (admin-post), REST permission callbacks + core REST cookie nonce, or core's own update-nav_menu nonce. Hidden UI is never relied on.
  • Inputs are sanitized per field; only registered meta keys are accepted (Meta::own_keys() + registered adapters). Output is escaped.
  • URLs: javascript:, data:, vbscript:, file: and control characters are rejected; "Open destination" only opens http(s).
  • Imports: JSON only (no unserialize), schema ID + version check, size/item/depth limits, duplicate keys, cycles, malformed URLs, unsupported types. CSV cells beginning with = + - @ are prefixed on export to stop formula injection.
  • Link checks: wp_safe_remote_* (private/reserved addresses refused), redirects followed manually and re-validated, 5 s timeout, no cookies or credentials, batched in WP-Cron.
  • Login/logout redirects go through wp_validate_redirect(). Dynamic label values are escaped.
  • No telemetry. No third-party services.

Limits (filterable)

Limit Default Filter
Import size 2 MB nsnt_import_limits (max_bytes)
Import items 2,000 nsnt_import_limits (max_items)
Import depth 12 levels nsnt_import_limits (max_depth)
Undo history 50 steps per session —
Revisions per menu 25 (1–200) Settings
Activity retention 90 days Settings

Transfer format

JSON envelope (schema: "nonstop-nav-toolkit/menu", schema_version: 1, plugin_version, exported_at, source_url, menu.name, ordered items). Each item: key, parent (key or null), type, object, object_id (informational only), object_slug, url, path, title, attr_title, target, classes, xfn, description, meta (plugin-owned), extra (adapter fields).

CSV columns: key, parent_key, title, type, object, object_id, object_slug, url, path, target, classes, xfn, attr_title, description, meta. classes is space-separated; meta is a JSON object. Header row required; column order is free.

Imports never trust source IDs from another site. Content is matched by path, then slug, within the same post type or taxonomy, and only to content the importing user may read. Unmatched items are listed before import with "keep as custom link" or "skip". Importing the same file twice creates the items twice.

Block themes

Classic menus and the Navigation block are separate systems. On block themes the plugin explains the boundary, links to the Site Editor's navigation screen, does not force-enable Appearance → Menus, and never reads or writes Navigation blocks.

Caching

Menus with visibility rules, login/logout links, account links, or dynamic labels vary by visitor. Most page caches already bypass logged-in users. If guests or roles see the wrong menu, enable Settings → Page caching (sends no-cache headers and defines DONOTCACHEPAGE only on pages where an assigned menu varies) or configure your cache/CDN to vary by login cookie. Core logout links contain a per-user nonce.

Hooks for developers

Hook Type Purpose
nsnt_license_adapter filter Return a LicenseAdapter (Freemius integration point)
nsnt_feature_enabled filter Override a feature flag
nsnt_meta_adapters filter Register third-party item meta to carry through export, trash, revisions, duplication
nsnt_membership_providers filter Add membership/visibility providers
nsnt_import_limits filter Change import limits
nsnt_migrate_import filter Upgrade older/newer envelopes before validation
nsnt_label_only_markup filter Theme adapters for label-only items (e.g. submenu toggle buttons)

Example adapter:

add_filter( 'nsnt_meta_adapters', function ( $adapters ) {
    $adapters['menu-image'] = array(
        'label' => 'Menu Image',
        'keys'  => array( '_menu_item_image_id' => 'absint' ),
    );
    return $adapters;
} );

Label-only items and custom walkers

Headings and submenu parents are custom items with an empty URL plus _nsnt_kind. Through the core walker_nav_menu_start_el filter they render as <span class="nsnt-label"> text, never as an empty link. Themes whose custom walkers skip that filter will show core's default output (an <a> without href). Submenu toggle buttons need a theme adapter via nsnt_label_only_markup.

Licensing and the Pro split

  • includes/Features.php maps every feature to free, pro, or agency.
  • License::adapter()->plan() decides what is enabled. Today UnlockedLicense returns agency.
  • To split: ship a Pro add-on or Freemius build that supplies a real adapter through nsnt_license_adapter, and keep the free build returning free. When a license expires, keep at least free: existing data and public output are never removed.

Development

composer install
vendor/bin/phpcs -n
vendor/bin/phpunit
npm test

CI (.github/workflows/ci.yml) runs PHP syntax checks on 7.4–8.4, coding standards, PHPUnit, the JS tests, and builds the installable zip as an artifact.

See TESTING.md for the manual and browser test plan and the 300-item benchmark procedure.

Uninstall

By default, deleting the plugin keeps all data. If Settings → Uninstall is ticked, it deletes only plugin-owned data: trash records, revisions, activity, templates, settings, user preferences, scan results, and _nsnt_* item meta. It never deletes menus, menu items, or linked content.