The Blog

Broken links nobody noticed

Web performanceTechnical SEO

We renamed a few blog slugs during a content restructure and missed six internal links pointing to the old URLs. No errors in the build. No warnings in the CMS. Just quiet 404s leaking link equity. We added a link checker to CI so it can't happen again.

The problem

Slug changes that break things silently

Internal links break quietly. There's no error in the build. The CMS doesn't know that a rich text link in one post points to a slug that was renamed three weeks ago. The page still renders. The link still looks like a link. It just lands on a 404.

We found six of these during a routine content audit. All caused by the same thing: renaming blog slugs as part of a content restructure. The posts got better URLs. The internal links pointing to the old URLs got nothing, no redirect, no warning, no broken-build flag.

Six broken links across a blog with fewer than 50 posts. On a larger site, hundreds of pages, multiple editors, regular restructures, the number compounds fast. Every broken internal link is wasted crawl budget and leaked link equity. Google finds a 404 where it expected content, and the authority that link carried disappears.

The CMS stores content. It doesn't validate outbound references to other routes unless you build that check.

The fix

A link checker that runs on every push

We added a link validation step to our CI pipeline. On every push, after the build finishes, a script reads every internal link in every prerendered page and checks it against the pages the build actually produced, plus our redirects. If a link points to a URL that doesn't exist, the build fails. Links that only work through a redirect get a warning so they can be updated.

The check covers links in rich text content, navigation menus, CTA blocks, footer links, and any hardcoded internal references in components, across every page generated at build time. Developers see a broken link in the failing build. Editors see it when they try to publish: the CMS checks custom URLs and refuses to publish a link to a page that doesn't exist.

The key decision was making it a hard failure, not a warning. Warnings get ignored. A failing build gets fixed. The developer sees which page contains the broken link, which URL it points to, and can either update the link or add a redirect entry in the CMS.

The context

Rich text links are invisible references

In a traditional codebase, internal links are usually JSX or HTML, a linter or IDE can flag a broken import or a missing route. In a CMS-driven site, most internal links live inside rich text fields. They're stored as serialised data in the database. No linter sees them. No IDE underlines them.

Payload CMS supports document-level references in rich text, linking to a document by ID rather than by URL. When the document's slug changes, the link stays valid because it resolves from the ID. We use this where we can. But not every internal link is a document reference. Some are hardcoded paths typed into rich text or CTA blocks. Those are the ones that break.

The CI check is the safety net for links that can't be document references. It catches what the CMS can't validate on its own.

The balance

If the CMS doesn't enforce it, CI should

Broken links are one example of a broader pattern: content integrity checks that the CMS doesn't handle natively. Missing alt text on images. Orphaned pages with no internal links pointing to them. Redirect chains that add latency. Meta descriptions that exceed character limits.

Each of these can be checked programmatically. The question is where in the workflow that check runs. Checking in the CMS means building custom validation hooks for every content type. Checking in CI means one script that runs against the rendered output and catches everything — regardless of which collection or field the content lives in.

We run both. CMS-level validation for things editors need immediate feedback on (required fields, image dimensions). CI-level validation for things that only become visible when the full site is built (broken internal links, redirect chains, missing pages in the sitemap).

Six broken links across 50 posts. Found by accident. Now found by CI on every push.

FAQs

What the check covers, and what it doesn't

The link check adds around 15–20 seconds to a full build. It runs after the sitemap is generated and before the deployment step. The time cost is negligible compared to the cost of broken links persisting for weeks.

We don't check external links in CI, they're outside our control and can return false positives (rate limiting, geo-blocking, temporary outages). External link checking runs as a separate scheduled audit.

Yes. The CI output shows which page contains the broken link and what URL it points to. If the link is a rich text reference, the editor updates it in the CMS. If the old URL needs to persist, they create a redirect entry instead.

The CI check runs against the published site. Draft content isn't in the build output, so draft links aren't checked until the content is published. We're considering a separate validation hook at the CMS level for draft content.

Little Dash

How many broken links are hiding in your site right now?

We build CMS-driven sites with CI checks that catch broken links, redirect chains, and content integrity issues before they go live.