rapidlaunchcode.app
Enterprise StarterAdvisoryEngineeringProductsWritingAbout
Book a call
rapidlaunchcode.app

Independent technology advisory and engineering. København, Denmark.

Njalsgade 21F, 2. sal, København

CVR 45 44 13 93

Nicklas@rapidlaunchcode.app

WhatsApp +45 31 33 25 99

Work

  • Enterprise Starter
  • Advisory
  • Engineering
  • Products
  • Contact

Resources

  • Writing
  • Guides
  • Free tools
  • About

Elsewhere

  • HourIQ
  • Translately
  • NomadWorld
  • Privacy

© 2026 Rapid Launch Code ApS. All rights reserved.

Built in København with Next.js, Contentful, and zero consultancy bullshit.

Back to guides
5 min · CMS & CONTENT

Dynamic CMS linking: why your 'Link' field is a time bomb

Most CMS implementations let authors paste raw URLs into a text field. The day someone renames a slug, half your navigation silently 404s. Here is the model that makes that impossible.

On this page
  • The Tuesday morning incident
  • The default: a text field
  • Why it breaks at scale
  • The right model: links as references
  • Parent/child relations as the source of truth
  • How I solved it in Contentful
  • Migration path from text-field links
Updated 2026-04
TL;DR
  • A text-field URL input has no validation, no autocomplete, and no referential integrity.
  • Slug edits, locale variants, deletes, and parent/child moves all silently break links you can't find without crawling the site.
  • Model link fields as references to Page entries, not strings. Compute the URL from the page's position in the tree.
  • Parent/child relations belong in the CMS, with a single source of truth for the path.
  • I solved this in Contentful with a self-referencing Page type and a path resolver. Same pattern works in Sanity, Strapi, Storyblok.

The Tuesday morning incident

On a Tuesday morning, a marketer renames a campaign page in the CMS. They change /campaigns/spring-2024 to /campaigns/spring-launch. Three nav items, two homepage cards, and a paid-ad landing page now point at a 404. Nobody notices for eleven days. Paid traffic kept arriving the whole time.

This is not an author error. This is a model error. The CMS handed them a text field and a footgun.

The default: a text field

In the average CMS setup, "link to another page" is implemented as a string field. The author is expected to remember the URL, type or paste it correctly, and update every reference by hand if it ever changes.

There is no validation that the URL exists. No warning if the target is unpublished, deleted, or moved. No way for the CMS to tell you, six months later, "these 47 places link to a page that doesn't exist anymore." The agency that built it shipped on time. The cost was deferred onto the author and onto your traffic.

Why it breaks at scale

It doesn't take a redesign to break. The everyday operations of a content team break it:

  • Slug edits. SEO renames a page for keyword reasons. Every text-field link pointing at the old slug breaks.
  • Locale variants. The English author types /about. The Danish version is /da/om-os. Half the localized pages link out of locale.
  • Parent/child moves. A page moves under a new parent. Its full path changes. Every link to it is now wrong.
  • Deletes and unpublishes. A page comes down. Every text link to it points at a 404 forever.
  • Domain or scheme changes. Trailing slashes, http vs https, www vs apex. Pick any URL detail and find the author who typed it the other way.
A text field for "link to a page" is a database without foreign keys. It will betray you the moment your content team does normal work.

The right model: links as references

Stop storing URLs. Store a reference to the entry the author wants to link to, and compute the URL from the entry. The link field on every content type becomes:

  • A reference to a Page entry (or whichever entry types are linkable).
  • An optional override label, so authors can say "link to this page, but call it 'Read the story'."
  • An optional anchor, for in-page jumps.

The author no longer needs to know URLs. They search by page title, pick the entry, and the system guarantees the link will resolve as long as the page exists.

Parent/child relations as the source of truth

The other half of the fix is modeling navigation hierarchy in the CMS itself. In most setups, the URL of a page lives in three places: a slug field, an editor's memory, and a hand-maintained navigation menu. Three sources, no agreement.

The model that holds up:

  • One field on the page: the slug segment for that page only (e.g. spring-launch, not the full path).
  • A self-reference field: parent, pointing at the page above it in the tree.
  • A computed full path, derived by walking from the page to the root and joining the slug segments.
  • The navigation menu is generated from this tree, not maintained by hand.

When a page moves under a new parent, every URL beneath it updates. Every reference link to it continues to resolve, because references store entry IDs, not paths.

How I solved it in Contentful

The pattern below is the exact model I now use on every Contentful project. It is small. It is the difference between a CMS that ages well and one that decays into a graveyard of broken links.

