@areca-cms/skills 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,11 +6,14 @@ ESM only. Import paths and their exports:
6
6
 
7
7
  | Export | Signature | Notes |
8
8
  | --- | --- | --- |
9
- | `defineSiteContent` | `<T extends SiteContentConfig>(config: T) => T` | Identity helper for `areca.config.ts` default export |
10
- | `manifestFromConfig` | `(config: SiteContentConfig) => SiteManifest` | Validates identifiers and duplicates, returns `{ collections }` |
11
- | `createArecaClient` | `(options?: { cmsUrl?; siteToken?; schemaToken? }) => ArecaClient` | Falls back to `ARECA_CMS_URL`, `ARECA_SITE_TOKEN`, `ARECA_SCHEMA_TOKEN`; URL and site token required at creation |
12
- | `getPreviewEntry` | `({ token: string; client?: ArecaClient }) => Promise<PreviewResult>` | `PreviewResult` = `{ collection, slug, entry }`. Needs `ARECA_CMS_URL` and `ARECA_SITE_TOKEN` (or pass `client`). |
9
+ | `defineSiteContent` | `<const T extends {...}, const G extends {...}>(config: { siteKey; collections: T; globals?: G; assets? }) => same object` | Identity helper for `areca.config.ts`. Checks at compile time: `options` only on `select`, `collection` required on `reference` and must be a collection key, `after` must name a date key, `route` contains `$slug`. Globals take the same fields, with no `slug`, and may set `group` |
10
+ | `manifestFromConfig` | `(config: SiteContentConfig) => SiteManifest` | Validates identifiers, duplicates and reserved keys (`resolve`, `href`, `preview`, `globals`, `previewGlobals`, `in`, `languages`, `matchLocale`), returns `{ collections }` plus `globals` only when there are sections |
11
+ | `createArecaClient` | `(options?: { cmsUrl?; siteToken?; schemaToken? }) => ArecaClient` | Falls back to `ARECA_CMS_URL`, `ARECA_SITE_TOKEN`, `ARECA_SCHEMA_TOKEN`. Only the URL is required at creation; each call resolves the one credential it sends and fails before any request if it is missing |
12
+ | `FieldDropsRequiredError` | `class extends Error { drops: FieldDrop[] }` | Thrown by `syncManifest` when the CMS refuses removed fields that still hold content. Nothing was changed. Retry with `confirmDropFields` only after the user confirms |
13
+ | `getPreviewEntry` | `({ token: string; client?: ArecaClient; locale?: string }) => Promise<PreviewResult>` | `PreviewResult` = `PreviewEntryResult` `{ kind: 'entry', collection, slug, entry }` or `PreviewGlobalResult` `{ kind: 'global', global, entry }`. Needs `ARECA_CMS_URL` only (or pass `client`): the signed token is the credential. `locale` previews in that language |
14
+ | `toCamelKey` | `(raw: string) => string` | Converts a raw CMS key such as `blog-posts` or `company_info` to its code name (`blogPosts`, `companyInfo`) |
13
15
  | `previewHeaders` | `() => { 'X-Robots-Tag': 'noindex, nofollow'; 'Cache-Control': 'no-store' }` | Apply to preview responses |
16
+ | `matchLocale` | `(input: string \| readonly string[] \| null \| undefined, languages: SiteLanguages \| null \| undefined) => string \| undefined` | Picks a site language from a tag, a list of tags or an `Accept-Language` header: exact match, then subtag, then `languages.main`. Returns `undefined` without languages |
14
17
  | `seoHead` | `(seo: SeoMeta \| null \| undefined, options?: SeoHeadOptions) => { title, meta, links, scripts }` | TanStack Router `head()` shape |
15
18
  | `seoHeadHtml` | `(seo, options?) => string` | Escaped `<head>` HTML |
16
19
  | `seoJsonLd` | `(seo) => string[]` | Escaped JSON-LD strings |
@@ -18,32 +21,94 @@ ESM only. Import paths and their exports:
18
21
  | `richTextMarkdown` | `(value) => string` | `''` when empty |
19
22
  | `richTextHtml` | `(value) => string` | Static HTML; trusted CMS output only |
20
23
 
