> 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".

Now define the fields of your new page type.

**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".

### 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.

### 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.

### 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*/}