@sleekcms/sync 1.0.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/dist/AGENT.md ADDED
@@ -0,0 +1,568 @@
1
+ # SleekCMS — Site Builder Reference
2
+
3
+ Cloud-based headless CMS with an integrated static site builder. Files sync automatically — every save triggers a rebuild and deploy. No Git, no servers, no manual builds.
4
+
5
+ ---
6
+
7
+ ## How It Works
8
+
9
+ Each page = **model** (schema) + **template** (EJS) + optional **layout** (EJS wrapper).
10
+
11
+ The **file name** (key) links a model to its template and determines the URL path.
12
+
13
+ Sync/build errors are written to `sync-errors.log` in the workspace root. If a save does not render or deploy as expected, check that file first.
14
+
15
+ ---
16
+
17
+ ## File Naming Convention
18
+
19
+ Keys are lowercase, dash-separated. For pages, `_` in the key maps to `/` in the URL. A `+` suffix marks a collection.
20
+
21
+ | File key | URL |
22
+ |---|---|
23
+ | `_index` | `/` (home) |
24
+ | `about` | `/about` |
25
+ | `blog+` | `/blog/<slug>` (one page per entry) |
26
+ | `docs_getting-started` | `/docs/getting-started` |
27
+
28
+ The keys for model, template, and content file are the same — if the model is a **collection**, the `+` suffix is part of the key and must appear on **every** related file.
29
+
30
+ Examples:
31
+ - Collection page `blog`: `models/pages/blog+.model`, `pages/blog+.ejs`, `content/pages/blog+/<slug>.json`
32
+ - Collection entry `testimonials`: `models/entries/testimonials+.model`, `entries/testimonials+.ejs`, `content/entries/testimonials+.json`
33
+ - Single page `about`: `models/pages/about.model`, `pages/about.ejs`, `content/pages/about.json` (no `+`)
34
+
35
+ ---
36
+
37
+ ## Folder Structure within src/
38
+
39
+ ```
40
+ /models/pages/<key>.model Page content models
41
+ /models/entries/<key>.model Entry content models
42
+ /models/blocks/<key>.model Block component models
43
+
44
+ /pages/<key>.ejs Page templates
45
+ /entries/<key>.ejs Entry templates
46
+ /blocks/<key>.ejs Block templates
47
+ /layouts/<name>.ejs Layout wrappers
48
+
49
+ /css/<name>.css Stylesheets (require head injection)
50
+ /css/tailwind.css Tailwind CSS (auto-compiled, auto-injected)
51
+ /js/<name>.js Scripts (require head injection)
52
+
53
+ /content/pages/<key>.json Content for a single (non-list) page
54
+ /content/pages/<key>/<slug>.json Content for one item of a collection page (<key> ends with +)
55
+ /content/entries/<key>.json Content for a single entry (object)
56
+ /content/entries/<key+>.json Content for a collection entry (array of objects; <key>+ matches the model filename)
57
+
58
+ /content/images.json Site-level reusable images (handle → shortcut)
59
+ ```
60
+
61
+ > **Tailwind**: Creating `/css/tailwind.css` enables Tailwind. It is compiled and injected automatically — do NOT add it via `link()`.
62
+ > All other CSS/JS files must be included via `link()` or `script()`.
63
+
64
+ ---
65
+
66
+ ## Content Models
67
+
68
+ ### Model Types
69
+
70
+ | Type | Purpose | Has URL | File path |
71
+ |---|---|---|---|
72
+ | **Page** | Routable content | Yes | `models/pages/<key>.model` |
73
+ | **Entry** | Shared/reusable data (nav, footer, authors) | No | `models/entries/<key>.model` |
74
+ | **Block** | Reusable component schema embedded in pages/entries | No | `models/blocks/<key>.model` |
75
+
76
+ Pages and entries can be **single** (one record) or **collection** (many records, key ends with `+`). Blocks are embedded component definitions; they do not have their own top-level content records.
77
+
78
+ ### .model File Format
79
+
80
+ JSON structure without quotes on keys or string values. Scalar values are the field type name.
81
+
82
+ ```
83
+ {
84
+ title: text,
85
+ image: image,
86
+ content: markdown
87
+ }
88
+ ```
89
+
90
+ **Groups** — Nest fields in an object:
91
+ ```
92
+ {
93
+ hero: {
94
+ heading: text,
95
+ background: image
96
+ }
97
+ }
98
+ ```
99
+
100
+ **Collections** (repeatable lists) — Wrap a group in `[]`:
101
+ ```
102
+ {
103
+ features: [
104
+ {
105
+ title: text,
106
+ icon: image
107
+ }
108
+ ]
109
+ }
110
+ ```
111
+
112
+ **Block field** — Use `block(key)` to embed one reusable block schema, or `[block(key)]` for a repeatable list of the same block:
113
+ ```
114
+ {
115
+ cta: block(cta),
116
+ ctas: [block(cta)]
117
+ }
118
+ ```
119
+
120
+ ### Blocks
121
+
122
+ Blocks are reusable components made from two files:
123
+
124
+ - `models/blocks/<key>.model` defines the fields editors can fill in.
125
+ - `blocks/<key>.ejs` defines how that block renders.
126
+
127
+ A block does **not** own content. There is never a `content/blocks/...` file. Instead, each page or entry that declares `block(key)` or `[block(key)]` stores its own copy of that block's field values inside its own content JSON. The reusable part is the schema and template; the content is local to the page or entry using it.
128
+
129
+ Use blocks when a group of fields also needs a reusable view, such as SEO metadata, cards, or calls to action that should render the same way across multiple pages. If the structure is only used on one page, a group (`{ ... }`) or repeating group (`[{ ... }]`) is usually simpler. Shared content like testimonials, authors, team members, categories, or reusable navigation data should generally be entries referenced with `entry(key)` or `[entry(key)]`, not blocks.
130
+
131
+ Example block model:
132
+
133
+ **`models/blocks/hero.model`**
134
+ ```
135
+ {
136
+ heading: text,
137
+ subheading: paragraph,
138
+ background: image,
139
+ cta_label: text,
140
+ cta_link: link
141
+ }
142
+ ```
143
+
144
+ Use that block in a page model:
145
+
146
+ **`models/pages/about.model`**
147
+ ```
148
+ {
149
+ title: text,
150
+ hero: block(hero)
151
+ }
152
+ ```
153
+
154
+ Store that page's block data inline:
155
+
156
+ **`content/pages/about.json`**
157
+ ```json
158
+ {
159
+ "title": "About us",
160
+ "hero": {
161
+ "heading": "Care that feels personal",
162
+ "subheading": "A small team focused on long-term relationships.",
163
+ "background": "pexels:doctor",
164
+ "cta_label": "Book a visit",
165
+ "cta_link": "/contact"
166
+ }
167
+ }
168
+ ```
169
+
170
+ Render it from the page template with `render()`. The `block(hero)` declaration tells the renderer to use `blocks/hero.ejs`:
171
+
172
+ **`pages/about.ejs`**
173
+ ```ejs
174
+ <h1><%= item.title %></h1>
175
+ <%- render(item.hero) %>
176
+ ```
177
+
178
+ **`blocks/hero.ejs`**
179
+ ```ejs
180
+ <section class="hero" style="background-image: url('<%- src(item.background, '1600x700') %>')">
181
+ <h2><%= item.heading %></h2>
182
+ <p><%= item.subheading %></p>
183
+ <a href="<%= item.cta_link %>"><%= item.cta_label %></a>
184
+ </section>
185
+ ```
186
+
187
+ Block models cannot contain other blocks. Use groups (`{ ... }`) or collections (`[{ ... }]`) inside a block when you need nested structure, and use `entry(key)` when a block needs shared reusable data.
188
+
189
+ Repeatable blocks use the same collection syntax as groups and entries: `[block(cta)]`. Use this sparingly, only when each repeated item should use the same reusable block template. For simple one-page repeated content, prefer a repeating group (`[{ ... }]`). For reusable content selected across pages, prefer a collection entry plus `[entry(key)]`.
190
+
191
+ **Entry reference** — Use `entry(key)` for one, `[entry(key)]` for many:
192
+ ```
193
+ {
194
+ author: entry(authors+),
195
+ testimonials: [entry(testimonials+)],
196
+ tags: [entry(tags+)]
197
+ }
198
+ ```
199
+
200
+ ### Field Types and Serialization
201
+
202
+ Model fields declare both the editor type and the shape expected in content JSON. Templates receive those same values, except image shortcuts are resolved to full image objects and entry references are resolved to entry object(s).
203
+
204
+ | Model type | Content JSON value / template value |
205
+ |---|---|
206
+ | `text`, `paragraph`, `richtext`, `markdown`, `code`, `color`, `link` | String |
207
+ | `number` | Number |
208
+ | `boolean` | `true` / `false` |
209
+ | `date` | `"YYYY-MM-DD"` string |
210
+ | `datetime` | ISO 8601 string |
211
+ | `time` | `"HH:mm"` string |
212
+ | `image` | Either a resolved `{ "url": "...", "alt": "..." }` object, **or** a shortcut string `"<source>:<search>"` (e.g., `"pexels:doctor"`, `"url:https://picsum.photos/200.jpg"`, `"cms:logo"`). Supported sources: `unsplash`, `pexels`, `pixabay`, `iconify`, `url`, `cms` (reuses an image declared in `/images.json` by handle). Append `\|<alt text>` to set the image's alt, e.g. `"pexels:doctor\|Smiling doctor with stethoscope"`. On save, the sync engine resolves the shortcut/link to a full image object automatically. |
213
+ | `video` | `{ "url": "...", "embed": "..." }` |
214
+ | `json` | Object or array |
215
+ | `sheet` | Array of arrays |
216
+ | `location` | `{ "markers": [{ "lat": n, "lng": n }], "img": "..." }`; `img` is a Google Maps Static Image URL that already includes the markers and can include formatting parameters such as `size`, `zoom`, `scale`, `maptype`, and styling |
217
+ | `block(key)` | Object matching that block's model; stored inline in the parent page/entry content and rendered with `blocks/<key>.ejs` |
218
+ | `[block(key)]` | Array of block objects; each item matches the block model and renders with `blocks/<key>.ejs` |
219
+ | `entry(key)` / `[entry(key)]` | Slug string / array of slug strings in content JSON; entry object / array of entry objects in templates |
220
+ | Group `{ ... }` | Nested object |
221
+ | Collection `[{ ... }]` | Array of nested objects |
222
+
223
+ `richtext` returns HTML; output it with `<%- %>`. `markdown` returns raw markdown; convert it with `marked()` before output. Inside markdown, use the same image shortcut convention in standard image syntax: `![alt](pexels:doctor)`. Append `\|<alt>` to set alt text, and include a `<width>x<height>` token to set URL `w`/`h` params (defaults to `600x400`), e.g. `![doctor](pexels:doctor\|Smiling doctor 800x600)`.
224
+
225
+ ---
226
+
227
+ ## Content Records
228
+
229
+ Content files are JSON records under `/content/` that hold the actual values for the fields declared in each `.model`. Editing a content file and saving it triggers the same sync-build-deploy loop as editing a template — you can view and edit content directly from this workspace.
230
+
231
+ **Blocks never have top-level content files.** Block data is always embedded inside the page or entry that declares the block field.
232
+
233
+ ### File layout
234
+
235
+ | Model shape | File path | JSON top-level |
236
+ |---|---|---|
237
+ | Single page (e.g., `about`) | `content/pages/about.json` | Object |
238
+ | Collection page (e.g., `blog+`) | `content/pages/blog+/<slug>.json` (the `+` is part of the key, not an extra suffix) | Object; one file per slug |
239
+ | Single entry (e.g., `header`) | `content/entries/header.json` | Object |
240
+ | Collection entry (e.g., `authors`) | `content/entries/authors+.json` (the `+` is part of the key — same as the model filename) | Array of objects |
241
+
242
+ ### Example
243
+
244
+ Given a model:
245
+
246
+ ```
247
+ { title: text, image: image, hero: block(hero), tags: [entry(tags+)] }
248
+ ```
249
+
250
+ The content file at `content/pages/about.json`:
251
+
252
+ ```json
253
+ {
254
+ "title": "About us",
255
+ "image": "pexels:team meeting",
256
+ "hero": {
257
+ "heading": "Hello",
258
+ "subheading": "Welcome",
259
+ "background": "pexels:doctor",
260
+ "cta_label": "Contact us",
261
+ "cta_link": "/contact"
262
+ },
263
+ "tags": ["engineering", "design"]
264
+ }
265
+ ```
266
+
267
+ Here `hero` is embedded block data stored directly in the page content file. `image` and `hero.background` use the shortcut form — on save, the sync engine replaces each shortcut with a real image object (`{ "url": "...", "alt": "..." }`). Write the object form directly when you have a specific asset URL.
268
+
269
+ ### Reusable images (`/images.json`)
270
+
271
+ For images used in more than one place (logos, recurring icons, hero art), declare them once in `/images.json` as a flat map of `handle → shortcut` using the same shortcut convention as `image` fields:
272
+
273
+ ```json
274
+ {
275
+ "logo": "url:https://cdn.example.com/logo.svg",
276
+ "hero": "pexels:mountain sunrise",
277
+ "apple-icon": "iconify:mdi:apple"
278
+ }
279
+ ```
280
+
281
+ Then reference any of them from any content `image` field with `"cms:<handle>"` (e.g., `"cms:logo"`). The sync engine resolves it to the full image object on save. Templates can also fetch the resolved object directly via `getImage('<handle>')`.
282
+
283
+ ---
284
+
285
+ ## EJS Templates
286
+
287
+ ### Syntax
288
+
289
+ | Tag | Purpose |
290
+ |---|---|
291
+ | `<%= expr %>` | Output with HTML escaping (text content) |
292
+ | `<%- expr %>` | Output raw HTML (blocks, images, rich text, helpers) |
293
+ | `<% code %>` | Execute JS (loops, conditionals, variables) |
294
+
295
+ ### Template Context
296
+
297
+ Every template receives these variables:
298
+
299
+ | Variable | Type | Description |
300
+ |---|---|---|
301
+ | `item` | Object | Current page, block, or entry being rendered |
302
+ | `pages` | Array | All page records (each has `_path`, `_slug`, fields) |
303
+ | `entries` | Object | All entries keyed by model handle |
304
+ | `main` | String | Rendered page template output (**layout only**) |
305
+
306
+ `item` always refers to the current record. In a page template, `item` is the page. In a block template, `item` is the block instance. In an entry template, `item` is the entry.
307
+
308
+ Page records include: `item._path`, `item._slug` (collections), `item._meta.updated_at`.
309
+
310
+ ---
311
+
312
+ ## Helper Functions
313
+
314
+ ### Content Access
315
+
316
+ | Function | Returns | Description |
317
+ |---|---|---|
318
+ | `getPage(path)` | Object \| undefined | Page by exact path |
319
+ | `getPages(path, opts?)` | Array | Pages where path starts with prefix. `{ collection: true }` for collection pages only |
320
+ | `getEntry(handle)` | Object \| Array | Entry by handle. Single → object, collection → array |
321
+ | `getSlugs(path)` | string[] | Slugs under a collection path |
322
+ | `getImage(name)` | Object \| undefined | Site-level image by handle |
323
+ | `getOptions(name)` | Array \| undefined | Option set as `[{ label, value }]` |
324
+ | `getContent(query?)` | Any | Full content payload, or filter with JMESPath |
325
+ | `path(page)` | String | URL path of a page object |
326
+ | `url(pathOrPage?)` | String | Site origin (e.g. `https://example.com`). Pass a path string to get a full URL (`url('/blog')` → `https://example.com/blog`), or pass a page object to resolve its path into a full URL. |
327
+
328
+ ### Rendering
329
+
330
+ | Function | Returns | Description |
331
+ |---|---|---|
332
+ | `render(val, separator?)` | HTML string | Render a block/entry (or array of them) through its template |
333
+ | `marked(md)` | HTML string | Convert a markdown string to HTML |
334
+
335
+ ### Images
336
+
337
+ | Function | Returns | Description |
338
+ |---|---|---|
339
+ | `src(image, attr)` | URL string | Optimized image URL |
340
+ | `img(image, attr)` | HTML string | `<img>` element |
341
+ | `picture(image, attr)` | HTML string | `<picture>` with dark/light variants |
342
+ | `svg(image, attr?)` | HTML string | Inline SVG with optional attributes |
343
+
344
+ `attr` can be `"WxH"` string or `{ w, h, size, fit, type, class, style }` object.
345
+
346
+ ### Location
347
+
348
+ `location` fields can be passed to image helpers for static maps, embedded maps, or marker data.
349
+
350
+ | Function | Returns | Description |
351
+ |---|---|---|
352
+ | `src(loc, attr)` | URL string | Static map URL |
353
+ | `img(loc, attr)` | HTML string | Static map `<img>` |
354
+ | `embed(loc, attr?)` | HTML string | Google Maps `<iframe>` |
355
+ | `markers(loc)` | `{ lat, lng }[]` | Marker coordinates |
356
+
357
+ `attr` can be `"WxH"` or `{ w, h, size, zoom, maptype, scale, class, style }`.
358
+
359
+ ### Head Injection
360
+
361
+ Call from **any template** (page, block, entry, or layout). Deduplicated automatically.
362
+
363
+ | Function | Description |
364
+ |---|---|
365
+ | `title(text)` | Set page `<title>` |
366
+ | `meta(attrs)` | Add `<meta>` tag |
367
+ | `link(value, order?)` | Add `<link>` tag (string URL auto-detects type, or pass object) |
368
+ | `style(css, order?)` | Add `<style>` block |
369
+ | `script(value, order?)` | Add `<script>` (`.js` URL → external, otherwise inline) |
370
+
371
+ ---
372
+
373
+ ## SEO
374
+
375
+ Create a **block model** (e.g., `seo.model`) and add SEO tags manually in its template:
376
+
377
+ **`models/blocks/seo.model`**
378
+ ```
379
+ {
380
+ title: text,
381
+ description: paragraph,
382
+ image: image
383
+ }
384
+ ```
385
+
386
+ **`blocks/seo.ejs`**
387
+ ```ejs
388
+ <% if (item.title) title(item.title) %>
389
+ <% if (item.description) meta({ name: 'description', content: item.description }) %>
390
+ <% if (item.image) { %>
391
+ <% meta({ property: 'og:image', content: src(item.image, '1200x630') }) %>
392
+ <% } %>
393
+ ```
394
+
395
+ Then include `seo: block(seo)` in any page model and render it: `<%- render(item.seo) %>`
396
+
397
+ ---
398
+
399
+ ## Forms
400
+
401
+ Any `<form>` with a `data-sleekcms="<name>"` attribute works automatically — submissions are captured, stored, and viewable in the CMS dashboard. No backend setup, no action URL, no JS required.
402
+
403
+ ```html
404
+ <form data-sleekcms="contact">
405
+ <input name="name" type="text" required>
406
+ <input name="email" type="email" required>
407
+ <textarea name="message"></textarea>
408
+ <button type="submit">Send</button>
409
+ </form>
410
+ ```
411
+
412
+ The `<name>` value (e.g., `contact`, `newsletter`, `quote-request`) groups submissions by form. Use standard `name` attributes on inputs — each field is stored as-is.
413
+
414
+ ---
415
+
416
+ ## RSS Feeds
417
+
418
+ Create an RSS feed by adding a page with the key `rss.xml` — this maps to the URL `/rss.xml`. Because the extension is `.xml`, the static server serves it with the correct content type automatically. The template outputs raw XML and must **not** use a layout.
419
+
420
+ **`models/pages/rss.xml.model`**
421
+ ```
422
+ {
423
+ title: text,
424
+ description: paragraph
425
+ }
426
+ ```
427
+
428
+ **`content/pages/rss.xml.json`**
429
+ ```json
430
+ {
431
+ "title": "My Blog",
432
+ "description": "Latest posts from My Blog"
433
+ }
434
+ ```
435
+
436
+ **`pages/rss.xml.ejs`**
437
+ ```ejs
438
+ <?xml version="1.0" encoding="UTF-8"?>
439
+ <rss version="2.0">
440
+ <channel>
441
+ <title><%= item.title %></title>
442
+ <link><%= url() %></link>
443
+ <description><%= item.description %></description>
444
+ <% for (const post of getPages('/blog', { collection: true })) { %>
445
+ <item>
446
+ <title><%= post.title %></title>
447
+ <link><%= url(post) %></link>
448
+ <description><%= post.description %></description>
449
+ <pubDate><%= new Date(post._meta.updated_at).toUTCString() %></pubDate>
450
+ <guid><%= url(post) %></guid>
451
+ </item>
452
+ <% } %>
453
+ </channel>
454
+ </rss>
455
+ ```
456
+
457
+ Notes:
458
+ - The key `rss.xml` follows the standard naming convention — the dot is part of the key as-is.
459
+ - `getPages('/blog', { collection: true })` fetches all blog collection pages; adjust the path to match your collection key.
460
+ - `url(post)` resolves the page object to a full absolute URL (e.g. `https://example.com/blog/my-post`) — no need to store the site URL in content.
461
+ - `post._meta.updated_at` is an ISO 8601 timestamp; `.toUTCString()` converts it to RFC 822 format required by RSS.
462
+ - Use a dedicated `description` or `summary` field in your blog model for feed excerpts; fall back to any short-text field if one doesn't exist.
463
+ - To autodiscover the feed, add `<% link({ rel: 'alternate', type: 'application/rss+xml', title: 'RSS', href: '/rss.xml' }) %>` in your layout or page templates.
464
+
465
+ ---
466
+
467
+ ## Examples
468
+
469
+ ### Layout
470
+
471
+ ```ejs
472
+ <!DOCTYPE html>
473
+ <html lang="en">
474
+ <head>
475
+ <meta charset="UTF-8">
476
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
477
+ </head>
478
+ <body>
479
+ <% const header = getEntry('header'); %>
480
+ <header>
481
+ <%- render(header) %>
482
+ </header>
483
+
484
+ <main><%- main %></main>
485
+
486
+ <% const footer = getEntry('footer'); %>
487
+ <footer>
488
+ <%- render(footer) %>
489
+ </footer>
490
+ </body>
491
+ </html>
492
+ ```
493
+
494
+ ### Page template
495
+
496
+ ```ejs
497
+ <% title(item.title + ' | My Site') %>
498
+ <% link('/css/styles.css') %>
499
+ <% script('/js/app.js') %>
500
+
501
+ <h1><%= item.title %></h1>
502
+ <%- img(item.image, '1200x600') %>
503
+ <div><%- item.content %></div>
504
+ ```
505
+
506
+ ### Block template
507
+
508
+ ```ejs
509
+ <section class="hero" style="background-image: url('<%- src(item.background, '1920x800') %>')">
510
+ <h2><%= item.heading %></h2>
511
+ <p><%= item.subheading %></p>
512
+ <a href="<%= item.cta_link %>" class="btn"><%= item.cta_label %></a>
513
+ </section>
514
+ ```
515
+
516
+ ### List collection pages
517
+
518
+ ```ejs
519
+ <% for (const post of getPages('/blog', { collection: true })) { %>
520
+ <a href="<%- path(post) %>">
521
+ <%- img(post.image, '400x250') %>
522
+ <h3><%= post.title %></h3>
523
+ </a>
524
+ <% } %>
525
+ ```
526
+
527
+ ### Render blocks and entry references
528
+
529
+ Given a model:
530
+ ```
531
+ {
532
+ hero: block(hero),
533
+ ctas: [block(cta)],
534
+ team: [entry(people+)]
535
+ }
536
+ ```
537
+
538
+ Template:
539
+ ```ejs
540
+ <%- render(item.hero) %>
541
+ <%- render(item.ctas) %>
542
+
543
+ <% for (const person of item.team) { %>
544
+ <%- render(person) %>
545
+ <% } %>
546
+ ```
547
+
548
+ ---
549
+
550
+ ## Rules for AI
551
+
552
+ 1. Include CSS/JS files via **`link()`** and **`script()`** — never raw `<link>` or `<script>` tags in templates.
553
+ 2. Exception: `/css/tailwind.css` is auto-injected — do **not** add it via `link()`.
554
+ 3. `richtext` returns **HTML** — use `<%- %>` (unescaped) to output it. `markdown` returns **raw markdown** — convert with `marked()` first: `<%- marked(item.content) %>`.
555
+ 4. Use modern design with tailwind unless design details are specified.
556
+ 5. If sync, build, render, image resolution, or deploy behavior fails, check `sync-errors.log` in the workspace root before guessing.
557
+ 6. To change what appears on a page or in shared data, edit the matching JSON under `/content/` — do **not** hard-code content into `.ejs` templates. Templates define structure; content files hold the values.
558
+ 7. Fields in a content JSON file must match the keys defined in the corresponding `.model`. Adding a new field requires updating the `.model` first.
559
+ 8. Collection page items each live in their own file under `content/pages/<key>/<slug>.json` — the collection key already includes `+` (e.g., `content/pages/blog+/my-post.json`). The `<slug>` filename is the URL segment; renaming the file renames the URL.
560
+ 9. **Collection key suffix `+` is mandatory and must appear on every related file.** For a collection model (pages or entries — e.g., `blog`, `testimonials`, `authors`), the key `<name>+` is part of the filename on the model, template, **and** content JSON: `models/entries/testimonials+.model`, `entries/testimonials+.ejs`, `content/entries/testimonials+.json` (array). Same rule for collection pages: `models/pages/blog+.model`, `pages/blog+.ejs`, and one file per slug under `content/pages/blog+/<slug>.json`. Never drop the `+` — files without it are treated as singles and will not resolve.
561
+ 10. Blocks are reusable component schemas/templates only. Never create `content/blocks/...`; store block values inline in the page or entry JSON that declares `block(key)` or `[block(key)]`.
562
+ 11. Use blocks only when a group of fields also benefits from a reusable template. For one-off page-only structure, prefer groups (`{ ... }`) or repeating groups (`[{ ... }]`).
563
+ 12. Use entries, not blocks, for shared reusable content such as testimonials, authors, people, categories, tags, navigation, or footer data.
564
+ 13. Block models cannot contain `block(...)` fields. Use groups, collections, or `entry(key)` references instead.
565
+ 14. For `image` fields in content JSON, prefer the shortcut form `"<source>:<search>"` (sources: `unsplash`, `pexels`, `pixabay`, `iconify`) — e.g., `"pexels:doctor"`. Add alt text by appending `|<alt>` to the shortcut: `"pexels:doctor|Smiling doctor with stethoscope"`. The sync engine resolves it to a full `{ url, alt }` object on save. When the same image is reused across pages (logos, shared icons, recurring art), declare it once in `/images.json` and reference it via `"cms:<handle>"`.
566
+ 15. Inside `markdown` fields, embed images with `![alt](<source>:<search>)` — same sources as image fields. Append `|<alt>` to store alt on the image record (e.g. `![doctor](pexels:doctor|Friendly family doctor)`). Including a `<W>x<H>` token in the description (e.g. `|hero shot 1200x600`) sets the rendered URL's `w` and `h` query params; otherwise the default is `600x400`. On save, refs are rewritten to actual CDN URLs and the underlying image record is created automatically.
567
+ 16. Always create RSS feed for blogs and link them in meta so it is discoverable. Use "rss.xml" as the key.
568
+ 17. Make the sites extremely SEO friendly and sharing friendly.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * CLI setup, prompts, and UI for the CMS CLI
3
+ */
4
+ export interface CliOptions {
5
+ version?: boolean;
6
+ token?: string;
7
+ env?: string;
8
+ path?: string;
9
+ }
10
+ export interface KeyboardHandlers {
11
+ onExit?: () => void | Promise<unknown>;
12
+ onRefetch?: () => void | Promise<unknown>;
13
+ }
14
+ export declare function parseArgs(): CliOptions;
15
+ export declare function prompt(question: string): Promise<string>;
16
+ export declare function showWatchHelp(): void;
17
+ export declare function setupKeyboardInput(handlers: KeyboardHandlers): void;
18
+ export declare function showEditorMenu(viewsDir: string, handlers: KeyboardHandlers): void;