@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.
@@ -0,0 +1,169 @@
1
+ # Interview
2
+
3
+ Ask four batches, one at a time. Wait for the answer before you ask the next batch.
4
+
5
+ Rules for every batch:
6
+
7
+ - Number the questions. The user answers by number, for example "1 yes, 2 nl, 3 defaults".
8
+ - Show the default in brackets for each question.
9
+ - Say that the user can reply `defaults` to accept every default in the batch.
10
+ - Skip a question when you already detected the answer. Show the detected value and ask "correct?" instead.
11
+ - Keep each question to one line where you can.
12
+
13
+ ## Batch A: Project
14
+
15
+ Send this when step 0 is done. Fill in the detected values.
16
+
17
+ ```text
18
+ A. Project
19
+
20
+ 1. CMS URL? [detected: https://cms.example.com, or none. Local dev: http://localhost:3000]
21
+ 2. Site key (the CMS slug, lowercase, e.g. acme-site)? [detected: from areca.config.ts, or ask]
22
+ 3. Does this site exist in the CMS yet? [no]
23
+ If no, I will walk you through creating it. You will need the production URL field: use http://localhost:<dev port> for now.
24
+ 4. Framework: <detected>. Correct? [yes]
25
+ 5. Package manager: <detected>. Correct? [yes]
26
+ 6. Dev port: <detected, e.g. 3001>. [keep]
27
+ 7. Content folder for pulled content? [src/generated/content]
28
+ 8. Types file (areca.gen.ts)? [src/generated/content/areca.gen.ts]
29
+ 9. Commit the generated types file? [yes; the pulled JSON stays out of Git]
30
+ 10. Download images and files into the repo on pull? [yes]
31
+ 10a. Folder for them? [public/areca]
32
+ 10b. URL path the site serves that folder from? [only asked when the folder is not under public/]
33
+ 11. Schema source: connect the CMS over MCP (the schema is created in the CMS, then saved to areca.config.ts) or write areca.config.ts in code? [MCP, if you are an agency admin; otherwise code]
34
+
35
+ Reply "defaults" to accept all of A.
36
+ ```
37
+
38
+ Notes for the agent:
39
+
40
+ - Q3: if the site does not exist, read the "Create the site" section of `connect.md` before you go on.
41
+ - Q11: if MCP is not connected, ask only this: "Do you want to connect the CMS over MCP now?" If yes, go to step 1. If no, code.
42
+ - If the CMS URL is `localhost`, say that a remote CMS needs its own site token.
43
+
44
+ ## Batch B: Content model
45
+
46
+ Ask what the site needs. Then go through each item with its recipe in `recipes.md`.
47
+
48
+ ```text
49
+ B. Content model
50
+
51
+ 1. What does the website need? Name the content in your own words.
52
+ Examples: blog posts, team members, vacancies, services, case studies, events, FAQ.
53
+ 2. Which company pages does the site need?
54
+ [company info, home page, about page, footer]
55
+ 3. Do you want any of these extras? [all yes]
56
+ a. Categories for blog posts (a collection)
57
+ b. Authors for blog posts (a collection, used as a reference)
58
+ c. Departments for vacancies (a collection, used as a reference)
59
+ d. Navigation links (a collection, see the note in recipes.md)
60
+ e. SEO on the home and about pages (on by default)
61
+ 4. Routes: each collection gets a route. These are the defaults from the recipe, for example /blog/$slug and /vacancies/$slug. Change them?
62
+ 5. Anything that does not fit the recipes? Describe the fields in your own words. I will map them to field types.
63
+ 6. Company details for the company info section: name, email, phone and address. [name: the client's name, the rest empty]
64
+ Give only what you know. I will leave the others empty. I will not guess a founding year, a phone number or an address.
65
+
66
+ Reply "defaults" to accept the recipe fields and all the extras in B (the company details stay empty except the name).
67
+ ```
68
+
69
+ For each item the user names:
70
+
71
+ 1. Find its recipe in `recipes.md`. If no recipe fits, build one from the field types in `areca-sdk`.
72
+ 2. Show the default fields in a short list. Ask: "Keep these, add, or remove?" Ask only one question per item.
73
+ 3. Note the translatable choice for each text field. Use the recipe defaults.
74
+
75
+ Do not invent extra fields. Add what the user asked for and the recipe defaults.
76
+ - A declined extra removes the fields that point to it. For example, no authors means no `author` field on posts. Show each removal in the plan.
77
+ - Never invent a fact. A value the user did not give stays empty when the field is optional.
78
+
79
+ ## Batch C: Languages
80
+
81
+ Skip this batch when the user said "single language" in batch B or when the project has no i18n. The default is one language.
82
+
83
+ ```text
84
+ C. Languages
85
+
86
+ 1. One language or several? [one]
87
+ 2. If several: the main language (the fallback) and all languages in display order?
88
+ [main: <detected or en>, all: <main> only]
89
+ 3. Route for each collection in each extra language. Default: the same route in every language. The site adds the language prefix itself (for example /nl/blog/...). Ask per collection only when the user wants different words, for example /journal in English and /journaal in Dutch.
90
+ 4. Which languages must be complete before an item can be marked ready? [all extra languages, for every collection and section]
91
+ 5. Which fields are the same in every language (names, prices, contact details, URLs, emails)? [the recipe defaults]
92
+ 6. i18n approach:
93
+ - If the project already uses a library, use it. Only Paraglide is wired by the SDK. For another library, the content is read with an explicit locale.
94
+ - If the project has none: Paraglide is recommended. It needs a new package, and the recipe is in `frameworks.md` (TanStack Start is verified). Install it only if you say yes.
95
+ The lighter alternative is the explicit locale: no new package, and the locale is passed to each read.
96
+ [Paraglide]
97
+
98
+ Reply "defaults" to accept all of C (one language, so C is skipped).
99
+ ```
100
+
101
+ Notes for the agent:
102
+
103
+ - Languages cannot be turned off once set in the CMS. Say so before Q2 is confirmed.
104
+ - Do not install any i18n library without a yes to Q6.
105
+
106
+ ## Batch D: Build and preview
107
+
108
+ ```text
109
+ D. Build and preview
110
+
111
+ 1. Pages for each collection:
112
+ a. Connect to existing pages. I change the data source only.
113
+ b. Build simple pages (list and detail) using the site's own components. No styling beyond what the site uses.
114
+ c. Skip for now.
115
+ [b when the project has no pages for the collection, otherwise a]
116
+ 2. Connect the home page and the about page to their sections? [yes, when the plan has them]
117
+ 3. Draft preview route (so editors can see drafts in the site)? [yes]
118
+ It needs the production URL in the CMS. A localhost URL is fine for development.
119
+ 4. Run a content pull before every build and before the dev server? [yes, prebuild only; predev only after the first release]
120
+ 5. Will you deploy with go-live builds (Dokploy) later? [no]
121
+ If yes, I add the health route now. Read the areca-sdk skill, references/production.md.
122
+ 6. First release mode: demo or production? [demo]
123
+ Demo does not deploy and never shows to visitors. Production needs a deployment target and a health check URL. Use demo for the first release.
124
+
125
+ Reply "defaults" to accept all of D.
126
+ ```
127
+
128
+ ## Plan format
129
+
130
+ After batch D, show the plan. Keep it short. Then ask: "Reply yes to confirm, or tell me what to change."
131
+
132
+ ```text
133
+ Plan
134
+
135
+ Project
136
+ - Framework: TanStack Start, pnpm, dev port 3001
137
+ - Site key: acme-site (exists in the CMS: yes)
138
+ - CMS: https://cms.example.com (schema over MCP)
139
+
140
+ Content folder
141
+ - src/generated/content (types committed, media in public/areca, downloaded on pull)
142
+
143
+ Collections
144
+ - posts: /blog/$slug, 9 fields, author -> authors
145
+ - authors: /team/$slug, 5 fields
146
+ - ...
147
+
148
+ Sections
149
+ - companyInfo (no SEO), homePage (SEO, preview /), aboutPage (SEO, preview /about)
150
+
151
+ Languages
152
+ - none, or: en (main), nl. Routes: /blog/$slug and /nl/blog/$slug. Required: nl.
153
+
154
+ i18n
155
+ - none, or Paraglide (needs your yes to install)
156
+
157
+ Pages
158
+ - posts: list and detail, simple pages
159
+ - home and about: connected to their sections
160
+
161
+ Preview route: yes. Prebuild pull: yes.
162
+ First release: demo mode.
163
+ ```
164
+
165
+ Keep the plan to the facts. Mark every item the user has not confirmed.
166
+
167
+ ## Recording answers
168
+
169
+ Keep the answers in your working notes for the rest of the run. Re-read them before step 3 and step 4. If the user changes an answer later, update the plan and show the changed lines only.
@@ -0,0 +1,243 @@
1
+ # Recipes
2
+
3
+ Starting points for collections and sections. Use them in batch B and in the approval table (step 3). Adapt the fields to the client. Keep the list short.
4
+
5
+ Field rules that apply to every recipe (see the `areca-sdk` skill for the full list):
6
+
7
+ - Keys (collections, sections and fields) start with a letter. They use letters, digits, `_` or `-`, and are 64 characters at most.
8
+ - Collection and section keys cannot be `resolve`, `href`, `preview`, `globals`, `previewGlobals`, `in`, `languages` or `matchLocale`, in any case. Field keys may use them. Also avoid `manifest`, `constructor`, `prototype` and the Windows device names (`con`, `aux`, `nul`, `com1` and so on).
9
+ - Every collection needs a `route` with `$slug`. Even a collection with no page (for example testimonials) needs one.
10
+ - Every collection gets a `slug` field. Add it in the config, as the examples do.
11
+ - Text, textarea, richtext and url are translatable by default on a multilingual site. Names, prices, contact details and shop or apply links are usually not. Mark them `translatable: false`.
12
+ - The SDK has no list field. Use one of these instead: a collection with a number field for order, a reference, or several plain fields.
13
+ - `group` works only on sections (globals), not on collection fields.
14
+ - A `date` or `datetime` field is content only. It does not schedule an entry.
15
+ - When the user declines an extra (for example authors or departments), remove every field that points to it (`posts.author`, `vacancies.department`, `posts.category`, `homePage.featuredPost`). Show each removal in the approval table, for example "removed: author (authors declined)".
16
+ - Never invent facts for a field (a founding year, a phone number, an address, a salary). If the interview did not give the value, leave an optional field empty. Use placeholder text only for copy, and say so.
17
+
18
+ Field lines below use: `key` (type, required, notes). `slug` is always `slug` (type slug, required).
19
+
20
+ ## Collections
21
+
22
+ ### Blog posts
23
+
24
+ Collection `posts`. Route `/blog/$slug`. Label "Blog posts" (or the client's word, such as "News" or "Journal").
25
+
26
+ - `slug` (slug, required)
27
+ - `title` (text, required)
28
+ - `excerpt` (textarea), short summary for lists and SEO
29
+ - `body` (richtext, required)
30
+ - `coverImage` (image)
31
+ - `category` (select: `news`, `guides`, `updates`). Use a reference to categories instead if categories need their own page.
32
+ - `tags` (text). Comma-separated, with a description that says so. There is no list type.
33
+ - `author` (reference to `authors`)
34
+ - `publishedAt` (datetime). Content only, does not schedule.
35
+
36
+ Suggested extras: categories, authors, a home page section that features the latest post.
37
+
38
+ ### Categories
39
+
40
+ Collection `categories`. Route `/blog/category/$slug`.
41
+
42
+ - `slug` (slug, required)
43
+ - `name` (text, required)
44
+ - `description` (textarea)
45
+
46
+ ### Authors (team members)
47
+
48
+ Collection `authors`. Route `/team/$slug`. Label "Team".
49
+
50
+ - `slug` (slug, required)
51
+ - `name` (text, required, `translatable: false`)
52
+ - `role` (text)
53
+ - `bio` (textarea)
54
+ - `portrait` (image)
55
+
56
+ Used by posts (author reference) and projects.
57
+
58
+ ### Vacancies (job openings)
59
+
60
+ Collection `vacancies`. Route `/vacancies/$slug`. Label "Vacancies".
61
+
62
+ - `slug` (slug, required)
63
+ - `title` (text, required)
64
+ - `summary` (textarea, required). Shown on the list page and in SEO.
65
+ - `description` (richtext, required). Role, tasks, requirements.
66
+ - `department` (reference to `departments`)
67
+ - `location` (text). For example "Amsterdam (hybrid)".
68
+ - `employmentType` (select: `full-time`, `part-time`, `contract`, `internship`, `freelance`). Shared across languages.
69
+ - `hoursPerWeek` (number)
70
+ - `salaryRange` (text, `translatable: false`). For example "3,000 to 3,800 EUR per month". Keep it as text, the SDK has no currency type.
71
+ - `remote` (boolean). Open to remote work.
72
+ - `startDate` (date)
73
+ - `closingDate` (date). When it closes, the page can say "closed".
74
+ - `applyUrl` (url, `translatable: false`). Link to the applicant tracking system.
75
+ - `applyEmail` (text, `translatable: false`)
76
+
77
+ Suggested extras: departments, and a `/vacancies` list page on the site. Entries already have SEO in the CMS, so there is no SEO flag to set here.
78
+
79
+ ### Departments
80
+
81
+ Collection `departments`. Route `/departments/$slug`.
82
+
83
+ - `slug` (slug, required)
84
+ - `name` (text, required)
85
+ - `summary` (textarea)
86
+ - `body` (richtext)
87
+ - `contactEmail` (text, `translatable: false`)
88
+
89
+ ### Services
90
+
91
+ Collection `services`. Route `/services/$slug`.
92
+
93
+ - `slug` (slug, required)
94
+ - `title` (text, required)
95
+ - `summary` (textarea)
96
+ - `body` (richtext)
97
+ - `icon` (image)
98
+ - `order` (number). Lower numbers show first. Use `orderBy` in the code.
99
+
100
+ ### Projects and case studies
101
+
102
+ Collection `projects`. Route `/work/$slug`. Label "Projects" or "Work".
103
+
104
+ - `slug` (slug, required)
105
+ - `title` (text, required)
106
+ - `client` (text). The client name in the project. Use `translatable: false` if it is a name.
107
+ - `summary` (textarea)
108
+ - `coverImage` (image)
109
+ - `body` (richtext)
110
+ - `completedOn` (date)
111
+ - `services` (reference to `services`)
112
+
113
+ ### Testimonials
114
+
115
+ Collection `testimonials`. Route `/testimonials/$slug`. Even when the site shows them on one page, the route is still required.
116
+
117
+ - `slug` (slug, required)
118
+ - `quote` (textarea, required)
119
+ - `name` (text, required, `translatable: false`)
120
+ - `role` (text)
121
+ - `company` (text, `translatable: false`)
122
+ - `photo` (image)
123
+ - `order` (number)
124
+
125
+ ### FAQ
126
+
127
+ Collection `faqs`. Route `/faq/$slug`.
128
+
129
+ - `slug` (slug, required)
130
+ - `question` (text, required)
131
+ - `answer` (richtext, required)
132
+ - `order` (number)
133
+
134
+ ### Events
135
+
136
+ Collection `events`. Route `/events/$slug`.
137
+
138
+ - `slug` (slug, required)
139
+ - `title` (text, required)
140
+ - `summary` (textarea)
141
+ - `body` (richtext)
142
+ - `coverImage` (image)
143
+ - `startsAt` (datetime, required)
144
+ - `endsAt` (datetime, `after: "startsAt"`)
145
+ - `location` (text)
146
+ - `registrationUrl` (url, `translatable: false`)
147
+
148
+ ### Navigation links (optional)
149
+
150
+ Collection `navigationLinks`. Route `/navigation/$slug`. The site never renders this as a page. It only reads the links.
151
+
152
+ - `slug` (slug, required)
153
+ - `label` (text, required)
154
+ - `url` (url, `translatable: false`)
155
+ - `order` (number)
156
+
157
+ Mention the trade-off to the user. A menu that rarely changes is simpler in code. Use this collection when editors must change the menu themselves.
158
+
159
+ ## Sections (site content)
160
+
161
+ Sections are single sets of values. They have no slug and no route. Set `seo: true` only on pages that have their own SEO (home, about). Set `previewPath` to the page that shows the section, so editors can preview drafts there.
162
+
163
+ ### Company info
164
+
165
+ Section `companyInfo`. Label "Company info". No SEO.
166
+
167
+ Ask the user for the company details in batch B: name, email, phone and address. Use only what they give. Leave the others empty. Do not guess a founding year, a phone number or an address.
168
+
169
+ - `name` (text, required, `translatable: false`)
170
+ - `tagline` (textarea). The site's default description in the root head.
171
+ - `email` (text, `translatable: false`, `group: "Contact"`)
172
+ - `phone` (text, `translatable: false`, `group: "Contact"`)
173
+ - `address` (textarea, `group: "Contact"`)
174
+ - `mapUrl` (url, `translatable: false`, `group: "Contact"`)
175
+ - `instagramUrl` (url, `translatable: false`, `group: "Social"`)
176
+ - `linkedinUrl` (url, `translatable: false`, `group: "Social"`)
177
+ - `foundedYear` (number)
178
+
179
+ ### Home page
180
+
181
+ Section `homePage`. Label "Home page". `seo: true`. `previewPath: "/"`.
182
+
183
+ - `heroEyebrow` (text)
184
+ - `heroTitle` (text, required)
185
+ - `heroIntro` (textarea, required)
186
+ - `heroImage` (image)
187
+ - `ctaLabel` (text)
188
+ - `ctaUrl` (url, `translatable: false`)
189
+ - `featuredPost` (reference to `posts`), when the plan has posts
190
+
191
+ ### About page
192
+
193
+ Section `aboutPage`. Label "About page". `seo: true`. `previewPath: "/about"`.
194
+
195
+ - `heading` (text, required)
196
+ - `body` (richtext, required)
197
+ - `image` (image)
198
+
199
+ ### Footer
200
+
201
+ Section `footer`. Label "Footer". No SEO. Keep it small.
202
+
203
+ - `footerNote` (textarea). Short line under the links.
204
+ - `copyrightName` (text). Shown after the year.
205
+ - `legalNotice` (richtext). Terms or imprint text.
206
+
207
+ Use `companyInfo` for the address and contact fields. Do not repeat them.
208
+
209
+ ## SEO defaults
210
+
211
+ - Every entry has SEO in the CMS. Editors fill it in, and the SDK gives `entry.seo`. Use `seoHead` or `seoHeadHtml` in every entry page.
212
+ - Sections get SEO only with `seo: true`. Use it for home and about. Read the value from `content.globals.<key>.$seo`.
213
+ - The site name and the default description come from `companyInfo`. Use them in the root layout.
214
+ - Do not add SEO fields as collection fields. Use the CMS SEO panel.
215
+
216
+ ## Routes and languages
217
+
218
+ A collection has one `route` in the main language, for example `/blog/$slug`. A language that uses other words gets its own pattern in `routes`, for example `routes: { nl: '/vacatures/$slug' }`. Read from the SDK source (`packages/site-sdk/src/content.ts`):
219
+
220
+ - The SDK uses `route` and each `routes[locale]` value exactly as written. It never adds a language prefix. Write `/vacatures/$slug`, not `/nl/vacatures/$slug`. A prefix in `routes` is doubled once the site adds `/nl`.
221
+ - `content.posts.href(slug)` returns the route of the language in use, with `$slug` replaced. Example: `content.posts.href('my-post')` gives `/blog/my-post`. `content.in('nl').vacancies.href('dev')` gives `/vacatures/dev`. A string slug uses the language from `in(locale)` if set, else the locale function (Paraglide's `getLocale` when it is wired), else the main language. `href(entry)` uses the entry's own language.
222
+ - `entry.alternates` maps each complete language tag to its route, again with no prefix: `{ en: '/vacancies/dev', nl: '/vacatures/dev' }`.
223
+ - The site adds the prefix. The main language has none. Every other language is `/<tag>` followed by the route, so the Dutch vacancy is `/nl/vacatures/dev`. With Paraglide, the router rewrite does this (see [frameworks.md](frameworks.md)). Without Paraglide, add the prefix in code.
224
+
225
+ **The rule:** `href()` and `alternates` never include the language prefix. Add `/<tag>` yourself for every language except the main one.
226
+
227
+ ## Route suggestions
228
+
229
+ | Collection | Route | Page type |
230
+ | --- | --- | --- |
231
+ | posts | `/blog/$slug` | list at `/blog`, detail |
232
+ | categories | `/blog/category/$slug` | list of posts, filtered |
233
+ | authors | `/team/$slug` | list at `/team`, detail |
234
+ | vacancies | `/vacancies/$slug` | list at `/vacancies`, detail |
235
+ | departments | `/departments/$slug` | detail, linked from vacancies |
236
+ | services | `/services/$slug` | list at `/services`, detail |
237
+ | projects | `/work/$slug` | list at `/work`, detail |
238
+ | testimonials | `/testimonials/$slug` | route kept, shown on home |
239
+ | faqs | `/faq/$slug` | list at `/faq` |
240
+ | events | `/events/$slug` | list at `/events`, detail |
241
+ | navigationLinks | `/navigation/$slug` | none (read only) |
242
+
243
+ Sections have no route. Their pages are `/` (home), `/about`, and the footer appears on every page.
@@ -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, multilingual sites (languages, locales, hreflang), 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.5.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**.
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`.
17
+ 1. `areca.config.ts` declares **collections** (content types), **globals** (single site content sections) and their **fields**.
18
+ 2. `areca manifest push` sends that schema to the CMS (needs the schema token). It is refused while a removed field still holds content, until the removal is confirmed.
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, 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,19 +31,20 @@ 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
  | --- | --- | --- |
38
38
  | `ARECA_CMS_URL` | everything | The CMS base URL, e.g. `https://cms.example.com` |
39
- | `ARECA_SITE_TOKEN` | pull, client, preview | CMS: site Settings > Developer > build token (shown once; rotate to get a new one) |
39
+ | `ARECA_SITE_TOKEN` | reads (pull, `manifest pull`, the client's snapshot calls) | CMS: site Settings > Developer > build token (shown once; rotate to get a new one) |
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
- The CLI loads `.env.local` and then `.env`, and existing process env wins. Library imports do **not** load env files.
45
+ The CLI loads `.env.local` and then `.env`, and existing process env wins. Library imports do **not** load env files. Each call reads only the credential it sends: a schema push needs the schema token but not the site token, and a preview needs neither (its token is signed).
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,14 +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
- - `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.
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`. It prints the changes first. It asks (`--yes` without a TTY) only when something would be removed: a collection, a site content section, a field, a language, or the main language. Additions and changes are written without a question. A file that already matches writes nothing. A hand-written config that defines collections still asks before it is replaced.
87
+ - **Removing a field that still holds content** deletes that content. `manifest push` lists the affected fields (`key.field`, with the entry count) and pushes nothing until they are confirmed: `--confirm-drop <key.field>` per field (repeatable), or `--yes` for all. In code, `syncManifest(config, { confirmDropFields: ["articles.subtitle"] })` does the same. An unconfirmed push throws `FieldDropsRequiredError` (exported; `.drops` holds the `FieldDrop` list, each `{ kind, key, field, entries }`). **Ask the user before confirming a drop**; never add `--yes` or `--confirm-drop` on your own.
88
+
89
+ ### Site content (globals)
90
+
91
+ ```ts
92
+ globals: {
93
+ companyInfo: {
94
+ label: "Company info",
95
+ fields: [
96
+ { key: "name", label: "Name", type: "text", required: true },
97
+ { key: "email", label: "Email", type: "text", group: "Contact" },
98
+ ],
99
+ },
100
+ homePage: { label: "Home page", seo: true, previewPath: "/", fields: [/* ... */] },
101
+ },
102
+ ```
103
+
104
+ - 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`.
105
+ - `seo: true` adds a `$seo` value. `previewPath` is the page that shows the draft. Both are optional.
106
+ - `group` is an editor heading only. The SDK value stays flat, and collection fields may not set `group`.
107
+ - 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.
79
108
 
80
109
  ### Media (`assets`)
81
110
 
@@ -90,33 +119,48 @@ assets: { download: true, dir: "public/areca" /*, publicPath: "/areca" */ }, //
90
119
  ```text
91
120
  areca init [--cms-url <url>] [--token <t>] [--site-key <key>] [--out <dir>] [--config <path>]
92
121
  [--assets-dir <dir>] [--assets-public-path <path>] [--no-assets] [--yes] [--force]
93
- areca manifest push [config] # needs ARECA_SCHEMA_TOKEN (+ URL and site token)
122
+ areca manifest push [config] [--confirm-drop <key.field>]... [--yes] # needs ARECA_SCHEMA_TOKEN and ARECA_CMS_URL
94
123
  areca manifest pull [--config <path>] [--yes]
95
- areca pull [--out <dir>] [--publication <id>] [--config <path>] # default out: src/generated/content; media: public/areca
124
+ areca pull [--out <dir>] [--publication <id>] [--config <path>] [--types <path>] # default out: src/generated/content; types: <out>/areca.gen.ts; media: public/areca
96
125
  areca run -- <command> # run a command with .env.local/.env loaded
97
126
  ```
98
127
 
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"`.
128
+ `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
129
 
101
- ## Read content (server or build only)
130
+ ## Read typed content (server or build only)
102
131
 
103
132
  ```ts
104
- import { readContent, readContentManifest } from "@areca-cms/site-sdk/pull";
105
- import type { ImageValue, ReferenceValue, RichTextValue, SnapshotEntry } from "@areca-cms/site-sdk";
133
+ // src/lib/content.ts: server only
134
+ import { content } from "../generated/content/areca.gen";
135
+
136
+ export function getNewsArticles() {
137
+ return content.articles.all({ where: { category: "news" }, orderBy: { startsAt: "desc" }, limit: 10 });
138
+ }
106
139
 
107
- type Article = SnapshotEntry & {
108
- data: { title: string; body: RichTextValue; cover: ImageValue | null; author: ReferenceValue | null };
109
- };
140
+ export function findArticle(slug: string) {
141
+ return content.articles.get(slug); // Entry | undefined
142
+ }
110
143
 
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);
144
+ // in a server function:
145
+ const author = content.resolve(article.data.author); // Entry<"authors"> | null
115
146
  ```
116
147
 
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.
148
+ - Each collection has `all(options?)`, `first(options?)`, `get(slug)`, `byId(id)` and `href(entryOrSlug)`. `get` and `byId` return `undefined` for an unknown value.
149
+ - `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.
150
+ - `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.
151
+ - `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.
152
+ - `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>`.
153
+ - 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.
154
+ - Optional fields can be `null`, missing or `''`, and the types show each case. Required fields are never empty.
155
+ - `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 }`.
156
+ - 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.
157
+ - 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.
158
+ - `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.
159
+ - `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`.
160
+
161
+ ## Languages
162
+
163
+ Only when the CMS site has content languages. Set `languages`, `routes`, `requiredLanguages` and `translatable` in `areca.config.ts`. Reads pick a language with `content.in(locale)`, a per-call `{ locale, fallback }` or the `createContent({ locale })` resolver, and fall back to the main language. Entries carry `locale`, `isFallback`, `locales` and `alternates`; globals carry `$locale` and `$isFallback`. `seoHead` takes `locale` and `alternates` for hreflang, and `content.matchLocale` picks a visitor's language. `Href` is the union of every language's route; `content.in(locale)` narrows `href()` to that language's route. A pulled media URL is rewritten in every language view, so each language gets local files. Read [references/languages.md](references/languages.md) for the Paraglide setup, the other i18n libraries, preview with `locale`, and every export.
120
164
 
121
165
  ## Rich text
122
166
 
@@ -136,22 +180,25 @@ import { richTextDocument } from "@areca-cms/site-sdk";
136
180
  - 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
181
  - `seo` is optional. When `seo.summary`, `keyTakeaways` or `faq` exist, render them visibly on the page so the content matches the JSON-LD.
138
182
 
183
+ - 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)`.
184
+
139
185
  ## Preview, Go live and the client
140
186
 
141
187
  Read [references/production.md](references/production.md) when the user needs any of these:
142
188
 
143
- - A draft preview route: `getPreviewEntry` and `previewHeaders` at `/__preview/<collection>/<slug>?token=`.
189
+ - 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
190
  - The production health route that Go live waits for.
145
191
  - Docker or CI builds with `ARECA_SNAPSHOT_ID`.
146
192
  - The CMS settings needed: production URL, deployment target, publishing mode.
147
193
  - Direct use of `createArecaClient` and error messages.
148
194
 
149
- Read [references/api.md](references/api.md) for every export, type and signature.
195
+ Read [references/api.md](references/api.md) for every export, type and signature, and [references/languages.md](references/languages.md) for languages.
150
196
 
151
197
  ## Rules
152
198
 
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.
199
+ - Change the schema in `areca.config.ts` and then `manifest push`, then go live and pull. Confirm a field removal or a language removal with the user first: both can delete content or stop a language being served. Don't hand-edit generated content or `areca.gen.ts`; a pull rewrites both.
200
+ - Commit `areca.gen.ts`. Keep the pulled JSON out of Git.
201
+ - Never import the content object, `@areca-cms/site-sdk/pull` or a client with tokens into browser bundles.
155
202
  - Exclude `/__preview/*` from prerendering and caching.
156
203
  - 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
204
  - To edit content (not code) through an AI, use the Areca MCP server (see the `areca-mcp` skill).