Frameworks

Next.js

Render published articles in the App Router with a server component.

PeopleTide Content · HTTP integration guides

1. Prepare your project.

Use a Next.js App Router project with Node 22+. Save the shared helper as lib/delivery.js and set CONTENT_ORIGIN, CONTENT_ORGANIZATION_ID, and CONTENT_SITE_ID in .env.local. Keep these calls in the server component.

2. Add the shared delivery helper.

Copy this helper into the path named above. It follows a single published release without exposing authoring credentials.

JavaScript
// delivery.js — no SDK or API key required. Node 22+ or a modern browser.
export async function getArticle({ origin, organization, site, slug }) {
  const base = new URL(origin);
  if (base.protocol !== 'https:' && !['localhost', '127.0.0.1'].includes(base.hostname)) {
    throw new Error('Use an HTTPS Content origin');
  }
  if (![organization, site].every(id => /^[a-f0-9-]{36}$/.test(id))) {
    throw new Error('Set your organization and site UUIDs');
  }
  const prefix = '/delivery/v1/' + organization + '/' + site + '/';
  async function read(path) {
    const url = new URL(path, base);
    if (url.origin !== base.origin || !url.pathname.startsWith(prefix) || url.search || url.hash) {
      throw new Error('Unexpected delivery URL');
    }
    const response = await fetch(url, {
      credentials: 'omit', cache: 'no-store', redirect: 'error',
      signal: AbortSignal.timeout(8000),
    });
    if (!response.ok) throw new Error('Content delivery returned ' + response.status);
    return response.json();
  }
  const current = await read(prefix + 'current.json');
  const manifest = await read(current.url);
  const entry = manifest.entries.find(item => item.type === 'article' && item.slug === slug);
  if (!entry) return null;
  const document = await read(entry.url);
  return { releaseId: current.releaseId, document };
}

Connect and render.

Publish an article of type article with slug welcome and a body field before running the example. Split the labelled files into the paths shown.

TSX
// app/articles/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { getArticle } from '../../../lib/delivery';
export const dynamic = 'force-dynamic';
export default async function Article({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const result = await getArticle({ origin: process.env.CONTENT_ORIGIN,
    organization: process.env.CONTENT_ORGANIZATION_ID, site: process.env.CONTENT_SITE_ID, slug });
  if (!result) notFound();
  return <article><h1>{result.document.title}</h1>
    <p style={{ whiteSpace: 'pre-wrap' }}>{result.document.fields.body}</p></article>;
}
// app/articles/[slug]/error.tsx
'use client';
export default function ArticleError({ reset }: { reset: () => void }) {
  return <section><h1>Content temporarily unavailable</h1><button onClick={reset}>Try again</button></section>;
}

Choose when content refreshes.

This example uses dynamic rendering and no-store requests. The Content discovery cache may still take up to 30 seconds to show a new release. If you adopt static generation or revalidation, document the additional freshness window.

Verify success and failure.

Open /articles/welcome, then an unknown slug. Confirm the article and a 404 respectively. Simulate a delivery outage and check the error boundary. A saved draft must not change the rendered approved article.