@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.
- package/CHANGELOG.md +18 -0
- package/README.md +2 -1
- package/dist/cli.js +3 -1
- package/dist/skills.d.ts +8 -3
- package/dist/skills.js +19 -12
- package/package.json +1 -1
- package/skills/areca-mcp/SKILL.md +23 -8
- package/skills/areca-mcp/references/content.md +8 -1
- package/skills/areca-mcp/references/languages.md +86 -0
- package/skills/areca-mcp/references/tools.md +32 -9
- package/skills/areca-quickstart/SKILL.md +245 -0
- package/skills/areca-quickstart/references/connect.md +123 -0
- package/skills/areca-quickstart/references/first-release.md +181 -0
- package/skills/areca-quickstart/references/frameworks.md +533 -0
- package/skills/areca-quickstart/references/interview.md +169 -0
- package/skills/areca-quickstart/references/recipes.md +243 -0
- package/skills/areca-sdk/SKILL.md +81 -34
- package/skills/areca-sdk/references/api.md +78 -15
- package/skills/areca-sdk/references/languages.md +177 -0
- package/skills/areca-sdk/references/production.md +8 -0
|
@@ -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),
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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`.
|
|
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]
|
|
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
|
-
|
|
105
|
-
import
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
140
|
+
export function findArticle(slug: string) {
|
|
141
|
+
return content.articles.get(slug); // Entry | undefined
|
|
142
|
+
}
|
|
110
143
|
|
|
111
|
-
|
|
112
|
-
const
|
|
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
|
-
-
|
|
118
|
-
- `
|
|
119
|
-
- `
|
|
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
|
|
154
|
-
-
|
|
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).
|