21
- `SeoHeadOptions` = `{ titleTemplate?: string /* contains %s */ | ((title: string) => string); siteName?: string }`.
24
+ `SeoHeadOptions` = `{ titleTemplate?: string /* contains %s */ | ((title: string) => string); siteName?: string; locale?: string; alternates?: Partial<Record<string, string>>; main?: string }`. `locale` sets `og:locale`, `alternates` become `hreflang` links and `og:locale:alternate`, and `main` makes its alternate `x-default`.
22
25
 
23
26
  `ArecaClient` methods (plain `fetch`, no retries or caching):
24
27
 
25
28
  | Method | HTTP | Auth |
26
29
  | --- | --- | --- |
27
30
  | `fetchManifest(): Promise<SiteManifest>` | `GET /api/sites/manifest` | site token |
28
- | `syncManifest(config): Promise<{ ok: true }>` | `POST /api/sites/manifest` | schema token |
31
+ | `syncManifest(config, opts?: { confirmDropFields?: string[] }): Promise<{ ok: true }>` | `POST /api/sites/manifest` | schema token. `confirmDropFields` lists confirmed removals as `<key>.<field>` |
29
32
  | `fetchSnapshot(opts?: { publicationId?: string }): Promise<Snapshot>` | `GET /api/sites/snapshot/current[?publicationId=]` | site token |
