Astro

Your client edits in the Studio. Astro builds the site.

The client’s editor publishes the spring menu. Half a minute later, your host starts one build with it. You read what each page needs in its frontmatter, with one client and a key that only reads.

the journal, in Astro

---
// src/pages/journal/index.astro
import { moteur } from '../../lib/moteur';

const articles = (await moteur
  .channel('website')
  .entries.list('article', { limit: 10, sort: '-publishedAt', resolveUrl: true })) as Article[];
---

<ul>
  {articles.map(a => <li><a href={a.resolvedUrl}>{a.data.title.en}</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.

  3. Add the client

    npm install @moteurio/client, then one file that makes the client for the whole site.

  4. Read in your pages

    In frontmatter or getStaticPaths. Each page gets entries, pages, menus and addresses as JSON.

  5. Rebuild on publish

    Paste your host’s build hook in Setup › Webhooks. Vercel, Netlify and Cloudflare Pages all work.

One file

The client, made once

Every page imports it. The key stays in your build, never in the browser.

  • The same file works with your own server: change baseURL
  • Types for every model and block come from moteur types

src/lib/moteur.ts

// src/lib/moteur.ts
import { createMoteurPublicClient } from '@moteurio/client';

export const moteur = createMoteurPublicClient({
  baseURL: 'https://api.moteur.io',
  auth: { type: 'apiKey', apiKey: import.meta.env.MOTEUR_KEY, projectId: 'client-site' },
});

Up close

What a client site needs

Turn a card over for the code.

  • Addresses

    Every page, in every language

    The Studio gives each page an address per language. getStaticPaths builds them all, and each knows its alternates for hreflang.

    • /fr/a-propos and /en/about, one page
    • Breadcrumbs and menus in the same language

    src/pages/[...path].astro

    ---
    // src/pages/[...path].astro: every page, in every language
    import { moteur } from '../lib/moteur';
    
    export async function getStaticPaths() {
      const urls = (await moteur.site.urls()) as Address[];
      return urls.map(u => ({
        params: { path: u.url.replace(/^\/|\/$/g, '') || undefined },
        props: u,
      }));
    }
    
    const { nodeId, entryId, locale, alternates } = Astro.props;
    ---
  • Redirects

    Old addresses keep working

    When a live page moves, Moteur adds the redirect. Write them where your host reads them, before each build.

    • 301s made for you, chains collapsed
    • Loops refused before they’re saved

    scripts/redirects.ts

    // scripts/redirects.ts: run before astro build
    import { writeFile } from 'node:fs/promises';
    import { moteur } from '../src/lib/moteur';
    
    // Netlify and Cloudflare Pages read public/_redirects.
    const redirects = await moteur.site.redirects();
    await writeFile(
      'public/_redirects',
      redirects.map(r => `${r.from} ${r.to} ${r.status}`).join('\n'),
    );
  • Images

    Images in every size

    Crops and widths come from the image’s address. imageAttributes writes the src, the srcset for the widths the server makes, 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

    src/components/Cover.astro

    ---
    import { imageAttributes } from '@moteurio/client';
    const { cover, locale } = Astro.props;
    ---
    
    <img {...imageAttributes(cover, { ratio: '16:9', sizes: '(min-width: 60rem) 60rem, 100vw', locale })} />
  • Forms

    Forms in the client’s languages

    Ask for a token when the form shows, then send it. Answers come back checked, with what to fix in the visitor’s language.

    • The server checks every answer
    • Spam goes to its own folder, quietly

    src/components/Contact.astro

    <script>
      import { createMoteurPublicClient } from '@moteurio/client';
    
      const moteur = createMoteurPublicClient({
        baseURL: 'https://api.moteur.io',
        auth: { type: 'apiKey', apiKey: import.meta.env.PUBLIC_MOTEUR_FORMS_KEY, projectId: 'client-site' },
      });
      const form = document.querySelector('form')!;
      const token = await moteur.forms.token('contact');
    
      form.addEventListener('submit', async event => {
        event.preventDefault();
        const answers = Object.fromEntries(new FormData(form));
        const result = await moteur.forms.submit('contact', answers, { locale: 'fr', token });
        // ok, or the answers to fix, each in French, or the captcha to answer again
      });
    </script>

Previews

Previews of what isn’t live yet

The Studio opens the client’s site with a short-lived token in the address. A static page can’t read it, so add one route that renders on request, and make it the project’s preview address in Setup › Project. Everything else stays static.

  • The token reads drafts of one entry or one channel, for half an hour
  • Needs an Astro adapter for your host

src/pages/preview/journal/[id].astro

---
// Under the project's preview address, rendered on request
export const prerender = false;
import { createPreviewSession } from '@moteurio/client';
import { moteur } from '../../../lib/moteur';

// The Studio opens your site with ?preview=… on the address.
const { token } = createPreviewSession({ search: Astro.url.search });
const reader = token ? moteur.withPreview(token) : moteur;
const article = await reader.channel('website').entries.get('article', Astro.params.id!);
---

Questions about Astro

An agency developer asksIs there an Astro starter?

MoteurComing. A starter, and an @moteurio/astro 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 asksCan I use Astro’s content collections?

MoteurNot through a Moteur loader yet. Read in frontmatter or getStaticPaths, as above. Each answer is plain JSON.

An agency owner asksDoes the site rebuild for every save?

MoteurNo. Only when something goes live: an entry, a page, a menu, a redirect or a file. Moteur waits half a minute for the rest, then asks for one build.

An agency developer asksCan the client’s site be fully static?

MoteurYes, all but one preview route. If the client doesn’t need to see drafts on the site itself, nothing has to render on request.

Build your next client site with Astro and Moteur.

Start for free, make a key for the site channel, and read your first entry.