> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://contentful.com/developers/docs/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://contentful.com/_mcp/server. # Building a website with Compose in Next.js > Build a website with Compose in Next.js | Routing | Fetching content | Rendering content, metadata | Creating a new page type > **Info** > > Compose is being deprecated. It is in maintenance mode and won’t be updated with any new features. Current installations will work until the end of 2026. > **Info** > > This document describes Compose that is driven by a legacy content model. If you set up Compose after April 21st 2022, please refer to the updated [Compose documentation](/compose/what-is-compose). In this guide you will learn how to develop a website using Compose. Contentful provides an example implementation of a Help Center website in the [contentful/compose-starter-helpcenter-nextjs](https://github.com/contentful/compose-starter-helpcenter-nextjs/tree/legacy-content-model) GitHub repository. This is an application built with the [Next.js](https://nextjs.org/) framework which is powered by [React](https://reactjs.org/). ## Routing Each `page` entry has a unique `slug` text field that describes and identifies the given page. You can use `slug` values as part of page URLs in your website for a user- and SEO-friendly navigation. For example, on the Help Center website the “Introduction to Contentful” article will be available under the `/articles/introduction-to-contentful` URL. If your website supports multiple languages you may also want to include a `locale` param in the URL. See our documentation about [Localization with Contentful](/tutorials/general/setting-locales/) for more information. ## Fetching content Contentful allows you to fetch all the content needed on a given web page in one request (with certain [limits](/developers/docs/technical-limits/)). You can make a `GET` request to [Content Delivery API](/references/content-delivery-api/overview) directly or use the [JavaScript client library](https://contentful.github.io/contentful.js/contentful/7.14.8/), as in the following example: ``` import { createClient } from 'contentful'; const client = createClient({ space: CF_SPACE_ID, // ID of a Compose-compatible space to be used \ accessToken: CF_DELIVERY_ACCESS_TOKEN, // delivery API key for the space \ }); type GetPageParams = { pageContentType: string; slug: string; locale: string; }; async function getPage(params: GetPageParams) { const query = { limit: 1, include: 10, locale: params.locale, 'fields.slug': params.slug, content_type: 'page', 'fields.content.sys.contentType.sys.id': params.pageContentType, }; const { items: [page] } = await client.getEntries(query); return page || null; } ``` View the full code in [/src/lib/api.ts](https://github.com/contentful/compose-starter-helpcenter-nextjs/blob/legacy-content-model/src/lib/api.ts) You can then use the `getPage` function to fetch a given page. For example, when a user navigates to `/articles/introduction-to-contentful` URL on the Help Center website, we fetch a `Helpdesk Article` page with the `introduction-to-contentful` slug: ``` const page = await getPage({ pageContentType: 'page_help_center_article', slug: 'introduction-to-contentful', locale: 'de-DE', }); ``` ### Preview mode If you want to support previewing unpublished content on your website, create a second Contentful client: ``` const previewClient = createClient({ space: CF_SPACE_ID, // ID of a Compose-compatible space to be used accessToken: CF_PREVIEW_ACCESS_TOKEN, // preview API key for the space host: 'preview.contentful.com', }); ``` In the Help Center example we rely on a query string parameter `?preview=1` to decide which Contentful client to use for fetching content. ## Rendering content Once the correct `page` data is fetched, you can render it in your website. In the Help Center example, we traverse the `page` object recursively and use [React](https://reactjs.org/) to render individual entries that make the page. There’s a React component defined for each Content Type: ``` const ContentTypeMap = { 'page_help_center_article': HelpCenterArticle, 'component_text': Text, 'component_image': Image, 'component_video': Video, ... }; ``` View the full code in [/src/components/renderer/block-renderer.tsx](https://github.com/contentful/compose-starter-helpcenter-nextjs/blob/legacy-content-model/src/components/renderer/block-renderer.tsx) Below is an example of a simple React component rendering a `Text` Content Type. It includes rendering a RichText field as HTML using the [@contentful/rich-text-react-renderer](https://www.npmjs.com/package/@contentful/rich-text-react-renderer) package: ``` import React from 'react'; import { documentToReactComponents as renderRichText } from '@contentful/rich-text-react-renderer'; function Text(textEntry) { const { title, text } = textEntry.fields; return ( <> {title ?

{title}