30
- | `fetchPreview(token: string): Promise<PreviewResult>` | `GET /api/sites/preview?token=` | signed preview token |
33
+ | `fetchPreview(token: string, opts?: { locale?: string }): Promise<PreviewResult>` | `GET /api/sites/preview?token=[&locale=]` | signed preview token (no `Authorization` header) |
34
+
35
+ Language types from the main entry: `SiteLanguages` (`{ main; all }`), `I18nConfig` (`{ paraglide? }`), `LocalizedValues` (`{ slug?; data; seo? }`, one language of an entry as published), `PreviewLocaleStatus` (`{ complete; missing }`), `PreviewSnapshotEntry` and `PreviewSnapshotGlobal` (the preview snapshot with `localeStatus`).
36
+
37
+ Types from the main entry: `ArecaClient`, `ArecaClientOptions`, `FieldDrop` (`{ kind: 'collection' | 'global'; key; field; entries }`), `SiteContentConfig`, `AssetsConfig` (`{ download?, dir?, publicPath? }`), `CollectionConfig` (`{ route; label?; fields: Field[] }`), `Field` (a union discriminated on `type`: `reference` requires `collection`, `select` takes `options`, `date` and `datetime` take `after`), `FieldType`, `SiteManifest`, `Snapshot` (`{ publicationId, siteKey, createdAt, hash, collections, assets?, siteUrl?, schema? }`), `SnapshotAsset` (`{ id, storageKey, url, filename, mimeType, size }`), `SnapshotEntry` (the raw entry: `{ id, slug, publishedAt: string | null, data: Record<string, unknown>, seo? }`), `ImageValue`, `ReferenceValue<C = string>` (`{ collection: C; slug; id }`), `RichTextValue`, `SeoMeta`, `PreviewResult`, `PulledContentManifest`, `HeadMeta`, `HeadLink`, `HeadScript`, `SeoHeadOptions`, `SeoHeadResult`.
38
+
39
+ Site content types: `GlobalConfig` (`{ label?, fields, seo?, previewPath? }`, `seo` defaults to `false`), `SnapshotGlobal` (`{ id, publishedAt, data, seo? }`), `PreviewEntryResult`, `PreviewGlobalResult`, and `SiteManifest` / `Snapshot`, which gain optional `globals` (and `globalSchema` on the snapshot).
40
+
41
+ Typed content types (main entry, generated against `Register`):
42
+
43
+ | Export | Meaning |
44
+ | --- | --- |
45
+ | `Register` | Interface that `areca.gen.ts` augments with `schema: typeof schema` |
46
+ | `ContentSchema`, `ContentSchemaCollection`, `ContentSchemaField` | Shape of the generated schema (`key`, `label?`, `route`, `fields`; field `key`, `type`, `required`, `options?`, `collection?`) |
47
+ | `RegisteredSchema` | The registered schema, or `ContentSchema` when nothing is registered |
48
+ | `CollectionKey<S>` | camelCase collection keys |
49
+ | `FieldKey<C, S>` | field keys of collection `C` |
50
+ | `FieldValue<C, F, S>` | value type of one field, with null, missing and `''` where the CMS sends them |
51
+ | `EntryData<C, S>` | `data` of collection `C` |
52
+ | `Entry<C, S>` | `{ id, slug, publishedAt, collection: C, data, seo? }` |
53
+ | `AnyEntry<S>` | union of the entries of every collection, discriminated on `collection` |
54
+ | `Where<C, S>` | `where` option: field keys with equality or operator objects |
55
+ | `OrderBy<C, S>` | `orderBy` option: sortable field keys with `'asc' \| 'desc'`, or an array |
56
+ | `QueryOptions<C, S>` | `where`, `orderBy`, `limit`, `offset`, `with` |
57
+ | `QueryResult<C, O, S>` | the entry type returned for query options `O`, with expanded references |
58
+ | `RoutePath<R>` | a route with `$slug` replaced by `${string}` |
59
+ | `Locale` | union of the site's language codes, from the generated `Register` (`string` without languages) |
60
+ | `EntryLocale` | `{ locale; isFallback; locales; alternates }` added to each entry on a languages site |
61
+ | `GlobalLocale` | `{ $locale; $isFallback }` added to each section value on a languages site |
62
+ | `LocaleOptions` | per-call options: `{ locale?: Locale; fallback?: boolean }` (`fallback` defaults to `true`) |
63
+ | `Href<C, S, L>` | the route type of collection `C`: the union of every language's route on a languages site, such as `` `/blog/${string}` ``. With language `L`, the route of that language, as `content.in(L)` gives |
64
+ | `Content<S, G>` | the content object: one property per collection, plus `resolve`, `href`, `preview`, `globals` and `previewGlobals` |
65
+ | `ContentGlobalSchema` | one site content section in the generated `globalSchema`: `key`, `label?`, `seo`, `previewPath?`, `fields` |
66
+ | `GlobalSchema`, `RegisteredGlobalSchema` | the generated `globalSchema` (or the loose type when nothing is registered) |
67
+ | `GlobalKey<G>` | camelCase section keys |
68
+ | `GlobalData<K, G>` | the field values of section `K` |
69
+ | `Global<K, G>` | one section: `GlobalData<K>`, plus `$seo: SeoMeta` when the section has `seo: true` |
70
+ | `Globals<G>` | the object returned by `content.globals` and `previewGlobals`, one `Global` per section |
71
+ | `GlobalsPreview<G>` | the result of `previewGlobals`: `{ globals, previewing, complete, issues }` |
72
+ | `CollectionApi<C, S>` | `all`, `first`, `get`, `byId` and `href` of one collection |
73
+ | `PreviewEntry<S>` | the union returned by `content.preview`, narrowed by `collection` and `complete`: `{ collection, slug, complete: true, entry: Entry, issues: [] }` or `{ collection, slug, complete: false, entry: DraftEntry, issues: string[] }` |
74
+ | `DraftEntry<C, S>`, `DraftEntryData<C, S>` | the entry of an incomplete preview draft; every field has its optional type, whether or not it is required |
75
+
76
+ ## `@areca-cms/site-sdk/content` (Node only)
31
77
 
