How to Rebuild a Nav Menu From wp_posts and wp_postmeta When the Admin UI Lies

The WordPress navigation menu system stores its structure as a set of nav_menu_item post types, each holding a pointer to an original object—a page, a category, a custom link—and a parent-child relationship that defines the tree. When the admin UI shows a menu that looks fine but the front end renders a broken hierarchy, missing items, or a flat list, the problem is almost always in the wp_posts and wp_postmeta rows that hold the menu’s actual configuration. This article walks through the exact schema, the failure patterns I’ve seen repeatedly in the field, and a manual rebuild process that bypasses the unreliable UI. It’s written for small-to-mid publishing teams who can’t afford menu downtime and need a direct, repeatable fix.

Why the Menu Admin UI Lies

The WordPress admin menu editor reads from the same wp_posts and wp_postmeta tables the front end uses, but it assembles the tree with its own logic. When a menu breaks, the UI often still displays the items because it caches the structure in the browser or because it tolerates orphaned menu_order values. The front end, on the other hand, relies on wp_nav_menu() and the Walker_Nav_Menu class, which interpret the post_parent and menu_order columns strictly. A single mismatch—a parent ID pointing to a deleted post, a duplicate menu_order, or a missing _menu_item_object_id—can collapse the entire tree. The UI won’t warn you. It’ll just show the items, and you’ll assume the database is correct. It isn’t.

Close-up of database schema diagrams on a monitor

The Exact Schema You Need to Know

Every menu is a term in the wp_terms table, linked to wp_term_taxonomy with taxonomy = 'nav_menu'. The items are posts of type nav_menu_item in wp_posts, connected to the menu term via wp_term_relationships. The columns in wp_posts that matter are:

  • ID – the unique post ID for the menu item itself.
  • post_parent – the ID of the parent menu item (not the original object). A top-level item has post_parent = 0.
  • menu_order – the sort order within the same parent. Lower numbers appear first.
  • post_type – always 'nav_menu_item'.
  • post_status – should be 'publish' for visible items.

The wp_postmeta table stores the item’s type and target. The essential meta keys are:

  • _menu_item_type – 'post_type', 'taxonomy', or 'custom'.
  • _menu_item_object_id – the ID of the linked post or term.
  • _menu_item_object – the object type, e.g., 'page', 'category', 'custom'.
  • _menu_item_url – used for custom links.
  • _menu_item_menu_item_parent – a duplicate of post_parent stored as meta; often the source of inconsistency.

When the UI lies, the discrepancy is usually between post_parent and _menu_item_menu_item_parent, or between menu_order values that conflict. The fix is to query the raw data, identify the break, and rewrite the rows directly.

Step 1: Extract the Raw Menu Data

Connect to the database using your preferred client—Adminer, phpMyAdmin, or the MySQL CLI. Identify the menu’s term ID first. If you know the menu slug (e.g., 'primary'), run:

SELECT t.term_id, t.name, t.slug
FROM wp_terms t
JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id
WHERE tt.taxonomy = 'nav_menu' AND t.slug = 'primary';

Note the term_id. Then pull every menu item belonging to that term, along with its parent and order:

SELECT
    p.ID AS menu_item_id,
    p.post_title,
    p.post_parent,
    p.menu_order,
    p.post_status,
    pm_object.meta_value AS object_id,
    pm_type.meta_value AS object_type,
    pm_parent.meta_value AS meta_parent
FROM wp_posts p
JOIN wp_term_relationships tr ON p.ID = tr.object_id
LEFT JOIN wp_postmeta pm_object ON p.ID = pm_object.post_id AND pm_object.meta_key = '_menu_item_object_id'
LEFT JOIN wp_postmeta pm_type ON p.ID = pm_type.post_id AND pm_type.meta_key = '_menu_item_object'
LEFT JOIN wp_postmeta pm_parent ON p.ID = pm_parent.post_id AND pm_parent.meta_key = '_menu_item_menu_item_parent'
WHERE tr.term_taxonomy_id = (
    SELECT term_taxonomy_id FROM wp_term_taxonomy WHERE term_id =  AND taxonomy = 'nav_menu'
)
AND p.post_type = 'nav_menu_item'
ORDER BY p.menu_order ASC;

Export this result to a spreadsheet or a text file. You now have the ground truth of what the database holds, not what the admin UI renders.

Person inspecting database query results on a laptop screen

Step 2: Identify the Structural Breaks

Look for these specific failure patterns in your exported data:

Orphaned Parent References

If a menu item has post_parent = X but no row exists with ID = X in the same menu, the item is orphaned. The front end will either drop the item or render it at the top level, depending on the theme’s walker. The admin UI often still shows the intended hierarchy because it does not validate parent IDs on every load.

Mismatched Parent Pointers