: null} {renderRichText(text)} ); } ``` ## Rendering metadata In addition to the `page` content, we can also use the metadata stored in the `title` and `seo` fields. Below is an example of how to use those values on a web page: ``` const seo = page.fields.seo.fields; const { description = '', keywords = [], title = page.fields.title} = seo; const robots = [ seo.no_index === true ? 'noindex' : undefined, seo.no_follow === true ? 'nofollow' : undefined, ].filter((x) => x !== undefined); ... {title} {robots.length > 0 && } {description.trim() !== '' && ( )} {keywords.length > 0 && } ``` View the full code in [/src/components/page-head.tsx](https://github.com/contentful/compose-starter-helpcenter-nextjs/blob/legacy-content-model/src/components/page-head.tsx) ## Create a new page type and render it on the frontend Let's wrap up all the information and see an end-to-end use case where we start publishing pages of a different type. By this time, the following steps might look familiar. In our case we want to add tutorials to our Help Center, so the pages will display the walk-trough text, a difficulty indicator and, if available, a link to the repo where users can find the tutorial material. ### Step 1: Create the new page type Open your target space in the [Contentful web app](https://app.contentful.com/). Then navigate to the "Content Model" tab and click "Add content type". Enter your page type name, for us it's "Tutorial". ![Create content type](https://fdr-prod-docs-files-public.s3.us-east-1.amazonaws.com/contentful.docs.buildwithfern.com/7cc10f884aab3fdae6b0908224ed09126b25c630bc7a5621eb078961975cacb7/docs/assets/images/compose/compose-dialog-create-page-type.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNFOCF7G4B%2F20260929%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260929T234017Z&X-Amz-Expires=604800&X-Amz-Signature=87ad0ee52b2f52d0014ffa0099162716a99deb547fd78cc72156f735fe31ce8b&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) Now define the fields of your new page type. ![Create content type](https://fdr-prod-docs-files-public.s3.us-east-1.amazonaws.com/contentful.docs.buildwithfern.com/480a0b15f51b28dbff698ddaf8073786ea04ce9c9f51bdef832a294d589dd899/docs/assets/images/compose/compose-tutorial-content-type.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNFOCF7G4B%2F20260929%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260929T234017Z&X-Amz-Expires=604800&X-Amz-Signature=9a2c6c6c2901039bc4afc3520978f0f3487204007647bd14cd13886dd3b353c5&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) **Note:** One of the fields needs to have a short text (also `Symbol` type) field with field ID "name". In our page type the field `Title` has field ID "name". ![Tutorial title field](https://fdr-prod-docs-files-public.s3.us-east-1.amazonaws.com/contentful.docs.buildwithfern.com/ec8f6a541e5a70b4c814e89c0676352ddddfbbd7605b0649255cc4119dfb891c/docs/assets/images/compose/compose-tutorial-name-field.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNFOCF7G4B%2F20260929%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260929T234017Z&X-Amz-Expires=604800&X-Amz-Signature=7350f3673c654de3ae029d07993fad9e96bf857b1d85608023d101364ef0ecf5&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) ### Step 2: Link the new page type to "Compose: Page" Change the validation of the "Content" field of the "Compose: Page" content type. Make it accept your new page type. ![Add tutorial as content](https://fdr-prod-docs-files-public.s3.us-east-1.amazonaws.com/contentful.docs.buildwithfern.com/2d4e32a86eb8b72d4b967f38ac312f27ab479ac5b8ea6e97f5666e78aa5f8732/docs/assets/images/compose/compose-page-definition-with-tutorial.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNFOCF7G4B%2F20260929%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260929T234017Z&X-Amz-Expires=604800&X-Amz-Signature=e382eca18484e84e35e13d69f4408de2448b155f552b4f67376a78067bb25e1d&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) ### Step 3: Create and edit a new page It's time to get creative, open Compose and create a page with the new type. Fill in the fields with the content you want to serve in your website. Then publish the page. ![New tutorial content](https://fdr-prod-docs-files-public.s3.us-east-1.amazonaws.com/contentful.docs.buildwithfern.com/434aaacb3e5b39634a4d340978000013c9f4709a77bb3d577b13db204a8f71f0/docs/assets/images/compose/compose-tutorial-filled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNFOCF7G4B%2F20260929%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260929T234017Z&X-Amz-Expires=604800&X-Amz-Signature=3c84fca22f24daa425597070796ad2148b14f99d0f7763ef69877a7e8dc970a6&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) ### Step 4: Render your content To visualize our data, we need to add a new component to our Next.js app: ``` /* new file: src/pages/[locale]/tutorials/[slug].tsx */ import React from 'react'; import { TypePage } from 'lib/types'; import { withLocale } from 'lib/translations/locales'; import { isPreviewEnabled } from '../../../lib/preview'; import { getPage } from 'lib/api'; import { PageContentTypes } from '../../../lib/constants'; import ErrorPage from 'next/error'; import { PageHead } from '../../../components/page-head'; import { isRichText, renderRichText } from '../../../lib/rich-text/render'; /* The new Tutorial component, it renders the data fetched by getServerSideProps See https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering */ const Tutorial = ({ page }: { page: TypePage }) => { if (!page) { //getServerSideProps did not find the page return } const { difficulty, timeRequired, explanation, repoLink, } = page.fields.content.fields /* Visualize the data as you see fit, sky is the limit */ return ( <> {/* We reuse the existing code for title and seo metadata rendering in the header */}
Tutorial

{page.fields.title} {/*Turn difficulty into a 3-dots rating*/}

{timeRequired} minute{timeRequired !== 1 ? 's' : ''}
{/*Display a link to the code repo if any*/} {repoLink ? : null }
{isRichText(explanation) ? renderRichText(explanation) : explanation}
) } export default Tutorial export const getServerSideProps = withLocale(async (locale, { params, query }) => { const slug = String(params.slug); const preview = isPreviewEnabled(query); const pageContentType = 'tutorial'; /* <- the id of the new page type */ const page = await getPage({ slug, preview, locale, pageContentType }) return { props: { page }, }; }); /* Cosmetic component */ const TimeIcon = () => ( ) /* Cosmetic component */ const BeakerIcon = () => ( ) /* Cosmetic component */ const ColorRating = (props: { value: number }) => { const style = { backgroundColor: ['#2dce89', '#11cdef', '#fb6340'][props.value - 1], width: '0.6em', height: '0.6em', } return <>{Array(props.value).fill()} } ``` ### Enjoy your content ![New tutorial web page](https://fdr-prod-docs-files-public.s3.us-east-1.amazonaws.com/contentful.docs.buildwithfern.com/130e9ea1d01177468c47e82f665ffc5f029bcba9f039fb4ab0f17bb1dbb35239/docs/assets/images/compose/compose-new-tutorial.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNFOCF7G4B%2F20260929%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260929T234017Z&X-Amz-Expires=604800&X-Amz-Signature=6664f9b00e392e36e78aa77bfe20d5e1b1bcb1d227483e7d3503999bc8ed24e7&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) > Build a website with Compose in Next.js | Routing | Fetching content | Rendering content, metadata | Creating a new page type