32
- Types: `ArecaClient`, `ArecaClientOptions`, `SiteContentConfig`, `CollectionConfig` (`{ route; label?; fields: Field[] }`), `Field` (`{ key; label; type: FieldType; required?; options?; collection?; description?; after? }`), `FieldType`, `SiteManifest`, `Snapshot` (`{ publicationId, siteKey, createdAt, hash, collections: Record<string, SnapshotEntry[]>, assets?: SnapshotAsset[], siteUrl? }`), `SnapshotAsset` (`{ id, storageKey, url, filename, mimeType, size }`), `AssetsConfig` (`{ download?, dir?, publicPath? }`), `SnapshotEntry` (`{ id, slug, publishedAt: string | null, data: Record<string, unknown>, seo?: SeoMeta }`), `ImageValue`, `ReferenceValue`, `RichTextValue`, `SeoMeta`, `PreviewResult`, `PulledContentManifest`, `HeadMeta`, `HeadLink`, `HeadScript`, `SeoHeadOptions`, `SeoHeadResult`.
78
+ | Export | Signature | Notes |
79
+ | --- | --- | --- |
80
+ | `createContent` | `<const S extends ContentSchema, const G extends GlobalSchema>(options: { schema: S; globalSchema?: G; dir: string }) => Content<S, G>` | Builds the typed content object. `areca.gen.ts` calls it; use it only for another folder or schema. `dir` is resolved against `process.cwd()` on each call. Throws for a collection named `resolve`, `href`, `preview`, `globals` or `previewGlobals` |
81
+
82
+ `content` (exported by `areca.gen.ts`) has one property per collection: `all(options?)`, `first(options?)`, `get(slug)`, `byId(id)`, `href(entryOrSlug)`. It also has `resolve(ref)` (returns `Entry | null`), `href(ref)` and `preview(token, { client? })` (returns `PreviewEntry<S>`; check `complete` before using `entry` as published content). Queries run in memory. `get` and `byId` return `undefined` for unknown values. A collection missing from the publication returns `[]`. `content.globals` is the object of enabled site content sections, keyed by camelCase key, and `content.previewGlobals(source, { client? })` returns it with one section replaced by its draft (see Site content below).
83
+
84
+ ## Site content (globals)
85
+
86
+ | Export | Where | Notes |
87
+ | --- | --- | --- |
88
+ | `content.globals` | `areca.gen.ts` | `{ [camelKey]: Global }`. Read per call, from the current generation |
89
+ | `content.previewGlobals` | `areca.gen.ts` | `(source: string \| PreviewResult \| null \| undefined, { client? }) => Promise<GlobalsPreview>`. A falsy source or an entry result returns the published values with `previewing: null`. A draft that fails validation keeps the published section, with `complete: false` and `issues` |
90
+ | `readGlobalsFile` | `@areca-cms/site-sdk/pull` | `(outDir, manifest: PulledContentManifest) => Record<string, SnapshotGlobal>`. Reads `_globals.json` for the generation. Throws when the manifest lists globals but the file is missing |
91
+ | `PulledContentManifest.globals` | `@areca-cms/site-sdk/pull` | Optional `string[]` of camel keys, absent when there are no sections |
92
+
93
+ `$seo` is only present when the section has `seo: true`.
33
94
 
34
95
  ## `@areca-cms/site-sdk/pull` (Node only)
35
96
 
36
97
  | Export | Signature | Notes |
37
98
  | --- | --- | --- |
38
- | `pullContent` | `({ outDir: string; client?: ArecaClient; publicationId?: string; assets?: { dir: string; publicPath: string } }) => Promise<PullContentResult>` | With `assets`, downloads media into `dir` (absolute) and rewrites URLs to `publicPath`. Writes `outDir/.publications/publication-<random>/<collection>.json`, then atomically swaps `manifest.json`, which records the `publicationId`. Keeps the current and two previous generations. Defaults `publicationId` to `ARECA_SNAPSHOT_ID`. A failed pull keeps the previous generation. Result (`PullContentResult`) adds `localAssets?` and `warnings`. The `assets` option type is `ResolvedAssets`. |
39
- | `readContentManifest` | `(outDir: string) => PulledContentManifest` | `{ publicationId, hash, createdAt, collections counts, ... }` |
40
- | `readContent` | `<T = SnapshotEntry>(outDir: string, collectionKey: string, manifest?: PulledContentManifest) => T[]` | Throws `No content file for collection "x" ... Run \`areca pull\` first.` |
99
+ | `pullContent` | `(options: PullContentOptions) => Promise<PullContentResult>` | Validates the publication against its schema, writes `outDir/.publications/publication-<random>/<camelCollection>.json`, switches `manifest.json`, then writes the types file. Keeps the current and two previous generations. Defaults `publicationId` to `ARECA_SNAPSHOT_ID`. A failed pull before the switch keeps the previous generation |
100
+ | `readContentManifest` | `(outDir: string) => PulledContentManifest` | `{ publicationId, hash, createdAt, collections counts, generation?, assets? }` |
101
+ | `readContent` | `<C extends CollectionKey>(outDir: string, collectionKey: C, manifest?: PulledContentManifest) => Entry<C>[]` | Typed by the collection key, no type parameter. Throws `No content file for collection "x" ... Run \`areca pull\` first.` |
102
+
103
+ `PullContentOptions` = `{ outDir: string; client?: ArecaClient; publicationId?: string; assets?: ResolvedAssets; types?: string /* default <outDir>/areca.gen.ts */; contentDir?: string /* written into the module, default outDir */ }`.
41
104
 
