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

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

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

  3. Add the client

    npm install @moteurio/client, then one file that makes the client for server code.

  4. Read in server components

    Entries, pages, menus and addresses arrive as JSON. Pick each field’s language with t().

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

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.