Compare the post_parent column with the _menu_item_menu_item_parent meta value. If they differ, the front end typically trusts post_parent, but some plugins or custom walkers read the meta value. This inconsistency causes items to jump levels unpredictably.

Duplicate or Gapped menu_order

WordPress does not enforce unique menu_order values, but the Walker_Nav_Menu class sorts by menu_order then by ID. Duplicate values can cause items to swap positions on every page load. Gaps are harmless but often signal that items were deleted and re-added, leaving stale rows.

Stale Object References

If _menu_item_object_id points to a post or term that no longer exists, the menu item may still appear in the admin but will render as a broken link or not at all on the front end. The UI does not always validate object existence.

Step 3: Rebuild the Menu Structure Manually

Once you have identified the broken rows, you can rebuild the menu directly in the database. This approach is faster and more reliable than dragging items in the admin, especially for menus with dozens of items or deeply nested structures.

Delete Stale Items

Remove any nav_menu_item posts that point to non-existent objects. Use a query like this to find them:

SELECT p.ID, p.post_title, pm.meta_value AS object_id
FROM wp_posts p
JOIN wp_postmeta pm ON p.ID = pm.post_id AND pm.meta_key = '_menu_item_object_id'
WHERE p.post_type = 'nav_menu_item'
AND pm.meta_value NOT IN (SELECT ID FROM wp_posts WHERE post_status = 'publish');

Verify the results, then delete the orphaned menu items and their postmeta. If you are unsure, move them to trash first by setting post_status = 'trash'.

Reset Parent-Child Relationships

Map out your desired menu tree on paper or in a spreadsheet. Assign each item a new post_parent value based on the ID of its intended parent. Top-level items get post_parent = 0. Then update the wp_posts table and the _menu_item_menu_item_parent meta key in a single transaction to keep them in sync:

START TRANSACTION;

UPDATE wp_posts SET post_parent =  WHERE ID = ;
UPDATE wp_postmeta SET meta_value = 
WHERE post_id =  AND meta_key = '_menu_item_menu_item_parent';

COMMIT;

Repeat for every item that needs correction. If you are rebuilding the entire menu, consider deleting all items and re-inserting them with a script, but for most cases, targeted updates are safer.

Fix menu_order Values

Assign sequential menu_order values within each parent group, starting from 1. For example, if a parent has three children, set their menu_order to 1, 2, and 3. This prevents the front-end walker from mis-sorting items. Use a simple UPDATE statement for each item.

Person writing database queries on a whiteboard

Step 4: Validate the Menu in the Front End

After the updates, clear any server-side or plugin caches. Then load the front end of the site and inspect the menu. Use the browser’s developer tools to view the generated HTML. The menu should now reflect the exact parent-child relationships and order you set in the database. If items are still missing, check for theme-level filters like wp_nav_menu_objects or wp_nav_menu_items that might be altering the output. Also verify that the menu is assigned to the correct theme location under Appearance → Menus → Manage Locations.

When the Admin UI Is Useful Again

Once the database rows are consistent, the admin UI will usually start working correctly. You can then use it for minor adjustments. However, if your menu is large or complex, consider locking it down: disable the nav-menu.php capability for editors, or use a version-controlled menu export plugin to track changes. The goal is to treat the menu as configuration, not content, and to manage it with the same rigor you apply to schema design.

FAQ

Why does my menu look correct in the admin but break on the front end?

The admin menu editor does not strictly validate parent-child relationships or object references. It often displays items based on cached data or a more lenient interpretation of the database rows. The front end uses Walker_Nav_Menu, which requires consistent post_parent and menu_order values. A single orphaned parent or duplicate order can collapse the entire tree.

Can I fix a broken menu without touching the database?

Sometimes. If the break is minor—a single item out of order—you can often fix it by dragging the item in the admin and saving. But if the admin UI itself is unresponsive, or if items disappear when you try to move them, the database is the only reliable path. This is especially true for menus with 50+ items or deeply nested structures, where the admin’s AJAX handlers may time out or corrupt data.

How do I prevent menu corruption in the future?

Limit the number of users who can edit menus. Use a child theme or a custom plugin to lock critical menus via the wp_nav_menu_args filter. Regularly export your menu structure using WP-CLI or a database backup, and store the export in version control. If you manage a publishing team, consider treating the menu as part of your deployment process—build it once, export it, and re-import it on staging and production rather than editing it live.

What if my menu items point to categories that no longer exist?

Stale object references are a common cause of silent menu failures. Run the diagnostic query from Step 2 to identify menu items whose _menu_item_object_id does not match any published post or existing term. Delete those items from wp_posts and their associated postmeta. Then rebuild the missing links manually or via a bulk insert.

For related troubleshooting when a site returns unexpected results, see What to Fix First When a New WordPress Site Says Nothing Found, which covers the initial diagnostic steps for broken permalinks and query issues that can compound menu problems.