Page content type

A single Page type covers every URL on the site, with a self-reference for hierarchy.

Contentful · Page
title: Symbol            // required, indexed
slug:  Symbol            // required, segment only ("spring-launch")
parent: Link<Entry<Page>> // optional, self-reference
locale: Symbol           // required (en, da, ...)
sections: Array<Link<Entry<Section>>>
seo:    Link<Entry<Seo>>

// Validation:
//  - slug is unique among pages with the same parent + locale
//  - parent must be of type Page and same locale
//  - cycle check: a page cannot be its own ancestor

Link field used everywhere else

Every other content type that needs a link — CTAs, nav items, cards, rich-text inline links — uses the same PageLink shape. Never a URL string.

Contentful · PageLink
page:   Link<Entry<Page>> // required
label:  Symbol            // optional override
anchor: Symbol            // optional, e.g. "pricing"
rel:    Symbol            // optional, e.g. "nofollow"

// External links are a separate type (ExternalLink),
// so the model itself prevents typing a URL into a field
// that is supposed to be internal.

Path resolver

A small server function turns any Page into its full URL by walking the parent chain. The result is cached per page and invalidated by Contentful webhooks when a page's slug or parent changes.

ts
type PageRef = { sys: { id: string }; fields: { slug: string; parent?: PageRef; locale: string } }

export function resolvePath(page: PageRef): string {
  const segments: string[] = []
  let current: PageRef | undefined = page
  while (current) {
    segments.unshift(current.fields.slug)
    current = current.fields.parent
  }
  const localePrefix = page.fields.locale === "en" ? "" : `/${page.fields.locale}`
  return `${localePrefix}/${segments.join("/")}`.replace(/\/+/g, "/")
}

What the editor sees

When an author drops a link, they search for the page by title in Contentful's reference picker. The CMS shows the resolved URL as a read-only preview. If the target page is unpublished or deleted, Contentful surfaces a broken-reference warning right in the editor — before the change ships, not eleven days later.

Renaming a slug, moving a page under a new parent, or unpublishing a page now updates every link automatically, because no link ever stored the URL in the first place.

Migration path from text-field links

You don't have to rebuild to adopt this. The migration is mechanical:

  1. Add the new PageLink reference type alongside the existing text URL field. Don't delete the old one yet.
  2. Run a one-time script that walks every entry, parses the URL string, finds the matching Page by path, and writes the reference. Log unmatched URLs.
  3. Triage the unmatched list. Most will be external links (move them to ExternalLink) or links to pages that no longer exist (these were already broken — now you know).
  4. Stand up a redirect table for the 404s you found. This is also CMS-managed: a Redirect type with from string and to as a PageLink.
  5. Hide the old text URL field in the editor. Remove it after one publish cycle.

This is not Contentful-specific

The exact same model works in Sanity (references + parent self-reference), Storyblok (story links), Strapi (relations), and any headless CMS that supports references. The point is structural, not vendor-specific.
What to actually do
  • Audit every link field in your CMS. If it accepts a URL string, it is a future 404.
  • Replace internal link fields with references to Page entries. External links go in a separate type.
  • Model parent/child as a self-reference on the Page type. Compute the path; don't store it.
  • Generate the navigation menu from the tree, not from a hand-maintained list.
  • Add a CMS-managed redirect table during migration to catch the URLs you can't map.

Want this kind of judgment on your project?

I read every email within one working day. Bring a project, a quote, or a system you're stuck on.

Book a 30-min callSee the Enterprise Starter
Related guides
  • 9 min

    15 things every Contentful enterprise project gets wrong in the first 6 weeks

    The 15 production gaps every enterprise Contentful + Next.js build hits in the first six weeks — and how to close each one without burning a sprint. A pre-kickoff checklist for technical leads on a Contentful enterprise starter.

  • 13 min

    REST + GraphQL hybrids for multi-locale CMS-driven sites

    Why neither REST-only nor GraphQL-only is the right call for an enterprise multi-locale CMS site, and how to split by concern instead. Includes the circular-reference problem on full REST payloads, the bundle cost of GraphQL on the client, the decision matrix per call site, the unified fetcher, granular cache tags, the block-as-fragment pattern, locale fallback in one round trip, Live Preview survival, Server Actions for CMA writes, the Algolia Sync API exception, and the migration sequence.

  • 3 min

    CMS-driven analytics: stop paying developers for every tracking change

    How to structure your CMS so marketers can add tracking events without developer intervention. Save tens of thousands in ongoing costs.