@areca-cms/skills 0.3.0 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to `@areca-cms/skills`.
4
4
 
5
+ ## 0.4.0 (2026-10-14)
6
+
7
+ ### Features
8
+
9
+ - add site content (globals) sections (b1c0aaf)
10
+ - implement full end-to-end types (86c76bb)
11
+
12
+ ### Bug fixes
13
+
14
+ - address review feedback on site content globals (46791c2)
15
+
5
16
  ## 0.3.0 (2026-10-11)
6
17
 
7
18
  No user-facing changes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@areca-cms/skills",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Agent skills for Areca CMS: the site SDK and the MCP server. Installs into Claude Code, Codex, Cursor, Copilot, OpenCode, Gemini CLI and 20+ other agents.",
5
5
  "keywords": [
6
6
  "areca",
@@ -3,12 +3,12 @@ name: areca-mcp
3
3
  description: Connects an AI client to an Areca CMS site through its MCP server and uses its tools to write, edit, review and publish content. Covers the per-site connector URL, OAuth sign-in or personal keys, roles and go-live permission, the draft/ready/live workflow, field and Markdown formats, SEO, images, and every tool. Use when the user wants to connect Areca to an AI app or coding agent, or to create, update, approve, schedule or publish Areca CMS entries, upload images, or check what is live.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.3.0"
6
+ version: "0.4.0"
7
7
  ---
8
8
 
9
9
  # Areca MCP
10
10
 
11
- Every Areca site has its own remote MCP server. Through it an AI can read and write that site's content: entries, the media Library, releases and, for agency admins, content types. It edits CMS content and is separate from the `@areca-cms/site-sdk` website code (see the `areca-sdk` skill).
11
+ Every Areca site has its own remote MCP server. Through it an AI can read and write that site's content: entries, site content (single sections such as company info), the media Library, releases and, for agency admins, content types and site content structure. It edits CMS content and is separate from the `@areca-cms/site-sdk` website code (see the `areca-sdk` skill).
12
12
 
13
13
  ## Connect
14
14
 
@@ -20,8 +20,8 @@ Every Areca site has its own remote MCP server. Through it an AI can read and wr
20
20
 
21
21
  ## First calls (always)
22
22
 
23
- 1. `get_site_info`: returns your role, what you **cannot** do, the collections and the site's publishing mode.
24
- 2. `get_collection_schema({ collection })`: returns exact field formats, an example payload and SEO guidance. Call it before every first write to a collection.
23
+ 1. `get_site_info`: returns your role, what you **cannot** do, the collections, the site content sections (`globals`) and the site's publishing mode.
24
+ 2. `get_collection_schema({ collection })`: returns exact field formats, an example payload and SEO guidance. Call it before every first write to a collection. For a site content section, `get_global({ global })` gives the same information.
25
25
 
26
26
  Tools your role can't use are not listed at all. If the user asks for one of those actions, explain who can do it and don't look for workarounds.
27
27
 
@@ -37,7 +37,7 @@ schedule_entry -> scheduled goes live at a set time (also publishes o
37
37
  - `status`: `draft` | `ready` | `scheduled` | `archived`. To answer "what is live" use the flags: `live` (on the production site now), `pendingRelease` (approved changes waiting for the next go live) and `editedSinceReady`.
38
38
  - Drafts are safe. Nothing changes the live site until an entry is marked ready **and** a go live happens.
39
39
  - Only call `mark_ready`, `go_live`, `schedule_entry`, `rollback`, `delete_*` or `archive_entry` when the user explicitly asks.
40
- - Writes return a `version`. Pass it as `expectedVersion` on the next `update_entry`. On a version conflict, call `get_entry`, reapply the change and retry.
40
+ - Writes return a `version`. Pass it as `expectedVersion` on the next `update_entry` or `update_global`. On a version conflict, call `get_entry` (or `get_global`), reapply the change and retry.
41
41
  - Show the user the `previewUrl` that create and update return (it's signed and valid for about 1 hour). If the site has no production URL, preview isn't available.
42
42
 
43
43
  ## Roles
@@ -47,7 +47,7 @@ schedule_entry -> scheduled goes live at a set time (also publishes o
47
47
  | editor | read everything, create and edit drafts, upload images, edit alt text, preview |
48
48
  | owner | everything an editor can, plus `mark_ready`, `unpublish_entry`, `archive_entry`, `cancel_schedule`, `delete_entry`, `restore_revision`, `delete_asset` |
49
49
  | + go-live permission | `go_live`, `schedule_entry`, `rollback`, `cancel_build`. Granted per connection or key with "Allow going live" (owners and agency admins only). |
50
- | agency admin | everything above, plus `upsert_collection` and `delete_collection` (needs 2FA) |
50
+ | agency admin | everything above, plus `upsert_collection`, `delete_collection`, `upsert_global` and `delete_global` (needs 2FA) |
51
51
 
52
52
  ## Writing content
53
53
 
@@ -56,7 +56,8 @@ schedule_entry -> scheduled goes live at a set time (also publishes o
56
56
  - **Field values:** `image` and `reference` take ids, `date` is `YYYY-MM-DD`, `datetime` is ISO 8601 with an offset, `select` must match an option exactly, and `slug` is lowercase with dashes and unique. Date fields are content only and never schedule an entry.
57
57
  - **Clearing:** use `""` for a field or SEO text, `[]` for SEO lists and `false` for `noindex`/`nofollow`. Avoid `null` (`seo` rejects it). A key you leave out keeps its value.
58
58
  - **SEO on every new entry:** `seo.title` (50-60 chars), `seo.description` (140-160), `focusKeyphrase`, `summary` (2-3 sentences) and 3-5 `keyTakeaways`. Add `faq` only when the body really answers those questions.
59
- - **Schema:** if the site project has `areca.config.ts`, change the schema there and push it with the SDK rather than calling `upsert_collection`. `upsert_collection` replaces the whole field list.
59
+ - **Site content (globals):** single sections such as company info or the home page. Read one with `get_global({ global })` and change it with `update_global({ global, fields?, seo?, expectedVersion? })`. It is not a collection, so `create_entry` and `list_entries` do not apply. Its `id` (from `get_global`) works with `mark_ready`, `schedule_entry`, `cancel_schedule`, `list_revisions`, `restore_revision` and `get_preview_url`. Use `discard_global_draft` to go back to the ready values. `seo` is only accepted when the section has SEO on.
60
+ - **Schema:** if the site project has `areca.config.ts`, change the schema there and push it with the SDK rather than calling `upsert_collection` or `upsert_global`. Both replace the whole field list.
60
61
 
61
62
  Full formats, the SEO object, limits and errors are in [references/content.md](references/content.md). Every tool with its inputs is in [references/tools.md](references/tools.md).
62
63
 
@@ -19,6 +19,10 @@ Unknown field keys are refused, and the error lists the valid ones. Entry data i
19
19
 
20
20
  **Clearing on update:** use `""` for fields and SEO text, `false` for `noindex`/`nofollow`, `[]` for `keyTakeaways`/`faq`/`about`, and an empty `name` for `author`. Don't use `null`: fields accept it, but `seo` rejects it. A key you leave out keeps its value.
21
21
 
22
+ ## Site content (globals)
23
+
24
+ A section has the same field types as a collection, except `slug`. Its fields can set `group`, a heading shown above the field in the CMS editor (not in the data). Collection fields cannot set `group`. A section stores one set of values, so there is no create and no list: read it with `get_global` and change it with `update_global`. `seo` is only accepted on a section that has SEO turned on; `get_global` shows whether it does. `get_global` returns `previewAvailable: true` only when the section has a `previewPath` and the site has a production URL.
25
+
22
26
  ## Rich text Markdown
23
27
 
24
28
  - Use `##` for sections and `###` or `####` for subsections. Other heading levels are clamped.
@@ -63,7 +67,10 @@ Unknown SEO keys are refused. There is no SEO-generation tool, so you write thes
63
67
  | `Could not import the file:` | Ask the user for a direct, public file link |
64
68
  | `Only site owners…` / `Going live…` / `Only agency admins…` | Tell the user who can do it, e.g. an owner presses Go live in the CMS |
65
69
  | `No entry with id…` (or asset/job/publication) | Use the list tools to find valid ids |
66
- | `No collection "…" on this site` | Use one of the listed keys (see `get_site_info`) |
70
+ | `No collection "…" on this site` | Use one of the listed keys (see `get_site_info`). Site content keys are not collections: use `get_global`. |
71
+ | `No site content "…" on this site` | Use one of the `globals` keys from `get_site_info`. Disabled sections are not listed to editors and owners. |
72
+ | `… has no SEO` | The section has SEO off, so drop `seo`. Ask an agency admin to turn SEO on if it is needed. |
73
+ | `… has no preview page` | Preview needs a `previewPath` on the section. An agency admin sets it. |
67
74
  | `Something went wrong on the Areca server (ref …)` | Nothing is confirmed saved. Retry once, then report the ref. |
68
75
 
69
76
  HTTP: `401` = token or key invalid, expired, revoked or for another site, so reconnect. `403 mfa_required` = the agency admin must enable 2FA. `429` = wait `Retry-After` seconds (120/min per credential, plus 600/min per client IP when the CMS trusts its proxy).
@@ -6,7 +6,7 @@
6
6
 
7
7
  | Tool | Access | Inputs | Returns |
8
8
  | --- | --- | --- | --- |
9
- | `get_site_info` | all | none | `site` (name, slug, productionUrl, publishingMode), `you` (role, capabilities), `cannotDo[]`, `collections[]`, `guide`. **Call first.** |
9
+ | `get_site_info` | all | none | `site` (name, slug, productionUrl, publishingMode), `you` (role, capabilities), `cannotDo[]`, `collections[]`, `globals[]` (site content: key, label, enabled, fieldCount, seo, previewPath), `guide`. **Call first.** |
10
10
  | `get_collection_schema` | all | `collection*` | Per-field key, type, required, format, options; example `fields` and `seo` payloads; SEO guide |
11
11
 
12
12
  ## Entries
@@ -20,6 +20,16 @@
20
20
  | `list_revisions` | all | `id*` | Revision id, note, createdAt, createdBy |
21
21
  | `get_preview_url` | all | `id*` | `{ url }`, signed, about 1 hour. Fails without a site production URL. |
22
22
 
23
+ ## Site content (globals)
24
+
25
+ Single sections such as company info or the home page. Editors and owners see enabled sections only; agency admins see all of them. The `id` returned here is the entry id for the workflow tools below.
26
+
27
+ | Tool | Access | Inputs | Returns |
28
+ | --- | --- | --- | --- |
29
+ | `get_global` | all | `global*` (key from `get_site_info`) | `id`, `key`, `label`, `status`, `live`, `pendingRelease`, `version`, `editedSinceReady`, `previewAvailable`, `values` (rich text as Markdown, images as `{assetId,url,alt}`, references as `{entryId}`), `seo` (only with SEO on), `fields` (format of each field), `example` |
30
+ | `update_global` | all | `global*`, `fields?`, `seo?` (at least one; refused without SEO on the section), `expectedVersion?` | Updated draft + `previewUrl`. Keys you leave out keep their values. |
31
+ | `discard_global_draft` | all | `global*` | Draft reset to the ready values, kept in history. Refused when the section was never ready. |
32
+
23
33
  ## Workflow
24
34
 
25
35
  | Tool | Access | Inputs | Effect |
@@ -32,6 +42,8 @@
32
42
  | `restore_revision` | owner | `id*`, `revisionId*`, `expectedVersion?` | Copies a revision into the draft and returns `skippedFields`. Doesn't mark it ready. |
33
43
  | `schedule_entry` | go-live | `id*`, `publishAt*` (future ISO 8601 with offset), `timezone*` (IANA, e.g. `Europe/Amsterdam`) | Goes live at that time, together with the site's other ready changes |
34
44
 
45
+ `mark_ready`, `schedule_entry`, `cancel_schedule`, `list_revisions`, `restore_revision` and `get_preview_url` also take a site content section's `id`. `unpublish_entry`, `archive_entry` and `delete_entry` refuse it: change the section instead.
46
+
35
47
  ## Library (assets)
36
48
 
37
49
  | Tool | Access | Inputs | Returns |
@@ -59,6 +71,8 @@
59
71
  | --- | --- | --- | --- |
60
72
  | `upsert_collection` | agency | `key*`, `label*`, `routePattern*`, `fields*` (up to 100: `key*`, `label*`, `type*`, `required?`, `options?`, `collection?`, `description?`, `after?`), `enabled?` | Creates the collection or **replaces** its field list. Prefer `areca.config.ts` + `areca manifest push` when the site syncs its schema from code. |
61
73
  | `delete_collection` | agency | `key*` | Deletes the collection and its entries. Destructive. |
74
+ | `upsert_global` | agency | `key*`, `label*`, `fields*` (as `upsert_collection`, plus `group?` heading; no `slug` type), `seo?`, `previewPath?` (e.g. `/about`, `""` removes it), `enabled?` | Creates the site content section or **replaces** its fields, SEO setting, preview path and enabled flag. Keeps its content. |
75
+ | `delete_global` | agency | `key*` | Deletes the section. Its content is kept in the deleted-content archive. Destructive. |
62
76
 
63
77
  ## Prompt
64
78
 
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: areca-sdk
3
- description: Connects a website project to Areca CMS with the @areca-cms/site-sdk package. Covers setup, tokens and env vars, areca.config.ts collections and field types, the areca CLI (init, manifest push/pull, pull, run), reading pulled content, rich text, SEO head tags, draft preview routes and Go-live builds. Use when a project imports @areca-cms/site-sdk, has an areca.config.ts, mentions ARECA_* env vars, or the user wants to add, render or deploy Areca CMS content.
3
+ description: Connects a website project to Areca CMS with the @areca-cms/site-sdk package. Covers setup, tokens and env vars, areca.config.ts collections, site content globals and field types, the areca CLI (init, manifest push/pull, pull, run), typed content from the generated areca.gen.ts (import { content }), rich text, SEO head tags, draft preview routes and Go-live builds. Use when a project imports @areca-cms/site-sdk, has an areca.config.ts or areca.gen.ts, mentions ARECA_* env vars, or the user wants to add, render or deploy Areca CMS content.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.3.0"
6
+ version: "0.4.0"
7
7
  ---
8
8
 
9
9
  # Areca site SDK
@@ -14,13 +14,13 @@ Requirements: Node >= 22.6 (24 LTS recommended), ESM only. It works with any fra
14
14
 
15
15
  ## Mental model
16
16
 
17
- 1. `areca.config.ts` declares **collections** (content types) and their **fields**.
17
+ 1. `areca.config.ts` declares **collections** (content types), **globals** (single site content sections) and their **fields**.
18
18
  2. `areca manifest push` sends that schema to the CMS (needs the schema token).
19
- 3. Editors create entries in the CMS and **Go live**, which creates an immutable **publication** (snapshot).
20
- 4. `areca pull` downloads the publication into `src/generated/content/`.
21
- 5. Server code reads it with `readContent()` from `@areca-cms/site-sdk/pull`.
19
+ 3. Editors create entries in the CMS and **Go live**, which creates an immutable **publication** (snapshot) with the schema embedded.
20
+ 4. `areca pull` downloads the publication into `src/generated/content/` and writes `src/generated/content/areca.gen.ts`.
21
+ 5. Server code reads it through the typed `content` object from `areca.gen.ts`: `import { content } from "../generated/content/areca.gen"`.
22
22
 
23
- There are no singletons, blocks, locales, typegen, webhooks or runtime query API. A "page" is an entry in a collection. Filter, sort and paginate the returned arrays in your own code.
23
+ Typing is generated, not hand-written: after a pull, every collection, field, value, select option, reference and route is typed and autocompletes. Commit `areca.gen.ts`; never commit the pulled JSON. Single sections of values, such as company details or the home page, are site content (`globals`). There are no blocks, locales, webhooks or HTTP query API. A "page" is an entry in a collection, or a site content section. Queries run in memory on the pulled files, with `where`, `orderBy`, `limit`, `offset` and `with`.
24
24
 
25
25
  ## Setup
26
26
 
@@ -31,7 +31,7 @@ pnpm exec areca init # interactive; writes config, env files, s
31
31
  pnpm exec areca init --cms-url https://cms.example.com --token "$ARECA_SITE_TOKEN" --site-key acme-site --yes
32
32
  ```
33
33
 
34
- `init` writes `areca.config.ts` (including an `assets` block), `.env.example`, `.env.local` (`ARECA_CMS_URL`, `ARECA_SITE_TOKEN`), `.gitignore` entries (content and media folders), `src/generated/content/manifest.json`, and the package scripts `sync-manifest` and `content:pull`. It also wraps `dev` in `areca run --`. Use `--force` to overwrite an existing config. `--out` and `--config` change the paths. Media flags: `--assets-dir <dir>` (default `public/areca`), `--assets-public-path <path>` (only for folders outside `public/`), `--no-assets`. On an existing config, init inserts an `assets` block if there is none.
34
+ `init` writes `areca.config.ts` (including an `assets` block), `.env.example`, `.env.local` (`ARECA_CMS_URL`, `ARECA_SITE_TOKEN`), `.gitignore` entries (the pulled content folder as `<out>/*` with `!<out>/areca.gen.ts` so the types stay tracked, and the media folder), `src/generated/content/manifest.json`, and the package scripts `sync-manifest` and `content:pull`. It also wraps `dev` in `areca run --`. Use `--force` to overwrite an existing config. `--out` and `--config` change the paths. Media flags: `--assets-dir <dir>` (default `public/areca`), `--assets-public-path <path>` (only for folders outside `public/`), `--no-assets`. On an existing config, init inserts an `assets` block if there is none.
35
35
 
36
36
  | Env var | Needed for | Where the user gets it |
37
37
  | --- | --- | --- |
@@ -40,10 +40,11 @@ pnpm exec areca init --cms-url https://cms.example.com --token "$ARECA_SITE_TOKE
40
40
  | `ARECA_SCHEMA_TOKEN` | `manifest push` only | Settings > Developer > Rotate schema token. Keep it in the authoring env only, never in builds. |
41
41
  | `ARECA_SITE_KEY` | `manifest pull` without a config | The site key shown in Settings |
42
42
  | `ARECA_SNAPSHOT_ID` | pinned pulls during Go-live builds | Set by the CMS deploy; empty or `0` means current live |
43
+ | `ARECA_CONTENT_DIR` | the generated `content` object at run time | Optional. The content folder, when it is not the folder that was pulled into |
43
44
 
44
45
  The CLI loads `.env.local` and then `.env`, and existing process env wins. Library imports do **not** load env files.
45
46
 
46
- **Security:** both tokens are server secrets. Never put them in browser code or behind a `VITE_`, `NEXT_PUBLIC_` or `PUBLIC_` prefix, and never commit them. Only import `readContent` and the client in server or build code.
47
+ **Security:** both tokens are server secrets. Never put them in browser code or behind a `VITE_`, `NEXT_PUBLIC_` or `PUBLIC_` prefix, and never commit them. Only import the content object, `readContent` and the client in server or build code.
47
48
 
48
49
  ## Declare content
49
50
 
@@ -68,15 +69,42 @@ export default defineSiteContent({
68
69
  { key: "endsAt", label: "Ends", type: "datetime", after: "startsAt" },
69
70
  ],
70
71
  },
72
+ authors: {
73
+ route: "/authors/$slug",
74
+ fields: [{ key: "name", label: "Name", type: "text", required: true }],
75
+ },
71
76
  },
72
77
  });
73
78
  ```
74
79
 
75
- - Field types: `text`, `textarea`, `richtext`, `image`, `date`, `datetime`, `select` (`options`), `reference` (`collection`), `slug`, `boolean`, `number`, `url`. Optional per field: `required`, `description`, `after` (date or datetime key this must be on or after).
80
+ - Field types: `text`, `textarea`, `richtext`, `image`, `date`, `datetime`, `select` (`options`), `reference` (`collection`, required), `slug`, `boolean`, `number`, `url`. Optional per field: `required`, `description`, `after` (only on `date` or `datetime`, naming another date key in the same collection).
81
+ - `defineSiteContent` checks these at compile time: `options` only on `select`, `collection` required on `reference` and must be a collection key of the same config, `after` must name a date key, and `route` must contain `$slug`.
76
82
  - Keys match `^[A-Za-z][A-Za-z0-9_-]{0,63}$`. Collection keys must be unique case-insensitively. Reserved: `manifest`, `constructor`, `prototype`, `__proto__`, and Windows device names (`con`, `prn`, `aux`, `nul`, `com1`, `lpt1`, ...).
83
+ - Collection keys cannot be `resolve`, `href`, `preview`, `globals` or `previewGlobals` (any case, and `preview-globals` too), because the content object uses those names. Field keys may use them.
84
+ - In code, keys are camelCase: `blog-posts` is `content.blogPosts`, `cover_image` is `entry.data.coverImage`. The pull fails when two keys collide after this conversion (`blog-posts` and `blog_posts`).
77
85
  - A push is the **full** manifest. Collections left out are disabled (their entries are kept). The CMS adds a required `slug` field if one is missing. Limits: 100 collections, 100 fields each, 256 KiB.
78
86
  - `areca manifest pull --config areca.config.ts [--yes]` regenerates the config from the CMS. It replaces the whole file and keeps `siteKey` and `assets`. Without a TTY it needs `--yes` if the config already has collections.
79
87
 
88
+ ### Site content (globals)
89
+
90
+ ```ts
91
+ globals: {
92
+ companyInfo: {
93
+ label: "Company info",
94
+ fields: [
95
+ { key: "name", label: "Name", type: "text", required: true },
96
+ { key: "email", label: "Email", type: "text", group: "Contact" },
97
+ ],
98
+ },
99
+ homePage: { label: "Home page", seo: true, previewPath: "/", fields: [/* ... */] },
100
+ },
101
+ ```
102
+
103
+ - A global is one set of fields with no slug and no route. Its key follows the same identifier rules as a collection key and may not be `globals`, `previewGlobals`, `resolve`, `href` or `preview`.
104
+ - `seo: true` adds a `$seo` value. `previewPath` is the page that shows the draft. Both are optional.
105
+ - `group` is an editor heading only. The SDK value stays flat, and collection fields may not set `group`.
106
+ - Sections may reference collections, not the other way round. A push is a full sync: a section left out is disabled and its content is kept.
107
+
80
108
  ### Media (`assets`)
81
109
 
82
110
  ```ts
@@ -92,31 +120,42 @@ areca init [--cms-url <url>] [--token <t>] [--site-key <key>] [--out <dir>] [--c
92
120
  [--assets-dir <dir>] [--assets-public-path <path>] [--no-assets] [--yes] [--force]
93
121
  areca manifest push [config] # needs ARECA_SCHEMA_TOKEN (+ URL and site token)
94
122
  areca manifest pull [--config <path>] [--yes]
95
- areca pull [--out <dir>] [--publication <id>] [--config <path>] # default out: src/generated/content; media: public/areca
123
+ areca pull [--out <dir>] [--publication <id>] [--config <path>] [--types <path>] # default out: src/generated/content; types: <out>/areca.gen.ts; media: public/areca
96
124
  areca run -- <command> # run a command with .env.local/.env loaded
97
125
  ```
98
126
 
99
- `areca sync-manifest` is a deprecated alias for `manifest push`. Run the CLI as `pnpm exec areca`, `npx areca` or `yarn areca`. A pull fails until something has gone live in the CMS. To pull before every build, add `"prebuild": "areca pull"`.
127
+ `areca sync-manifest` is a deprecated alias for `manifest push`. Run the CLI as `pnpm exec areca`, `npx areca` or `yarn areca`. A pull fails until something has gone live in the CMS. To pull before every build, add `"prebuild": "areca pull"`. A pull prints a `Types:` line with the file it wrote, and it fails before writing anything when an entry does not match its schema or two keys collide in camelCase.
100
128
 
101
- ## Read content (server or build only)
129
+ ## Read typed content (server or build only)
102
130
 
103
131
  ```ts
104
- import { readContent, readContentManifest } from "@areca-cms/site-sdk/pull";
105
- import type { ImageValue, ReferenceValue, RichTextValue, SnapshotEntry } from "@areca-cms/site-sdk";
132
+ // src/lib/content.ts: server only
133
+ import { content } from "../generated/content/areca.gen";
106
134
 
107
- type Article = SnapshotEntry & {
108
- data: { title: string; body: RichTextValue; cover: ImageValue | null; author: ReferenceValue | null };
109
- };
135
+ export function getNewsArticles() {
136
+ return content.articles.all({ where: { category: "news" }, orderBy: { startsAt: "desc" }, limit: 10 });
137
+ }
110
138
 
111
- const dir = "src/generated/content";
112
- const manifest = readContentManifest(dir); // capture once for consistent multi-collection reads
113
- const articles = readContent<Article>(dir, "articles", manifest);
114
- const post = articles.find((a) => a.slug === slug);
139
+ export function findArticle(slug: string) {
140
+ return content.articles.get(slug); // Entry | undefined
141
+ }
142
+
143
+ // in a server function:
144
+ const author = content.resolve(article.data.author); // Entry<"authors"> | null
115
145
  ```
116
146
 
117
- - Entry: `{ id, slug, publishedAt, data, seo? }`. The field values live in `data`. You write the types yourself because there is no codegen.
118
- - `ImageValue` = `{ url, width|null, height|null, alt, mimeType }`, where `url` is the local path after `areca pull` (e.g. `/areca/hero-3f2a91c0.jpg`), or an absolute CMS URL with `assets.download: false`. Serve the media folder with the site (it is part of `public/` by default). `ReferenceValue` = `{ collection, slug, id }`. Optional and image fields can be `null` or missing, so check them.
119
- - `readContent` reads files at runtime, so ship `src/generated/content/` (including `.publications/`) with the server build or container. For TanStack Start, call it inside a `createServerFn` handler, never in route or component code.
147
+ - Each collection has `all(options?)`, `first(options?)`, `get(slug)`, `byId(id)` and `href(entryOrSlug)`. `get` and `byId` return `undefined` for an unknown value.
148
+ - `where`: a direct value means equality. Operators depend on the field type: text fields take `eq`, `ne`, `in`, `notIn`, `contains`; `select` takes `eq`, `ne`, `in`, `notIn`; `date`, `datetime` and `number` take `eq`, `ne`, `gt`, `gte`, `lt`, `lte`; `boolean` takes `eq`, `ne`; optional fields of any type take `isNull`. Several keys are AND-ed. Null, `''` and a missing key count as empty.
149
+ - `orderBy` takes an object such as `{ startsAt: "desc" }`, or an array for tie-breakers. Empty values sort last. Images, rich text and references are not sortable.
150
+ - `with: { author: true }` expands reference fields one level. `content.resolve(ref)` returns the target entry or `null`. `content.href(ref)` and `content.blogPosts.href(entry)` build routes, typed as template literals.
151
+ - `content.preview(token)` fetches a draft for a preview route. Check `result.complete` first: when true, `entry` is an `Entry`; when false, `entry` is a `DraftEntry` (every field optional) and `issues` lists the problems as `<field>: <problem>`.
152
+ - Entry: `{ id, slug, publishedAt, collection, data, seo? }`. Field values live in `data`, keyed by camelCase field name. No hand-written types are needed. Types are per collection, with `Entry<"articles">` for one collection.
153
+ - Optional fields can be `null`, missing or `''`, and the types show each case. Required fields are never empty.
154
+ - `ImageValue` = `{ url, width|null, height|null, alt, mimeType }`, where `url` is the local path after `areca pull` (e.g. `/areca/hero-3f2a91c0.jpg`), or an absolute CMS URL with `assets.download: false`. Serve the media folder with the site (it is part of `public/` by default). `ReferenceValue<C>` = `{ collection, slug, id }`.
155
+ - The content object reads files at runtime, so ship `src/generated/content/` (including `.publications/`) with the server build or container. For TanStack Start, call it inside a `createServerFn` handler, never in route or component code.
156
+ - Low-level: `readContent(dir, "blogPosts")` from `@areca-cms/site-sdk/pull` returns the typed entries of one collection and takes no type parameter. Use `readContentManifest(dir)` once when reading several collections from one publication.
157
+ - `content.globals.<camelKey>` returns a section as a flat object of its field values, plus `$seo` when SEO is on. Types: `CompanyInfoGlobal` (exported from `areca.gen.ts`), and `Global<K>`, `Globals`, `GlobalKey`, `GlobalData` and `GlobalSchema` from the package root. Without a registered schema, the value is typed as a loose record.
158
+ - `content.previewGlobals(token)` returns `{ globals, previewing, complete, issues }`. `previewing` is the camel key of the section the token targets (or `null`). A valid draft is in `globals`; an incomplete one keeps the published section, with `complete: false` and `issues` listing the problems. It also accepts a `PreviewResult` or `null`.
120
159
 
121
160
  ## Rich text
122
161
 
@@ -136,11 +175,13 @@ import { richTextDocument } from "@areca-cms/site-sdk";
136
175
  - Astro or plain SSR: `seoHeadHtml(entry.seo, options)` from `@areca-cms/site-sdk/seo` returns an escaped HTML string. `seoJsonLd(seo)` returns safe JSON-LD strings.
137
176
  - `seo` is optional. When `seo.summary`, `keyTakeaways` or `faq` exist, render them visibly on the page so the content matches the JSON-LD.
138
177
 
178
+ - Site content sections with `seo: true` have `$seo` on the section value, in the same shape as an entry's `seo`: `seoHead(content.globals.homePage.$seo)`.
179
+
139
180
  ## Preview, Go live and the client
140
181
 
141
182
  Read [references/production.md](references/production.md) when the user needs any of these:
142
183
 
143
- - A draft preview route: `getPreviewEntry` and `previewHeaders` at `/__preview/<collection>/<slug>?token=`.
184
+ - A draft preview route: `getPreviewEntry` and `previewHeaders` at `/__preview/<collection>/<slug>?token=`, or `content.preview(token)` for a typed result. A site content section previews on its `previewPath` with `?arecaPreview=<token>`, and `content.previewGlobals(token)` returns the page's values, with the draft in place when it is complete and `previewing` is the page's section.
144
185
  - The production health route that Go live waits for.
145
186
  - Docker or CI builds with `ARECA_SNAPSHOT_ID`.
146
187
  - The CMS settings needed: production URL, deployment target, publishing mode.
@@ -150,8 +191,9 @@ Read [references/api.md](references/api.md) for every export, type and signature
150
191
 
151
192
  ## Rules
152
193
 
153
- - Change the schema in `areca.config.ts` and then `manifest push`. Don't hand-edit generated content.
154
- - Never import `@areca-cms/site-sdk/pull` or a client with tokens into browser bundles.
194
+ - Change the schema in `areca.config.ts` and then `manifest push`, then go live and pull. Don't hand-edit generated content or `areca.gen.ts`; a pull rewrites both.
195
+ - Commit `areca.gen.ts`. Keep the pulled JSON out of Git.
196
+ - Never import the content object, `@areca-cms/site-sdk/pull` or a client with tokens into browser bundles.
155
197
  - Exclude `/__preview/*` from prerendering and caching.
156
198
  - The SDK doesn't create CMS sites or tokens. An agency admin creates the site in the CMS. Ask the user for the URL, site key and tokens instead of guessing them.
157
199
  - To edit content (not code) through an AI, use the Areca MCP server (see the `areca-mcp` skill).
@@ -6,10 +6,11 @@ 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 }` |
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`), returns `{ collections }` plus `globals` only when there are sections |
11
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`). |
12
+ | `getPreviewEntry` | `({ token: string; client?: ArecaClient }) => Promise<PreviewResult>` | `PreviewResult` = `PreviewEntryResult` `{ kind: 'entry', collection, slug, entry }` or `PreviewGlobalResult` `{ kind: 'global', global, entry }`. Needs `ARECA_CMS_URL` and `ARECA_SITE_TOKEN` (or pass `client`). |
13
+ | `toCamelKey` | `(raw: string) => string` | Converts a raw CMS key such as `blog-posts` or `company_info` to its code name (`blogPosts`, `companyInfo`) |
13
14
  | `previewHeaders` | `() => { 'X-Robots-Tag': 'noindex, nofollow'; 'Cache-Control': 'no-store' }` | Apply to preview responses |
14
15
  | `seoHead` | `(seo: SeoMeta \| null \| undefined, options?: SeoHeadOptions) => { title, meta, links, scripts }` | TanStack Router `head()` shape |
15
16
  | `seoHeadHtml` | `(seo, options?) => string` | Escaped `<head>` HTML |
@@ -29,17 +30,73 @@ ESM only. Import paths and their exports:
29
30
  | `fetchSnapshot(opts?: { publicationId?: string }): Promise<Snapshot>` | `GET /api/sites/snapshot/current[?publicationId=]` | site token |
30
31
  | `fetchPreview(token: string): Promise<PreviewResult>` | `GET /api/sites/preview?token=` | signed preview token |
31
32
 
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`.
33
+ Types from the main entry: `ArecaClient`, `ArecaClientOptions`, `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`.
34
+
35
+ 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).
36
+
37
+ Typed content types (main entry, generated against `Register`):
38
+
39
+ | Export | Meaning |
40
+ | --- | --- |
41
+ | `Register` | Interface that `areca.gen.ts` augments with `schema: typeof schema` |
42
+ | `ContentSchema`, `ContentSchemaCollection`, `ContentSchemaField` | Shape of the generated schema (`key`, `label?`, `route`, `fields`; field `key`, `type`, `required`, `options?`, `collection?`) |
43
+ | `RegisteredSchema` | The registered schema, or `ContentSchema` when nothing is registered |
44
+ | `CollectionKey<S>` | camelCase collection keys |
45
+ | `FieldKey<C, S>` | field keys of collection `C` |
46
+ | `FieldValue<C, F, S>` | value type of one field, with null, missing and `''` where the CMS sends them |
47
+ | `EntryData<C, S>` | `data` of collection `C` |
48
+ | `Entry<C, S>` | `{ id, slug, publishedAt, collection: C, data, seo? }` |
49
+ | `AnyEntry<S>` | union of the entries of every collection, discriminated on `collection` |
50
+ | `Where<C, S>` | `where` option: field keys with equality or operator objects |
51
+ | `OrderBy<C, S>` | `orderBy` option: sortable field keys with `'asc' \| 'desc'`, or an array |
52
+ | `QueryOptions<C, S>` | `where`, `orderBy`, `limit`, `offset`, `with` |
53
+ | `QueryResult<C, O, S>` | the entry type returned for query options `O`, with expanded references |
54
+ | `RoutePath<R>` | a route with `$slug` replaced by `${string}` |
55
+ | `Href<C, S>` | the route type of collection `C`, such as `` `/blog/${string}` `` |
56
+ | `Content<S, G>` | the content object: one property per collection, plus `resolve`, `href`, `preview`, `globals` and `previewGlobals` |
57
+ | `ContentGlobalSchema` | one site content section in the generated `globalSchema`: `key`, `label?`, `seo`, `previewPath?`, `fields` |
58
+ | `GlobalSchema`, `RegisteredGlobalSchema` | the generated `globalSchema` (or the loose type when nothing is registered) |
59
+ | `GlobalKey<G>` | camelCase section keys |
60
+ | `GlobalData<K, G>` | the field values of section `K` |
61
+ | `Global<K, G>` | one section: `GlobalData<K>`, plus `$seo: SeoMeta` when the section has `seo: true` |
62
+ | `Globals<G>` | the object returned by `content.globals` and `previewGlobals`, one `Global` per section |
63
+ | `GlobalsPreview<G>` | the result of `previewGlobals`: `{ globals, previewing, complete, issues }` |
64
+ | `CollectionApi<C, S>` | `all`, `first`, `get`, `byId` and `href` of one collection |
65
+ | `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[] }` |
66
+ | `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 |
67
+
68
+ ## `@areca-cms/site-sdk/content` (Node only)
69
+
70
+ | Export | Signature | Notes |
71
+ | --- | --- | --- |
72
+ | `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` |
73
+
74
+ `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).
75
+
76
+ ## Site content (globals)
77
+
78
+ | Export | Where | Notes |
79
+ | --- | --- | --- |
80
+ | `content.globals` | `areca.gen.ts` | `{ [camelKey]: Global }`. Read per call, from the current generation |
81
+ | `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` |
82
+ | `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 |
83
+ | `PulledContentManifest.globals` | `@areca-cms/site-sdk/pull` | Optional `string[]` of camel keys, absent when there are no sections |
84
+
85
+ `$seo` is only present when the section has `seo: true`.
33
86
 
34
87
  ## `@areca-cms/site-sdk/pull` (Node only)
35
88
 
36
89
  | Export | Signature | Notes |
37
90
  | --- | --- | --- |
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.` |
91
+ | `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 |
92
+ | `readContentManifest` | `(outDir: string) => PulledContentManifest` | `{ publicationId, hash, createdAt, collections counts, generation?, assets? }` |
93
+ | `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.` |
94
+
95
+ `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
96
 
42
- Type: `PullContentOptions`.
97
+ `ResolvedAssets` = `{ dir: string /* absolute */; publicPath: string }`.
98
+
99
+ `PullContentResult` = the snapshot as the CMS sent it, plus `schema`, `localAssets?` (`{ dir, total, downloaded }`), `warnings: string[]` and `typesPath`.
43
100
 
44
101
  ## Other subpaths
45
102
 
@@ -64,5 +121,3 @@ Type: `PullContentOptions`.
64
121
  ```
65
122
 
66
123
  `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? }`.
@@ -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)
@@ -85,5 +88,9 @@ await pullContent({ outDir: "src/generated/content", client });
85
88
  | `400` / `413` on push | Invalid schema, or the manifest is over 256 KiB |
86
89
  | `No content file for collection` | Run `areca pull`, then check the collection key |
87
90
  | `Unsafe collection or field identifier` | Rename the key to match `^[A-Za-z][A-Za-z0-9_-]{0,63}$` |
91
+ | `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. |
92
+ | `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. |
93
+ | `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. |
94
+ | `Warning: Publication <id> has no embedded schema` | An older publication. Types use the current schema. Go live again to embed it. |
88
95
  | `Expected publication X, received Y` | Pinned pull mismatch. Check `ARECA_SNAPSHOT_ID`. |
89
96
  | TS config import fails | Use Node 24, or run through `tsx` |