Next.js
Your client edits in the Studio. Next.js serves it.
The client’s editor fixes a price and publishes. Your site gets a signed call, and that page is fresh for the next visitor. You read entries in server components, with a key that never reaches the browser.
the journal, in Next.js
// app/[locale]/journal/page.tsx
import { t } from '@moteurio/client';
import { moteur } from '@/lib/moteur';
export default async function Journal({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const articles = (await moteur
.channel('website')
.entries.list('article', { limit: 10, sort: '-publishedAt', resolveUrl: true })) as Article[];
return (
<ul>
{articles.map(a => (
<li key={a.id}><a href={a.resolvedUrl}>{t(a.data.title, locale)}</a></li>
))}
</ul>
);
}Setup
Five steps to a client’s site
Choose what the site reads
In Setup › API channels, the site channel is already there: pick the models and fields it shows. It reads published content only.
Make its key
A read-only key for that channel, limited to the client’s domains if you like. Put it in MOTEUR_KEY, on the server.
Add the client
npm install @moteurio/client, then one file that makes the client for server code.
Read in server components
Entries, pages, menus and addresses arrive as JSON. Pick each field’s language with t().
Refresh on publish
Point a webhook at a route of yours, or paste Vercel’s deploy hook to rebuild instead.
One file
The client, made once
Import it from server components and route handlers only. The key stays on the server.
- The same file works with your own server: change baseURL
- Types for every model and block come from moteur types
lib/moteur.ts
// lib/moteur.ts: imported by server code only, so the key stays there
import { createMoteurPublicClient } from '@moteurio/client';
export const moteur = createMoteurPublicClient({
baseURL: 'https://api.moteur.io',
auth: { type: 'apiKey', apiKey: process.env.MOTEUR_KEY!, projectId: 'client-site' },
});Up close
What a client site needs
Turn a card over for the code.
Addresses
Any address, in any language
The Studio gives each page an address per language. One catch-all route asks what lives there: a page, an entry, or a redirect to follow.
- /fr/a-propos and /en/about, one page
- Alternates for hreflang come with it
app/[...path]/page.tsx
// app/[...path]/page.tsx: any address the Studio gave a page import { notFound, permanentRedirect, redirect } from 'next/navigation'; import { moteur } from '@/lib/moteur'; export default async function Page({ params }: { params: Promise<{ path: string[] }> }) { const { path } = await params; const found = await moteur.site.resolve('/' + path.join('/')); if (!found) notFound(); if (found.match === 'redirect') found.status === 301 ? permanentRedirect(found.to) : redirect(found.to); // found.nodeId, found.entryId, found.locale and its alternates in every language }Redirects
Old addresses keep working
When a live page moves, Moteur adds the redirect. Hand the list to Next.js when it builds.
- 301s made for you, chains collapsed
- Loops refused before they’re saved
next.config.mjs
// next.config.mjs import { createMoteurPublicClient } from '@moteurio/client'; const moteur = createMoteurPublicClient({ baseURL: 'https://api.moteur.io', auth: { type: 'apiKey', apiKey: process.env.MOTEUR_KEY, projectId: 'client-site' }, }); export default { async redirects() { const list = await moteur.site.redirects(); return list.map(r => ({ source: r.from, destination: r.to, permanent: r.status === 301 })); }, };Images
Images in every size
Crops and widths come from the image’s address. imageAttributes writes the src, the srcset, the size and the alt text in the visitor’s language.
- Eight crop ratios and a focal point
- WebP or AVIF, never wider than the original
components/Cover.tsx
import { imageAttributes } from '@moteurio/client'; export function Cover({ cover, locale }: { cover: ResolvedImage; locale: string }) { return ( <img {...imageAttributes(cover, { ratio: '16:9', sizes: '(min-width: 60rem) 60rem, 100vw', locale })} /> ); }Previews
Drafts on the real site
The Studio opens your site with a short-lived token in the address. Read with it, and the page shows the draft.
- One entry or one channel, for half an hour
- Without a token, what’s live
app/[locale]/journal/[id]/page.tsx
// app/[locale]/journal/[id]/page.tsx import { moteur } from '@/lib/moteur'; export default async function Article({ params, searchParams, }: { params: Promise<{ id: string }>; searchParams: Promise<{ preview?: string }>; }) { const [{ id }, { preview }] = await Promise.all([params, searchParams]); // The Studio opens your site with ?preview=… on the address. const reader = preview ? moteur.withPreview(preview) : moteur; const article = await reader.channel('website').entries.get('article', id); // … }
Webhooks
Fresh when something goes live
Add a webhook in Setup › Webhooks for entries published, pages, menus and redirects. Each one is signed: check the signature, then refresh what you need.
- The event name comes in X-Moteur-Event
- Retried if your site is down
app/api/moteur/route.ts
// app/api/moteur/route.ts: Setup › Webhooks points here
import { createHmac, timingSafeEqual } from 'node:crypto';
import { revalidatePath } from 'next/cache';
export async function POST(request: Request) {
const body = await request.text();
const expected =
'sha256=' +
createHmac('sha256', process.env.MOTEUR_WEBHOOK_SECRET!).update(body).digest('hex');
const given = request.headers.get('x-moteur-signature') ?? '';
if (given.length !== expected.length || !timingSafeEqual(Buffer.from(given), Buffer.from(expected)))
return new Response('Bad signature', { status: 401 });
revalidatePath('/', 'layout');
return Response.json({ ok: true });
}Questions about Next.js
An agency developer asksIs there a Next.js starter?
MoteurComing. A starter, and an @moteurio/react package that renders blocks for you, aren’t published yet. Until then, @moteurio/client and the code on this page are all you need.
An agency developer asksApp Router or Pages Router?
MoteurEither. The client is plain TypeScript that runs anywhere Node does. This page shows the App Router.
An agency developer asksDoes Next.js cache what the client reads?
MoteurThe client doesn’t use Next’s fetch, so Next doesn’t cache its answers on its own. Render pages statically and refresh them with the webhook, or set revalidate on a route.
An agency owner asksCan we host on Vercel?
MoteurYes. Paste Vercel’s deploy hook to rebuild on publish, or use the webhook above to refresh without a build.
Setup › Webhooks › Rebuild the site
{
"name": "Rebuild the client site",
"url": "https://api.vercel.com/v1/integrations/deploy/…",
"kind": "deploy",
"batchSeconds": 30
}Build your next client site with Next.js and Moteur.
Start for free, make a key for the site channel, and read your first entry.