42
- Type: `PullContentOptions`.
105
+ `ResolvedAssets` = `{ dir: string /* absolute */; publicPath: string }`.
106
+
107
+ `PullContentResult` = the snapshot as the CMS sent it, plus `schema`, `localAssets?` (`{ dir, total, downloaded }`), `warnings: string[]` and `typesPath`.
43
108
 
44
109
  ## Other subpaths
45
110
 
46
- - `@areca-cms/site-sdk/seo`: `seoHead`, `seoHeadHtml`, `seoJsonLd` and their types.
111
+ - `@areca-cms/site-sdk/seo`: `seoHead`, `seoHeadHtml`, `seoJsonLd`, `ogLocale(tag)` (`nl-BE` gives `nl_BE` for `og:locale`) and their types.
47
112
  - `@areca-cms/site-sdk/rich-text`: `richTextDocument`, `richTextMarkdown`, `richTextHtml`.
48
113
  - `@areca-cms/site-sdk/rich-text-react`: `richTextComponents(components?: MarkdownComponents) => MarkdownComponents`. Wraps `img` so editor sizes are kept. Needs `react` (optional peer) and `@tanstack/markdown@1.0.0`.
49
114
 
@@ -64,5 +129,3 @@ Type: `PullContentOptions`.
64
129
  ```
65
130
 
66
131
  `seo` is missing on snapshots from older CMS versions. `seoHead` and `seoHeadHtml` then return empty head data.
67
-
68
- `PulledContentManifest` = `{ publicationId, hash, createdAt, collections: Record<string, number>, generation? }`.
@@ -0,0 +1,177 @@
1
+ # Multilingual sites (SDK)
2
+
3
+ Read this when the CMS site has content languages. `get_site_info` in the MCP server, or `content.languages` in the SDK, tells you: `content.languages` is `null` on a single-language site, and then the `locale` options, `entry.locale` and the `Locale` type do not apply.
4
+
5
+ ## Configure the languages
6
+
7
+ ```ts
8
+ // areca.config.ts
9
+ import { defineSiteContent } from "@areca-cms/site-sdk";
10
+
11
+ export default defineSiteContent({
12
+ siteKey: "acme",
13
+ languages: { main: "en", all: ["en", "nl"] },
14
+ i18n: { paraglide: "./src/paraglide/runtime.js" },
15
+ collections: {
16
+ articles: {
17
+ route: "/journal/$slug",
18
+ routes: { nl: "/journaal/$slug" },
19
+ fields: [
20
+ { key: "title", label: "Title", type: "text", required: true },
21
+ { key: "slug", label: "Slug", type: "slug", required: true },
22
+ { key: "price", label: "Price", type: "number", translatable: false },
23
+ ],
24
+ },
25
+ },
26
+ globals: {
27
+ homePage: { label: "Home page", requiredLanguages: ["nl"], fields: [/* ... */] },
28
+ },
29
+ });
30
+ ```
31
+
32
+ - `languages.main` is the fallback and the language of shared values. `languages.all` is the switcher order and includes `main`. Validation fails on duplicates, a `main` that is not in `all`, or an empty `all`.
33
+ - `routes` maps a language to its route pattern. Keys must be in `languages.all`, and each pattern must contain `$slug`. Languages not listed use `route`. The pattern has no language prefix; the site adds it.
34
+ - `requiredLanguages` on a collection or global lists the languages (other than main) that must be complete before an entry is marked ready. Every key must be in `languages.all`.
35
+ - `translatable` on a field overrides the default. Text, textarea, richtext and url are per language by default; every other type is shared across languages.
36
+ - `i18n.paraglide` is the Paraglide runtime path. When it is set, `areca pull` wires its `getLocale` into the generated content object.
37
+ - `languages` and the per-language settings are sent with the schema by `manifest push`. `i18n` and `assets` stay local.
38
+ - `manifest pull` lists language changes before it writes. Removing a language, or changing `main`, needs confirmation (`--yes` without a TTY). Adding one only appears in the diff. Ask the user before confirming: a removed language stops being served.
39
+ - `areca pull` warns when `project.inlang/settings.json` disagrees with `languages`.
40
+
41
+ The generated `areca.gen.ts` exports `languages` (`{ main, all }`) and the `Locale` union, which is registered for the typed API. Commit it like any other generated file.
42
+
43
+ ## Quick start with Paraglide
44
+
45
+ 1. `pnpm add @inlang/paraglide-js`, with `project.inlang/settings.json` listing the same `locales` as `languages.all`.
46
+ 2. Add the Vite plugin with `strategy: ['url', 'baseLocale']`, so the locale comes from the URL prefix only.
47
+ 3. Compile before anything imports the generated content: `paraglide-js compile --project ./project.inlang --outdir ./src/paraglide`. Then run `areca pull`.
48
+ 4. Wrap the server entry in `paraglideMiddleware`, so server rendering and prerendering see the URL's language.
49
+ 5. Localize the router's URLs with `deLocalizeUrl` and `localizeUrl` in `createRouter({ rewrite })`. Routes are written once in the main language.
50
+ 6. `content.articles.all()` now returns the language of the current URL. Use `getLocale()` for `Intl` formatting.
51
+
52
+ Prerender every language's tree. Each entry's `alternates` gives its path in each language.
53
+
54
+ ## Other i18n libraries
55
+
56
+ `areca pull` wires only Paraglide. For anything else, build your own content object from the generated schema and pass a resolver:
57
+
58
+ ```ts
59
+ // src/content.ts
60
+ import { createContent } from "@areca-cms/site-sdk/content";
61
+ import { schema, globalSchema } from "./generated/content/areca.gen.ts";
62
+ import i18next from "i18next";
63
+
64
+ export const content = createContent({
65
+ schema,
66
+ globalSchema,
67
+ dir: "src/generated/content",
68
+ locale: () => i18next.resolvedLanguage, // called on every read; undefined = main language
69
+ });
70
+ ```
71
+
72
+ `createContent({ locale })` takes a string or a function. A function is called on each read, so it follows the library's current language. Use `i18n.global.locale.value` for vue-i18n, or `i18n.locale` from `@lingui/core` for Lingui.
73
+
74
+ On a server with a per-request language (for example next-intl), do not rely on a global resolver. Pass the request's language per call: `content.in(locale).articles.all()` or `content.articles.all({ locale })`.
75
+
76
+ ## How the language is chosen
77
+
78
+ For each read, the first of these that is set wins:
79
+
80
+ 1. the `locale` in the call: `all({ locale })`, `get(slug, { locale })`, `byId(id, { locale })`;
81
+ 2. the scope from `content.in(locale)`;
82
+ 3. the resolver from `createContent({ locale })`;
83
+ 4. the main language.
84
+
85
+ `content.in(locale)` returns a new content object that reads in that language. A per-call `locale` still overrides the scope.
86
+
87
+ Types follow the same scope. `Href` (and `href()` on a collection or `content.href(ref)`) is the union of every language's route, such as `` `/journal/${string}` | `/journaal/${string}` ``. `content.in("nl").articles.href(entry)` narrows it to the Dutch route `` `/journaal/${string}` ``.
88
+
89
+ Media URLs are rewritten for every language view, so the main language and the other languages all serve the local files from `areca pull`, not CMS URLs.
90
+
91
+ ## Reading in a language
92
+
93
+ Every entry carries:
94
+
95
+ | Field | Meaning |
96
+ | --- | --- |
97
+ | `entry.locale` | The language of `data`, `slug` and `seo`. |
98
+ | `entry.isFallback` | `true` when it is served in a language other than the one asked for. |
99
+ | `entry.locales` | The languages with a complete translation of this entry. |
100
+ | `entry.alternates` | The entry's URL in each of `locales`, from its per-language slug and route, for example `{ en: "/journal/backend", nl: "/journaal/backend" }`. |
101
+
102
+ `content.languages` is `{ main, all }` or `null`. Section values (`content.globals`) carry `$locale` and `$isFallback` instead, and their other values stay flat.
103
+
104
+ ```ts
105
+ const dutch = content.in("nl");
106
+ const posts = dutch.articles.all({ orderBy: { title: "asc" } });
107
+
108
+ // Only entries that really exist in Dutch
109
+ const onlyDutch = content.articles.all({ locale: "nl", fallback: false });
110
+
111
+ const post = content.articles.get("backend-ontwikkelaar", { locale: "nl" });
112
+ if (post?.isFallback) {
113
+ // show the main-language text with a note, or link to the English page
114
+ }
115
+ ```
116
+
117
+ - `fallback` defaults to `true`. With `false`, `all` leaves out entries with no complete translation in the requested language, and `get` returns `undefined` for them.
118
+ - `where` and `orderBy` run on the language's own values, so a Dutch title sorts by the Dutch title.
119
+ - For a requested language, the entry is served from the first complete language of this chain: the exact tag (case-insensitive), its language subtag (`nl-BE` gives `nl`), the main language, then the first complete language in site order.
120
+
121
+ Use `LocaleOptions` (`{ locale?: Locale; fallback?: boolean }`) to type your own wrappers around these calls.
122
+
123
+ ## Match the visitor's language
124
+
125
+ ```ts
126
+ content.matchLocale("nl-BE"); // "nl"
127
+ content.matchLocale("de;q=0.9, en-GB;q=0.8"); // "en"
128
+ content.matchLocale(request.headers.get("accept-language")); // main language when nothing matches
129
+ ```
130
+
131
+ `content.matchLocale(input)` tries each tag in preference order, first as an exact match and then by its subtag. `Accept-Language` headers are sorted by quality. It returns the main language when nothing matches, and `undefined` on a single-language site. The pure function `matchLocale(input, languages)` from `@areca-cms/site-sdk` takes the languages explicitly, which is useful in a redirect or in a resolver.
132
+
133
+ ## Alternate links and hreflang
134
+
135
+ Pass the entry's alternates and language to `seoHead` (or `seoHeadHtml`):
136
+
137
+ ```ts
138
+ import { seoHead } from "@areca-cms/site-sdk";
139
+
140
+ export const head = ({ entry }) =>
141
+ seoHead(entry.seo, {
142
+ locale: entry.locale,
143
+ alternates: entry.alternates,
144
+ main: content.languages?.main,
145
+ });
146
+ ```
147
+
148
+ - `locale` sets `og:locale`, using `ogLocale(tag)` from `@areca-cms/site-sdk/seo`: `nl` stays `nl`, `nl-BE` becomes `nl_BE`.
149
+ - Each alternate becomes a `hreflang` link and an `og:locale:alternate`. The page's own locale is included as a link but not as an `og:locale:alternate`.
150
+ - `main` makes the main language's alternate also the `x-default` link.
151
+ - Relative alternate paths are made absolute from the site origin when the entry has a canonical URL.
152
+ - For pages that aren't entries, build the same alternates from the path in each language.
153
+ - Set the document's `lang` attribute to the page's language.
154
+
155
+ ## Preview in a language
156
+
157
+ The CMS opens preview links with `&locale=<code>`. Pass it through:
158
+
159
+ ```ts
160
+ const result = await content.preview(token, { locale: url.searchParams.get("locale") ?? undefined });
161
+ // or without the content object:
162
+ const raw = await getPreviewEntry({ token, locale: "nl" });
163
+ ```
164
+
165
+ - `content.previewGlobals(source, { locale })` takes the same option for sections.
166
+ - `client.fetchPreview(token, { locale })` sends the same query parameter.
167
+ - A preview draft carries `localeStatus`: a map from each site language to `PreviewLocaleStatus` (`{ complete, missing }`, where `missing` lists the labels of the missing required fields). `PreviewSnapshotEntry` and `PreviewSnapshotGlobal` are the preview types that include it. Use it to show an incomplete draft in the language it was written in.
168
+ - Preview responses are draft content. Send `previewHeaders()` (`X-Robots-Tag: noindex, nofollow`, `Cache-Control: no-store`) and keep them out of caches and search.
169
+
170
+ ## Checklist
171
+
172
+ - `languages` in `areca.config.ts` matches the CMS site and `project.inlang/settings.json`.
173
+ - `routes` for each language use the same segments as the router's localized URLs.
174
+ - Shared values (prices, contact details, product names) are `translatable: false`.
175
+ - Pages prerender in every language and link to their alternates.
176
+ - Untranslated entries either fall back with a note or are left out with `fallback: false`.
177
+ - Preview routes read `locale` from the query string and send `previewHeaders()`.
@@ -17,6 +17,7 @@ import { getPreviewEntry, previewHeaders } from "@areca-cms/site-sdk";
17
17
  try {
18
18
  const { collection, slug, entry } = await getPreviewEntry({ token });
19
19
  // render `entry` (same shape as SnapshotEntry) with the normal template
20
+ // (with the generated content object, `content.preview(token)` returns the typed entry)
20
21
  // and set every header from previewHeaders() plus <meta name="robots" content="noindex">
21
22
  } catch (error) {
22
23
  // Plain Error, no status field. The message holds the status:
@@ -25,6 +26,8 @@ try {
25
26
  }
26
27
  ```
