Build one Article from editor to browser#
This walkthrough creates a small but realistic content flow. At the end, an editor will publish an Article in Contoprix and a Next.js route will read it by slug.
Keep the first model intentionally small. A working title and slug is more useful than a large schema that nobody has delivered to the frontend yet.
What you will create#
Website + English language
↓
Article content type
↓
One Article entry: hello-contoprix
↓
Published delivery API response
↓
/blog/hello-contoprix in your Next.js app1. Choose the website and language#
In Contoprix Admin, switch to the tenant and website that this frontend will use. A website is important because delivery credentials, languages, content entries, and media are all scoped to it.
Make sure the website has an enabled default language. For this guide, use:
| Setting | Value |
|---|---|
| Language name | English |
| Language code | en |
| Default | Yes |
If the website already has English as its default language, keep it. Do not create a second English entry just for this tutorial.
2. Create an Article content type#
Open Content Types and create a type with these values:
| Setting | Value | Why |
|---|---|---|
| Name | Article | Clear label for editors |
| Code | article | Stable value used in SDK calls |
| Kind | Collection | Allows many articles per website and language |
| Drafts | Enabled | Lets editors save work before publishing |
| Versioning | Enabled | Keeps an edit history |
| Localization | Enable only if this content will be translated | Lets entries be created outside the website's default language |
The code is part of the integration contract. Your application will call client.content.getBySlug("article", ...), so keep the code stable after frontend work starts.
Add these fields in this order:
| Field name | Field code | Field type | Required | Example |
|---|---|---|---|---|
| Title | title | Text | Yes | Hello, Contoprix |
| Slug | slug | Slug | Yes | hello-contoprix |
| Summary | summary | Text Area | No | Our first published article. |
| Body | body | Rich Text | No | Article body copy |
Important
Use the exact field codes shown in your frontend mapper. The API puts them inside entry.data, so a field code change is a frontend change too.
3. Create the first entry#
Open Content Entries, choose the Article type, the website, and English. Create an entry with this content:
Title: Hello, Contoprix
Slug: hello-contoprix
Summary: Our first published article.
Body: This is the first Article delivered to our website.Save the entry, review it, and publish it. The slug is normalized for delivery, so Hello-Contoprix is stored and looked up as hello-contoprix.
If publishing is blocked, check the required fields first. If a workflow is configured for the entry, it may need approval before a user without workflow-bypass permission can publish it.
4. Create a delivery credential#
In the website's API-client or credential settings, create a delivery credential that has delivery read access for this website. Copy the delivery key once and place it in a local environment file.
CONTOPRIX_BASE_URL=https://cms.example.com
CONTOPRIX_DELIVERY_KEY=replace-with-your-delivery-keyUse the CMS API origin for CONTOPRIX_BASE_URL; do not add /api. Keep this file out of version control and do not use a NEXT_PUBLIC_ prefix for the key.
5. Create one shared client#
Create a small server-side client module:
import { ContoprixClient } from "@contoprix/client";
export const client = new ContoprixClient({
baseUrl: process.env.CONTOPRIX_BASE_URL!,
auth: {
type: "deliveryKey",
deliveryKey: process.env.CONTOPRIX_DELIVERY_KEY!,
},
languageCode: "en",
});The delivery key identifies the website. You do not manually pass a tenant ID or website ID in every SDK request.
6. Fetch the Article by slug#
The SDK response has metadata on the entry and your CMS fields under data. Map it before rendering:
import { client } from "./client";
type ArticleData = {
title?: string;
summary?: string;
body?: unknown;
};
export async function getArticle(slug: string) {
const entry = await client.content.getBySlug("article", slug, {
languageCode: "en",
});
const data = entry.data as ArticleData;
if (!data.title) {
throw new Error(`Article ${entry.id} is missing its required title.`);
}
return {
id: entry.id,
slug: entry.slug ?? slug,
title: data.title,
summary: data.summary ?? "",
body: data.body,
publishedAt: entry.publishedAt,
};
}The cast belongs in this data module, not inside each React component. Later, replace this small local type with generated types if your team uses the CLI.
7. Render it in a Next.js route#
import { getArticle } from "@/lib/contoprix/articles";
export default async function ArticlePage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const article = await getArticle(slug);
return (
<article>
<h1>{article.title}</h1>
{article.summary ? <p>{article.summary}</p> : null}
</article>
);
}Open /blog/hello-contoprix. If the page renders the title and summary, the entire first delivery flow is working.
Troubleshoot the first request#
| What you see | Likely cause | What to do |
|---|---|---|
401 or 403 | Missing, incorrect, or wrong-website delivery key | Check the environment values and delivery-read access. |
404 | Wrong content-type code, slug, language, or unpublished entry | Confirm article, hello-contoprix, en, and the entry's published state. |
entry.data is missing a field | Field is optional, has a different code, or was not filled in | Check the Content Type field code and entry values. |
| The browser shows a secret | The client module was imported into browser code | Keep the SDK client and delivery key in server-only modules. |
Continue building safely#
Add only one feature at a time:
- Add a hero image using a media field.
- Add an
authorcontent type and link it with a relation. - Add another language and create a separate Article entry for that language.
- Add a list route with
client.content.list(). - Configure preview or Visual Builder only after published delivery works.
Read Content Types before changing the Article schema, and Content Entries for draft, version, and publication behavior.