Internal links and site structure guide
Find and Repair Broken Internal Links After WordPress Content Changes
Find each broken link's source, trace it to the post, menu, pattern or template that stores it, choose a live destination, and fix the link itself instead of relying on a redirect.
On this page
To repair a broken internal link, you need three facts: the page the link sits on, where WordPress stores it (post content, a menu, a synced pattern, a widget or a template), and the live page it should point to. Collect those from a crawl, edit the stored link, and request the page again to confirm it now works. A redirect from the old address helps visitors who arrive from outside, but it does not change the link on your page, so fix the source as well.
Broken links usually appear after a content change: a slug edited, a page deleted or merged, a category given a new slug, or a page moved under a new parent. Knowing which change happened tells you where to look.
Collect broken links with their source pages
Run the crawl.py script from the small-site link audit and filter links.csv for rows whose status is 404, 410, a 5xx code or an error. Each row gives you the source page, the href exactly as written, and the resolved destination. Sort by destination: twenty rows pointing to one missing URL usually mean one link in a menu or template, not twenty separate mistakes.
Google’s documentation on HTTP status codes, checked September 27, 2026, says Google does not index URLs that return a 4xx status other than 429, and removes already indexed URLs that start returning one. For your own links, the more immediate cost is the reader who clicks and lands on an error page.
If you use WP Visibility, note that its Link Graph is not a broken-link checker. It records links in post content only when they resolve to a published post, so a link to a missing page does not appear in it. Read the links report describes what it does record.
Trace each link to where it is stored
The source page tells you where the link appears, not where it is stored. The same link can live in several places:
| Where it is stored | How to confirm | Where to edit |
|---|---|---|
| Post or page content | The link appears on one page only | Edit that post |
| Navigation menu, block theme | It appears in the header or footer on every page | Appearance, Editor, Navigation |
| Classic menu | Same, on a classic theme | Appearance, Menus |
| Synced pattern | It appears in the same block on several pages | Edit the pattern once |
| Widget or template part | It appears in a sidebar, header or footer | The widget screen or the Site Editor |
| Theme file | None of the above contain it | A child theme or the theme developer |
The Synced Patterns documentation, checked September 27, 2026, notes that editing a synced pattern updates it everywhere it is used, so one fix there repairs every page. A pattern that was detached into a regular block does not update, and each copy needs its own edit.
With WP-CLI, you can search the database for the broken path to find every row that stores it. The wp db search documentation, checked the same day, describes it as searching the text columns of database tables, by default the tables registered with WordPress:
wp db search '/tomato-pruning-calendar/' --stats
The results name the table and column. wp_posts rows can be posts, pages, synced patterns (wp_block), block theme menus and templates edited in the Site Editor. wp_postmeta rows under the key _menu_item_url are custom links in classic menus (wp_update_nav_menu_item reference, checked September 27, 2026). wp_options rows are often widgets or theme settings. The search also finds revisions, which you can ignore.
Also check the raw href column for relative paths. A link written as pruning/ without a leading slash resolves against the current page’s address (RFC 3986, section 5.2, checked September 27, 2026), so it works on one page and breaks on another. Replace it with a root-relative path such as /guides/pruning/ or the full URL.
Choose the right destination
For each broken link, pick the replacement by what the reader wanted, not by what is convenient:
- The page moved. Use its new address.
- The page merged into another. Link the section of the new page that covers the same need, if it has one.
- The page was removed with no replacement. Remove the link, and rewrite the sentence if it depended on it. Pointing every dead link at the homepage sends readers somewhere they did not ask to go.
- The page should exist but was deleted by mistake. Restore it from the trash or a backup. Since WordPress 5.6, a post restored from the trash comes back as a draft by default (wp_untrash_post reference, checked September 27, 2026), so publish it again, then check it answers.
Record each decision in your worksheet so the next person knows why a link was removed rather than replaced.
Update the stored link, not only the redirect
When a page moves, adding a redirect is good practice for visitors and outside links. WP Visibility’s Redirects module, for example, creates a 301 automatically when you change the slug of a published post while the module is on; Redirect a changed URL covers the rules. That redirect does not edit the links in your content. They keep pointing at the old address, and every click goes through the redirect. Cleaning those up is covered in Update internal links that point through redirects.
For a handful of links, edit them by hand in the editor, menu or pattern. For many links to the same old address, WP-CLI’s search-replace can change them in one pass. Take a full backup first, and always start with a dry run:
wp search-replace 'https://garden.example/tomato-pruning-calendar/' 'https://garden.example/guides/planting-calendar/' wp_posts --include-columns=post_content --dry-run
The search-replace documentation, checked September 27, 2026, describes --dry-run as running the whole operation and reporting it without saving. The example assumes the default wp_ table prefix; if wp db search showed a different one, use that table name. The reported count is the number of rows that would change, one per changed row such as a post, pattern or revision, not the number of links (command source, checked September 27, 2026), so compare it with the wp_posts post_content matches from wp db search rather than with your crawl. Two cautions:
- A search string is matched anywhere.
/calendar/would also change/calendar/2025/. Use the full URL, and run a second pass for root-relative links, such as a link to/tomato-pruning-calendar/with no domain, if your content uses them. - The command changes revisions too, and it cannot tell whether a replacement makes sense in its sentence. Read a sample of changed posts afterward.
Remove --dry-run only when the count looks right.
Verify the public responses
After editing:
- Clear any page cache so visitors receive the updated HTML.
- Check the new destination responds successfully:
curl -sI https://garden.example/guides/planting-calendar/ | grep -i "^HTTP"
- Open two or three of the source pages signed out, including one on a phone, and click the fixed links.
- Run the crawl again and filter for 4xx and 5xx statuses. The fixed rows should be gone. If a row remains, the link is stored somewhere you have not edited yet; search for it again.
Broken link checklist
- Broken rows grouped by destination, with their source pages.
- Each link traced to post content, a menu, a pattern, a widget, a template or a theme file.
- A replacement chosen for the reader’s need, or the link removed on purpose.
- Stored links edited; bulk changes backed up and dry-run first.
- Caches cleared, pages checked signed out, and a second crawl clean.