27
28
 
29
+ A site content section preview is different. The CMS opens the section's `previewPath` on the site (for example `/about`) with `?arecaPreview=<token>`, not `/__preview`. `getPreviewEntry` returns `{ kind: "global", global, entry }` for that token. Read the token on the page, then call `content.previewGlobals(token)` (or pass the `PreviewGlobalResult`). It returns `{ globals, previewing, complete, issues }`: `globals` has the section replaced by its draft when the draft is complete, and the published section otherwise. Use the draft only when `previewing` is this page's section, and show a note when `complete` is `false`. An expired or mistyped token makes the fetch throw, so catch it and serve the published values. Set `previewHeaders()` on the response as for entries.
30
+
28
31
  `getPreviewEntry` needs `ARECA_CMS_URL` and `ARECA_SITE_TOKEN` at runtime. Exclude `/__preview` from prerendering and CDN caching. In TanStack Start, wrap the fetch in a `createServerFn` handler.
29
32
 
30
33
  ## Health route (Production mode)
@@ -83,7 +86,12 @@ await pullContent({ outDir: "src/generated/content", client });
83
86
  | `404` / no publication on pull | Nothing is live yet. Go live in the CMS, then pull again. |
84
87
  | `429` | 300 requests/min per token. Wait for `Retry-After`. |
