---
title: "Strapi Integration"
description: "Read Strapi 5 content with typed server queries, serve generated image variants, and invalidate cached pages after an editor publishes."
canonical_url: "https://mcp.farmjs.dev/docs/integrations/strapi"
markdown_url: "https://mcp.farmjs.dev/docs/integrations/strapi.md"
last_updated: "2026-10-20"
x_farming_labs_generated_preamble: true
---

# Strapi Integration
URL: /docs/integrations/strapi
LLM index: /llms.txt
Description: Read Strapi 5 content with typed server queries, serve generated image variants, and invalidate cached pages after an editor publishes.

# Strapi Integration

Read Strapi 5 content with typed server queries, serve the fixed image variants generated by its
media library, and invalidate cached pages after an editor publishes.

## Configure Strapi

```bash
pnpm add @farm.js/strapi @strapi/client
```

```ts
// farm.config.ts
import { defineConfig } from "@farm.js/core";
import { strapi } from "@farm.js/strapi";

export default defineConfig({
  integrations: {
    cms: strapi(),
  },
});
```

With no options, the integration reads its configuration from the environment.

| Variable                | Purpose                                                         |
| ----------------------- | --------------------------------------------------------------- |
| `STRAPI_API_URL`        | Strapi Content API base URL, including `/api`.                  |
| `STRAPI_MEDIA_URL`      | Optional public media origin. Defaults to the API URL's origin. |
| `STRAPI_API_TOKEN`      | Optional server-only API token for non-public content.          |
| `STRAPI_WEBHOOK_SECRET` | Required once webhook invalidation is configured.               |

A missing API URL fails while the config loads, with a message naming `STRAPI_API_URL`.

## Choose client ownership

The application can own the official Strapi client. Build it once, use it from server queries, and
pass the same object to the integration so Farm does not construct a second client.

```ts
// src/lib/cms.server.ts
import {
  createStrapiClient,
  createStrapiCollection,
  resolveStrapiConfig,
  type StrapiDocument,
} from "@farm.js/strapi";

interface Article extends StrapiDocument {
  title: string;
  slug: string;
}

export const cms = createStrapiClient(resolveStrapiConfig({}));
export const articles = createStrapiCollection<Article>(cms, "articles");
```

```ts
// farm.config.ts
integrations: {
  cms: strapi({ instance: cms }),
}
```

Pass any configured `StrapiClient` through `instance` for official client options the integration
does not expose.

## Read content

Wrap collection reads in `createServerQuery`. Results are cached under the query key and can be
invalidated by that key later.

```ts
// src/lib/posts.ts
import { createServerQuery } from "@farm.js/core";
import { z } from "zod";
import { articles } from "./cms.server";

const article = z.object({
  documentId: z.string(),
  title: z.string(),
  slug: z.string(),
});

export const postsQuery = createServerQuery({
  output: z.array(article),
  key: () => ["strapi", "articles"],
  staleTime: "5m",
  handler: () =>
    articles.find({
      fields: ["title", "slug"],
      populate: ["cover"],
      sort: ["publishedAt:desc"],
      status: "published",
    }),
});
```

`createStrapiCollection<T>` accepts the official client's `populate`, `fields`, `filters`, `sort`,
`pagination`, `locale`, and `status` query options and unwraps the REST `data` envelope. Its generic
describes what the application expects, while the server query's `output` validates what permissions
and population actually returned.

## Serve images

Strapi generates a fixed set of sizes at upload time. `getStrapiImageProps` selects the smallest
available image at least as wide as requested, falls back to the largest without inventing an
upscaled URL, and builds a native `srcset` from the variants present on that asset.

```tsx
import { getStrapiImageProps, type StrapiMediaAsset } from "@farm.js/strapi";

export function Cover({ asset }: { asset: StrapiMediaAsset }) {
  return (
    <img
      {...getStrapiImageProps(asset, {
        mediaUrl: process.env.STRAPI_MEDIA_URL ?? new URL(process.env.STRAPI_API_URL!).origin,
        width: 800,
        sizes: "(max-width: 800px) 100vw, 800px",
      })}
    />
  );
}
```

Absolute upload-provider URLs are preserved. Relative URLs are resolved against `mediaUrl`, which
can differ from the Content API host.

## Invalidate on publish

Strapi can send a fixed header with each webhook. The integration verifies that shared secret in
constant time before parsing the body, then asks the application which cache entries the event
affects.

```ts
strapi({
  instance: cms,
  webhook: {
    onChange(payload) {
      if (payload.model !== "article") return;
      const entry = payload.entry as { documentId?: string; slug?: string };
      return {
        keys: [
          ["strapi", "articles"],
          ...(entry.documentId ? [["strapi", "article", entry.documentId] as const] : []),
        ],
        paths: ["/articles", ...(entry.slug ? [`/articles/${entry.slug}`] : [])],
      };
    },
  },
});
```

In Strapi, create a webhook under **Settings > Webhooks**:

- URL: `https://your-app.example/api/strapi/webhook`
- Events: the entry and media events your mapping handles
- Header name: `x-farm-webhook-secret`
- Header value: the value of `STRAPI_WEBHOOK_SECRET`

Change `webhook.secretHeader` if the Strapi project uses another header name. The route answers
`401` before parsing an unauthenticated body, `400` for malformed JSON, and `500` without reflecting
application errors or credentials.

## Production notes

**Expiry is the fallback.** Strapi does not retry failed webhook deliveries. Give server queries a
time-based `staleTime`; use webhooks to refresh earlier, not as the only freshness mechanism.

**Multiple instances.** Farm's data cache lives in memory per process unless a distributed adapter
is configured. On a serverless platform the webhook reaches one instance. Configure an adapter such
as `@farm.js/cache-redis` when invalidation must reach every instance.

**Tokens stay on the server.** Keep `STRAPI_API_TOKEN`, `createStrapiClient`, and collection helpers
in server-only application modules. A draft query uses the same Content API token, so enable draft
selection only after application-owned preview authentication, never from an untrusted query
parameter.

## Options

| Option                 | Default                 | Notes                                              |
| ---------------------- | ----------------------- | -------------------------------------------------- |
| `apiUrl`               | `STRAPI_API_URL`        | Content API base URL, including `/api`.            |
| `mediaUrl`             | API origin              | Public origin for relative upload URLs.            |
| `token`                | `STRAPI_API_TOKEN`      | Server only.                                       |
| `instance`             |                         | Existing `StrapiClient`; skips API URL validation. |
| `webhook.secret`       | `STRAPI_WEBHOOK_SECRET` |                                                    |
| `webhook.path`         | `/api/strapi/webhook`   |                                                    |
| `webhook.secretHeader` | `x-farm-webhook-secret` | Configure the same custom header in Strapi.        |
| `webhook.onChange`     |                         | Maps a payload to `{ keys?, paths? }`.             |
| `log`                  |                         | Integration lifecycle logger.                      |

See Strapi's [Preview documentation](https://docs.strapi.io/cms/features/preview) before exposing
draft content. Basic Preview is available without Live Preview; the side-by-side editor and source
maps are plan-dependent and remain experimental in Strapi.

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
Docs-scoped sitemap: [/docs/sitemap.md](/docs/sitemap.md).
Well-known sitemap: [/.well-known/sitemap.md](/.well-known/sitemap.md).
