Handling References to Unpublished Entries in Published Pages
Part of Draft State Management, this guide covers a failure that appears only in production: a published page references an entry that is still a draft, archived or deleted, and the delivery API returns the page with that reference missing, so a component that expects an object receives null, an empty array or a bare link stub.
Every headless CMS allows references to point at entries in any state, because editors need to build pages before every linked piece is ready. Preview hides the problem, since preview APIs resolve drafts. The published page only breaks after publishing, often on a busy launch day when a campaign page goes live before its hero banner or its featured product entry.
The Problem
A retail team builds a spring campaign page in Contentful. The page entry references a hero banner, three product teasers and a promotional code block. The page is approved and scheduled for 08:00. The hero banner, owned by the design team, is still in draft at 08:00 because its final image is pending. When the page publishes, Contentful’s delivery API returns the page with the hero link unresolved: in the REST API, the link remains in fields.hero but no matching entry appears in includes; in the GraphQL API, hero is null. The React component destructures hero.image.url, throws, and the page renders the framework’s error screen for the first hours of the campaign.
The same mechanism causes quieter bugs: a listing that skips archived entries but shows a gap in the grid, a related-articles block that renders three cards instead of four, and author bylines that disappear when an author profile is unpublished for editing.
How Each CMS Represents a Missing Reference
Platforms differ in how they signal the problem, which is why a single defensive pattern in the fetch layer is worth writing once.
The shapes fall into two groups. Some APIs keep a stub, such as a link object or a uuid string, that shows where content should have been. Others drop the value or return null. A robust fetch layer normalizes both into one explicit representation, so components can decide what to render without knowing which CMS is behind them.
Implementation
The approach has three parts: model the possibility in types, normalize API responses into those types, and make every component render something sensible for the missing case.
// lib/refs.ts
export type Ref<T> = { status: "resolved"; value: T } | { status: "missing"; id: string | null };
// Contentful REST: resolve links against the includes collection.
interface Link {
sys: { type: "Link"; linkType: "Entry" | "Asset"; id: string };
}
interface Includes {
Entry?: Array<{ sys: { id: string }; fields: Record<string, unknown> }>;
}
export function resolveEntryLink<T>(link: Link | undefined, includes: Includes, map: (fields: Record<string, unknown>) => T): Ref<T> {
if (!link) return { status: "missing", id: null };
const entry = includes.Entry?.find((e) => e.sys.id === link.sys.id);
return entry ? { status: "resolved", value: map(entry.fields) } : { status: "missing", id: link.sys.id };
}
export function resolveList<T>(links: Link[] | undefined, includes: Includes, map: (fields: Record<string, unknown>) => T): { items: T[]; missing: string[] } {
const items: T[] = [];
const missing: string[] = [];
for (const link of links ?? []) {
const r = resolveEntryLink(link, includes, map);
if (r.status === "resolved") items.push(r.value);
else if (r.id) missing.push(r.id);
}
return { items, missing };
}
// components/Hero.tsx
import type { Ref } from "@/lib/refs";
interface HeroData {
heading: string;
imageUrl: string;
imageAlt: string;
}
export function Hero({ hero, fallbackHeading }: { hero: Ref<HeroData>; fallbackHeading: string }): JSX.Element {
if (hero.status === "missing") {
// Render a neutral header rather than crashing or leaving a hole.
return (
<header className="hero hero--plain">
<h1>{fallbackHeading}</h1>
</header>
);
}
const { heading, imageUrl, imageAlt } = hero.value;
return (
<header className="hero">
<img src={imageUrl} alt={imageAlt} width={1600} height={900} />
<h1>{heading}</h1>
</header>
);
}
The Ref<T> type forces every consumer to handle the missing case at compile time, which is the main benefit: the next developer who adds a component cannot forget. Lists return both the resolved items and the missing ids, so a grid can render the items it has and log the gaps.
Logging the gaps is the second half. Report every missing reference on published pages to your monitoring with the parent entry id, the field and the missing id. The content team then gets a list of pages that published with incomplete references, which they can fix by publishing the referenced entries.
Configuration Reference
| Setting | Where | Purpose |
|---|---|---|
| Required reference fields | CMS content model | Blocks publishing of the parent when the field is empty, not when the target is unpublished. |
| Reference validation on publish | CMS workflow app or plugin | Prevents publishing a parent whose references are drafts. |
include depth |
Contentful REST | Must cover the referenced depth, or resolvable links look missing. |
resolve_relations |
Storyblok | Lists which relation fields to resolve into objects. |
populate |
Strapi | Controls which relations are returned at all. |
| Missing-reference metric | monitoring | Counts gaps per page type after each publish. |
Note the difference between the first two rows. A required field in the CMS only checks that a reference exists, not that its target is published. Blocking publication of a parent whose references are still drafts needs a workflow rule, such as Contentful’s reference validation in a custom app or a Sanity document action that checks referenced documents before publishing.
Gotchas & Edge Cases
- Include depth mistaken for unpublished content. Contentful resolves includes to a maximum depth. A reference deeper than the requested
includevalue looks exactly like a missing one. Set the depth to what the page renders and treat misses at the deepest level with suspicion. - Assets are references too. An image asset that is unpublished or deleted leaves the entry with a dangling asset link. Handle assets with the same
Refpattern and render a placeholder with explicit dimensions to avoid layout shift. - Caching the broken page. An ISR page rendered with a missing reference stays broken in the cache after the reference is published, because publishing the child does not invalidate the parent. Tag parent fetches with every referenced id, including missing ones, so a publish of the child revalidates the parent.
- Localized references. A reference can be resolved in one locale and missing in another when the target entry has no translation and no fallback. Run missing-reference checks per locale.
- Preview hides everything. Because preview resolves drafts, add a preview banner warning that lists referenced entries that are not yet published, so editors see the risk before they publish.
Verifying the Result
Create a test page in a staging environment that references one published and one draft entry of each referenced type, publish only the page, and render it through the delivery API. Every component should render its fallback for the draft reference, no error should reach the error boundary, and the monitoring should record one missing reference per draft target. Then publish the draft targets and confirm the parent page regenerates without a manual purge, which proves the dependency tags work.
Rollout Checklist
- Introduce the
Ref<T>type and a resolver per CMS in the fetch layer. - Update components that consume references to render fallbacks for the missing case.
- Tag parent fetches with the ids of all referenced entries, resolved or not.
- Report missing references on published pages to monitoring and to the content team.
- Add a pre-publish check in the CMS that warns about draft references.
- Show unpublished references in the preview banner.
Frequently Asked Questions
Should a page with a missing required reference return 404 instead?
Rarely. Most pages are still useful without one referenced block, and a 404 on a campaign page is worse than a plain header. Reserve 404s for references that define the page, such as the product entry behind a product detail page.
Can the CMS prevent this entirely?
Workflow rules can block publishing a parent while references are drafts, and some platforms support publishing a parent together with its references as a release. Both help, but references can still break later when a child is unpublished or deleted, so the frontend must handle missing references regardless.
How do I find existing pages with broken references?
Crawl the published site, or better, query the delivery API for every page entry and run the same resolver used in rendering, collecting missing ids. A nightly job that produces this report keeps the content team informed without waiting for a reader to notice.
Should editors be warned before publishing a page with draft references?
Yes, and it is the cheapest fix of all. A CMS sidebar app or document action that lists unpublished references at publish time catches most cases before they reach readers, and it teaches editors which entries their pages depend on.
Does GraphQL make this easier?
It makes the missing case explicit, since the field is null and schema types can mark it nullable, which generated TypeScript types then enforce. The rendering and caching problems are the same as with REST.