85
88
  | `400` / `413` on push | Invalid schema, or the manifest is over 256 KiB |
89
+ | `Removing <key.field> deletes content` / `FieldDropsRequiredError` | Nothing was pushed. Ask the user before confirming, then re-run with `--confirm-drop <key.field>` (or `--yes`), or pass `confirmDropFields` to `syncManifest`. |
86
90
  | `No content file for collection` | Run `areca pull`, then check the collection key |
87
91
  | `Unsafe collection or field identifier` | Rename the key to match `^[A-Za-z][A-Za-z0-9_-]{0,63}$` |
92
+ | `Collection key "<key>" is reserved` / `resolve`, `href`, `preview`, `globals`, `previewGlobals` | Collections cannot use these names, because the content object uses them. Rename the collection. Field keys may keep them. |
93
+ | `Collection keys ... both map to` / `Field keys ... both map to` | Two keys have the same camelCase name, such as `blog-posts` and `blog_posts`. Rename one. |
94
+ | `Publication does not match its schema` | An entry breaks its schema. Each line names the entry, field and problem. Fix the entry or the schema, publish, and pull again. Nothing is written. |
95
+ | `Warning: Publication <id> has no embedded schema` | An older publication. Types use the current schema. Go live again to embed it. |
88
96
  | `Expected publication X, received Y` | Pinned pull mismatch. Check `ARECA_SNAPSHOT_ID`. |
89
97
  | TS config import fails | Use Node 24, or run through `tsx` |