@sleekcms/sync 2.7.2 → 2.9.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.
Files changed (3) hide show
  1. package/README.md +19 -3
  2. package/dist/AGENT.md +108 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -201,9 +201,11 @@ No manual deploy. No waiting. SleekCMS rebuilds on every sync and the preview re
201
201
  │ ├── entries/<key>[].json # Collection entry content (array)
202
202
  │ └── images.json # Reusable site-level images (handle → shortcut)
203
203
  │
204
- └── public/
205
- ├── css/<name>.css # Stylesheets
206
- └── js/<name>.js # Scripts
204
+ ├── public/
205
+ │ ├── css/<name>.css # Stylesheets
206
+ │ └── js/<name>.js # Scripts
207
+ │
208
+ └── setup.json # Site setup: default page title, Tailwind on/off, sitemap on/off
207
209
  ```
208
210
 
209
211
  ### File naming
@@ -410,6 +412,20 @@ Any form with `data-sleekcms="<name>"` captures and stores submissions in the CM
410
412
 
411
413
  Content lives in `src/content/` as plain JSON. Edit any content file and save — it syncs like any other file.
412
414
 
415
+ ## Site setup (`src/setup.json`)
416
+
417
+ Site-wide decisions made once — the default page title, whether Tailwind is compiled, and whether a sitemap is published — live in `src/setup.json`, mirrored from **Settings → Website** in the CMS. The file is JSON with a comment above each key explaining it:
418
+
419
+ ```json5
420
+ {
421
+ title: "Acme Dental", // default <title> for pages without their own; null = the site name
422
+ tw_enabled: true, // compile src/public/css/tailwind.css and inject it into every page
423
+ sitemap_enabled: false, // publish sitemap.xml + robots.txt on every build
424
+ }
425
+ ```
426
+
427
+ Edit the values and save — it syncs like any other file. Keys you leave out keep their current value. Which pages are excluded from the sitemap is managed in the CMS UI, not in this file.
428
+
413
429
  ## Images
414
430
 
415
431
  Images are part of content, not a separate asset pipeline. Drop a shortcut string into any `image` field and the sync engine resolves it to a full `{ url, alt }` object on save:
package/dist/AGENT.md CHANGED
@@ -9,6 +9,7 @@ SleekCMS is a headless CMS plus static site generator. There are no servers, no
9
9
  | **Model** (`.model`) | The shape of the content (field names + types) | `src/models/...` |
10
10
  | **Content** (`.json` / `.md`) | The actual values for those fields | `src/content/...` |
11
11
  | **View** (`.ejs`) | The HTML template that renders the content | `src/pages/`, `src/entries/`, `src/blocks/`, `src/layouts/` |
12
+ | **Assets** (`.css` / `.js`) | Stylesheets and scripts | `src/css/`, `src/js/` |
12
13
 
13
14
  A page's HTML = **layout** wrapping ( **page view** rendering **page content** validated by **page model** ).
14
15
 
@@ -75,19 +76,39 @@ blocks/<key>.ejs Block views (required for every block)
75
76
  layouts/common.ejs The single site layout (see §8)
76
77
 
77
78
  css/<name>.css Stylesheets — must be included via link()
78
- css/tailwind.css Tailwind (auto-compiled, auto-injected — never link() it)
79
+ css/tailwind.css Tailwind v4 input CSS (auto-compiled, auto-injected — never link() it; @theme design tokens + the /* DESIGN */ header live here)
79
80
  js/<name>.js Scripts — must be included via script()
80
81
 
81
82
  content/pages/<key>.json Content for a single page (object)
82
- content/pages/<key>/<slug>.json Content for one record of a collection page (<key> ends with +)
83
- content/pages/<key>/<slug>.md Same record as Markdown + frontmatter, when the model qualifies (§7)
83
+ content/pages/<key>+/<slug>.json Content for ONE record of a collection page — one file per slug
84
+ content/pages/<key>+/<slug>.md Same record as Markdown + frontmatter, when the model qualifies (§7)
84
85
  content/entries/<key>.json Content for a single entry (object)
85
- content/entries/<key>.json Collection entry: <key> ends with +, file holds an array of objects
86
-
86
+ content/entries/<key>+.json Collection entry — ONE file holding a JSON array of all items
87
87
  content/images.json Site-level reusable images: handle → shortcut (§7)
88
- content/videos.json Site-level videos: handle → embed_url. READ-ONLY (§7)
88
+ content/videos.json Site-level videos: handle → pexels:/pixabay: ref (user uploads show as embed_url) (§7)
89
+
90
+ setup.json Site setup (JSON, comments allowed): default page title, Tailwind on/off, sitemap on/off — edit values in place, never delete
89
91
  ```
90
92
 
93
+ **Collection storage differs by kind — do not confuse them:**
94
+ - **Pages collection** (`blog+`) → a *folder* of per-slug files: `content/pages/blog+/post-1.json`, `post-2.json`, … Each item has its own URL.
95
+ - **Entries collection** (`testimonials+`) → a *single file* containing a JSON array: `content/entries/testimonials+.json` holds `[{...}, {...}]`. Items are shared data with no URLs.
96
+
97
+ **Path spaces** — three different things use `/`-style paths; don't confuse them:
98
+ - **Workspace file path** (what you edit under `src/`): `pages/about.ejs`, `css/styles.css`.
99
+ - **Public URL** (what `link()`/`script()` and HTML `href`/`src` take): `/css/styles.css`, `/js/app.js`.
100
+ - **Page route** (what visitors see): `/about`, `/blog/<slug>`. Computed from the page key.
101
+
102
+ **Site setup (`setup.json`)** — the set-once site decisions, mirrored from Settings > Website in the CMS. Each key is explained by the comment above it in the file:
103
+
104
+ | Key | Value | Effect |
105
+ |---|---|---|
106
+ | `title` | string or `null` | Default `<title>` for pages that don't set their own (via `title()` or the SEO block). `null` = the site name. |
107
+ | `tw_enabled` | boolean | `true` compiles `css/tailwind.css` and injects it into every page — required for any site styled with Tailwind utilities. |
108
+ | `sitemap_enabled` | boolean | `true` publishes `sitemap.xml` + `robots.txt` on every build. Turn it on for public sites and SEO work. |
109
+
110
+ Edit values in place and keep the keys. Keys you omit keep their stored values; unknown keys are ignored. Which pages are excluded from the sitemap is a CMS UI setting, not a key in this file. Never delete the file.
111
+
91
112
  ## 5. Common Tasks
92
113
 
93
114
  ### Create a single page (e.g. `/about`)
@@ -103,7 +124,7 @@ content/videos.json Site-level videos: handle → embed_url. READ-ON
103
124
  2. `pages/blog+.ejs` — renders **one** post; `item` is that post's content.
104
125
  3. One content file per post: `content/pages/blog+/<slug>.json` (or `.md` when the model qualifies, §7).
105
126
  4. Optionally create a listing page with key `blog` (single page) that loops with `getPages('/blog', { collection: true })`.
106
- 5. For blogs, also create an RSS feed (§11) and link it for autodiscovery.
127
+ 5. For blogs, also create an RSS feed (§12) and link it for autodiscovery.
107
128
 
108
129
  ### Change what a page says
109
130
 
@@ -130,7 +151,7 @@ Like JSON but with **no quotes**; each value is a field type name:
130
151
  features: [
131
152
  {
132
153
  title: text,
133
- icon: image
154
+ icon: icon
134
155
  }
135
156
  ]
136
157
  }
@@ -155,9 +176,10 @@ Like JSON but with **no quotes**; each value is a field type name:
155
176
  | `date` | `"YYYY-MM-DD"` string |
156
177
  | `datetime` | ISO 8601 string |
157
178
  | `time` | `"HH:mm"` string |
158
- | `image` | Shortcut string (preferred when writing) or resolved `{ "url", "alt" }` object — see §7. Use `image` for **anything visual, including icons** (`"iconify:..."`). Shortcuts only resolve in `image` fields — in a `text` field the string stays literal text |
159
- | `video` | Object. **Never write or invent this value** — uploads are user-driven in the CMS UI. Render with `embed()` only |
160
- | `file` | Object `{ name, url, size, type, ext }`. **Never write or invent this value** — uploads are user-driven. Render a link with `item.x.url` / `item.x.name` |
179
+ | `image` | Shortcut string (preferred when writing) or resolved `{ "url", "alt" }` object — see §7. Shortcuts only resolve in `image`/`icon` fields — in a `text` field the string stays literal text |
180
+ | `icon` | Svg icon. Use for icons the user is expected to edit/swap in the CMS (e.g. features/services). Fill with placeholder `"iconify:<prefix:name or search>"` (or `"cms:<handle>"` pointing at an svg); render with `svg(item.icon)`. Fixed UI icons don't need fields — call `svg('iconify:...')` directly in templates |
181
+ | `video` | Ref string (preferred when writing) or the resolved object you read back. Write `"cms:<handle>"` to reuse a video from `content/videos.json` (or a user upload by its handle), or `"pexels:<search>\|<title>"` / `"pixabay:<search>\|<title>"` for a one-off — resolved server-side (see §7). **Never invent** `video_id`/`embed_url` objects; render with `embed()` only |
182
+ | `file` | Write a plain file name, e.g. `"brochure.pdf"` (pdf, docx, xlsx, pptx, csv, tsv, txt, md — no path, no URL, never an object). On save the CMS attaches the site file with that name, or creates a small placeholder file the user replaces in the CMS UI; reads back as the file name. Render a link with `item.x.url` / `item.x.name` |
161
183
  | `json` | Any JSON |
162
184
  | `sheet` | Array of arrays |
163
185
  | `location` | `{ "markers": [{ "lat", "lng" }], "img": "<google static map url>" }` — `img` supports Google Static Maps params; markers are already part of the URL |
@@ -168,6 +190,8 @@ Like JSON but with **no quotes**; each value is a field type name:
168
190
  | Group `{ ... }` | Nested object |
169
191
  | Collection `[{ ... }]` | Array of nested objects |
170
192
 
193
+ > **Images need an `image` field and icons an `icon` field — never `text`.** A shortcut string (`unsplash:`/`pexels:`/`pixabay:`/`iconify:`/`url:`/`cms:`) resolves to a real image **only** when its model field is typed `image` or `icon`, or inside a `markdown` body via `![alt](…)`. In any other field — `text`, `paragraph`, `richtext`, `code`, `link` — the shortcut is stored and rendered **verbatim**. Symptom: a raw `iconify:…`/`pexels:…` string showing up on the page means the field was mis-typed — change it to `image`/`icon` in the `.model` and the existing value resolves on the next save.
194
+
171
195
  ### Entry references in content
172
196
 
173
197
  The value is the **0-based index** of the target record in `content/entries/<key>+.json`, written as a string:
@@ -225,7 +249,7 @@ Content files hold the values for model fields. Keys must match the model. Savin
225
249
 
226
250
  ### Images — shortcut strings
227
251
 
228
- When writing an `image` value, prefer the shortcut form `"<source>:<search>"`. Sources: `unsplash`, `pexels`, `pixabay`, `iconify`, `url` (direct URL), `cms` (reusable handle, below).
252
+ When writing an `image` value, prefer the shortcut form `"<source>:<search>"`. Sources — this is the complete set: `unsplash`, `pexels`, `pixabay`, `iconify`, `url` (direct URL), `cms` (reusable handle, below).
229
253
 
230
254
  - `"pexels:doctor"` — stock search
231
255
  - `"url:https://example.com/photo.jpg"` — specific asset
@@ -234,21 +258,33 @@ When writing an `image` value, prefer the shortcut form `"<source>:<search>"`. S
234
258
 
235
259
  On save, the sync engine resolves each shortcut to a real `{ "url", "alt" }` object.
236
260
 
237
- **Shortcuts only work in `image` fields.** A common mistake: declaring a visual field as `text` (e.g. `icon: text`) and then writing `"iconify:lucide:clock"` as its value. Nothing errors — the string simply stays literal text and never resolves to an image. Any field that will hold an icon, logo, photo, or illustration must be declared `icon: image` in the model. If a shortcut isn't resolving, check the field's type in the `.model` first.
261
+ Give **every** image field a resolvable shortcut from the sources above — including slots the user will eventually fill with their own photos (personal galleries, headshots, family pages, "I'll add my pictures later"): write a stock search whose subject and alt describe the photo that belongs there, e.g. `"pexels:friends laughing golden hour|Ghazia with her girlies"`. The site ships with a real photo in every slot, and the user swaps any image for their own in the CMS UI whenever they're ready.
238
262
 
239
- **Reusable images:** for images used in multiple places (logo, recurring icons), declare them once in `content/images.json` as `handle → shortcut`:
263
+ **Shortcuts only resolve in `image` and `icon` fields.** A common mistake: declaring a visual field as `text` (e.g. `icon: text`) and then writing `"iconify:lucide:clock"` as its value. Nothing errors — the string simply stays literal text and never resolves. Any field that will hold a photo or illustration must be typed `image`, and any CMS-editable icon must be typed `icon`. If a shortcut isn't resolving, check the field's type in the `.model` first.
264
+
265
+ **Reusable images and customizable site icons:** for site-wide images or icons the user is expected to change in the CMS (e.g. site logo), declare them once in `content/images.json` as `handle → shortcut` with a placeholder shortcut:
240
266
 
241
267
  ```json
242
- { "logo": "url:https://cdn.example.com/logo.svg", "hero": "pexels:mountain sunrise" }
268
+ { "logo": "iconify:lucide:sparkles", "hero": "pexels:mountain sunrise" }
243
269
  ```
244
270
 
245
- then reference with `"cms:logo"` in any image field, or fetch in templates via `getImage('logo')`.
271
+ then reference with `"cms:logo"` in any `image`/`icon` field, or render directly in templates — `svg('cms:logo', { class: 'h-8 w-auto' })` for SVGs/icons, `img('cms:hero', '1920x600')` for photos. (`getImage('logo')` returns the resolved image object for a bare handle — rarely needed; the helpers take the value or `cms:` ref directly.)
246
272
 
247
273
  **Images inside markdown:** use the same shortcuts in standard syntax: `![alt](pexels:doctor)`. Append `|<alt>` for alt text and an optional `<W>x<H>` token to set rendered size (default `600x400`): `![doctor](pexels:doctor|Friendly doctor 800x600)`. Refs are rewritten to CDN URLs on save.
248
274
 
249
- ### Videos — read-only
275
+ ### Videos
276
+
277
+ `content/videos.json` maps `handle → ref` for site-level videos. To add a stock video, declare a handle with a `pexels:<search>` or `pixabay:<search>` ref (their free stock **video** catalogs) — optional `_` (landscape) / `/` (portrait) marker, optional `|<title>`:
278
+
279
+ ```json
280
+ { "hero-loop": "pexels:_ocean waves|Hero background loop" }
281
+ ```
250
282
 
251
- `content/videos.json` maps `handle → embed_url` for every video the user uploaded via the CMS UI. **Never edit this file** — it is regenerated and local edits are overwritten. Use it only to discover handles, then in templates: `<%- embed(getVideo('intro'), { size: '1280x720' }) %>`. Never write `video` field values or hand-roll an `<iframe>`.
283
+ On save the platform searches the provider, picks a clip, and ingests it into the site's video library; processing is asynchronous (the embed shows a processing screen until transcoding finishes — no action needed). Writes MERGE by handle: handles you omit are preserved. Re-saving the same ref is a no-op; changing a ref replaces the video. Entries whose value is an **embed URL** are videos the user uploaded in the CMS UI — echo or omit them, never modify or re-point them; deletion is CMS-UI-only.
284
+
285
+ Render in templates: `<%- embed(getVideo('hero-loop'), { size: '1280x720' }) %>`. Never hand-roll an `<iframe>`.
286
+
287
+ Content `video` fields accept the same refs: `"cms:<handle>"` reuses a declared (or user-uploaded) video by handle — preferred when the same video appears in more than one place — while a direct `"pexels:<search>|<title>"` / `"pixabay:<search>|<title>"` creates a record-local video. Stock videos read back as refs; user-uploaded videos read back as objects — echo those untouched.
252
288
 
253
289
  ### Markdown content files (`.md`)
254
290
 
@@ -304,6 +340,7 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
304
340
  <head>
305
341
  <meta charset="UTF-8">
306
342
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
343
+ <% if (item.seo) { %><%- render(item.seo) %><% } %>
307
344
  </head>
308
345
  <body>
309
346
  <header><%- render(getEntry('header')) %></header>
@@ -321,7 +358,7 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
321
358
  | `getPages(path, opts?)` | Array | Pages whose path starts with the prefix. **Always a URL path, never a key.** `{ collection: true }` → collection pages only |
322
359
  | `getEntry(handle)` | Object \| Array | Entry by key. The `+` is optional — `getEntry('authors')` and `getEntry('authors+')` are equivalent. Single → object, collection → array |
323
360
  | `getSlugs(path)` | string[] | Slugs under a collection path |
324
- | `getImage(handle)` | Object \| undefined | Resolved image from `images.json` |
361
+ | `getImage(handle)` | Object \| undefined | Resolved image from `images.json` (bare handle, no `cms:` prefix) |
325
362
  | `getVideo(handle)` | Object \| undefined | Video from `videos.json` |
326
363
  | `getContent(query?)` | Any | Full content payload, optionally filtered with JMESPath |
327
364
  | `path(page)` | String | URL path of a page object |
@@ -345,6 +382,12 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
345
382
 
346
383
  `attr` is `"WxH"` or `{ w, h, size, fit, type, class, style }`. `location` values can be passed to `src()`/`img()` for static maps, to `embed(loc, attr?)` for a Google Maps iframe, or to `markers(loc)` for raw coordinates (`attr` adds `zoom`, `maptype`, `scale`).
347
384
 
385
+ Render `icon` fields or direct icon shortcuts with `svg()` — it inlines the svg markup at build time, so the icon scales cleanly and can be styled with CSS (e.g. `svg(item.icon, { class: 'w-6 h-6' })` or directly `svg('iconify:lucide:arrow-right', { class: 'w-4 h-4' })`).
386
+
387
+ > **Direct UI Icons vs. CMS-Editable Icons:**
388
+ > - **Static UI icons** (chevrons, arrows, badges, buttons, decorative bullet checks): render directly in `.ejs` with `svg('iconify:<family>:<name>', { class: '...' })` — no model field or `images.json` entry required.
389
+ > - **User-editable icons** (site logo, service/feature icons): when the user is expected to customize/swap the icon in the CMS, expose it via `content/images.json` (for site handles like `"logo"`) or an `icon: icon` model field, providing a placeholder `"iconify:..."` in content so the site ships visually complete. Render with `svg('cms:logo')` or `svg(item.icon)`.
390
+
348
391
  ### Helpers — video
349
392
 
350
393
  `embed(video, attr?)` returns a provider-aware `<iframe>`. `attr` is `"WxH"` or `{ w, h, size, autoplay, muted, loop, preload, t, class, style }`. Browsers block autoplay unless `muted: true` is also set.
@@ -371,7 +414,7 @@ Callable from **any** view (page, block, entry, layout); duplicates are removed
371
414
  <section data-aos="fade-up" data-aos-delay="150">...</section>
372
415
  ```
373
416
 
374
- **CSS/JS rules:** include assets only via `link()` / `script()` — never raw `<link>`/`<script>` tags in views. Exception: creating `css/tailwind.css` enables Tailwind, which is compiled and injected automatically — never `link()` it. Default to a modern Tailwind design unless the user specifies otherwise.
417
+ **CSS/JS rules:** include assets only via `link()` / `script()` — never raw `<link>`/`<script>` tags in views. Exception: creating `css/tailwind.css` enables Tailwind v4 — the file is the Tailwind *input CSS*, compiled against the rendered HTML and injected automatically; never `link()` it. Whenever you write it, keep `@import "tailwindcss";` as its first statement (plus the two `@plugin` lines: `@plugin "@tailwindcss/typography";` and `@plugin "@tailwindcss/forms";`) — dropping the import silently removes all Tailwind styling. Define the site's fonts/colors/radii as `@theme` tokens there (giving utilities like `font-display` / `bg-brand-600`) with a `/* DESIGN */` header comment recording the design direction. v4, not v3: custom utilities go in `@utility name { … }`, never `@layer utilities { .name { … } }` (a v3 pattern that yields "Cannot apply unknown utility class"); config is CSS-first — there is no `tailwind.config.js`. Default to a polished Tailwind design with a deliberate design direction.
375
418
 
376
419
  ## 9. SEO
377
420
 
@@ -393,11 +436,13 @@ Make every site strongly SEO- and sharing-friendly. The standard pattern is a re
393
436
  <% if (item.image) meta({ property: 'og:image', content: src(item.image, '1200x630') }) %>
394
437
  ```
395
438
 
396
- Add `seo: block(seo)` to page models and `<%- render(item.seo) %>` in page views.
439
+ Add `seo: block(seo)` to page models and render it **once from the layout head**: `<% if (item.seo) { %><%- render(item.seo) %><% } %>` in `layouts/common.ejs` (as in the layout example above) — not per page view.
440
+
441
+ Two site-level SEO switches live in `setup.json` (§4), not in views: `sitemap_enabled: true` publishes `sitemap.xml` + `robots.txt` on every build, and `title` is the site's default `<title>` for pages whose SEO block leaves the title empty (`null` = the site name). Sitemap page exclusions are chosen in the CMS UI — there is no file setting for them.
397
442
 
398
443
  ## 10. Forms
399
444
 
400
- Any `<form>` with `data-sleekcms="<name>"` works automatically — submissions are captured and shown in the CMS dashboard. No backend, no action URL, no JS.
445
+ Any `<form>` with `data-sleekcms="<hardcoded name>"` works automatically — submissions are captured and shown in the CMS dashboard. No backend, no action URL, no JS.
401
446
 
402
447
  ```html
403
448
  <form data-sleekcms="contact">
@@ -470,16 +515,48 @@ Always create an RSS feed for blogs. Use the page key `rss.xml` — because the
470
515
 
471
516
  Make it discoverable from the layout: `<% link({ rel: 'alternate', type: 'application/rss+xml', title: 'RSS', href: '/rss.xml' }) %>`
472
517
 
473
- ## 13. Critical Rules Checklist
518
+ ## 13. Ecommerce (Snipcart)
519
+
520
+ Snipcart powers stores. Use it when the business **sells products** and the user asks for a store, cart, or buy buttons — or when selling is clearly core to the business. **Never add a store speculatively.** Use Snipcart's default UI (side-modal cart, checkout, customer accounts) as-is; style only the site's own markup (buy buttons, product cards, header controls) with Tailwind.
521
+
522
+ 1. **Settings entry with the API key.** `models/entries/settings.model` → `{ snipcart_key: text }`, value in `content/entries/settings.json`. If the site already has a settings-style entry, add the field there instead of creating a second one. An empty value falls back to a shared test-mode key, so checkout works immediately as a test store.
523
+ 2. **One line in the layout.** At the end of `<body>` in `layouts/common.ejs`:
524
+
525
+ ```ejs
526
+ <%- snipcart(getEntry('settings')?.snipcart_key) %>
527
+ ```
528
+
529
+ This is the complete install — the helper loads all of Snipcart's CDN CSS/JS itself and returns the hidden `#snipcart` mount div. Call it exactly once, from the layout body only; everything after this line is pure markup. Optional second argument: `{ currency: 'eur' }`.
530
+ 3. **Products collection.** `models/pages/products+.model` with `{ title: text, price: number, image: image, description: paragraph }`, one content file per product, a `/products` listing page, and standard `snipcart-add-item` buy buttons wired to SleekCMS values:
531
+
532
+ ```ejs
533
+ <button class="snipcart-add-item"
534
+ data-item-id="<%= item._slug %>"
535
+ data-item-name="<%= item.title %>"
536
+ data-item-price="<%= Number(item.price).toFixed(2) %>"
537
+ data-item-url="<%= path(item) %>"
538
+ data-item-image="<%- src(item.image, '600x600') %>">
539
+ Add to cart
540
+ </button>
541
+ ```
542
+
543
+ `data-item-url` is the product's own page path (`path(item)`), and that page must contain the same buy button — Snipcart re-crawls it to validate orders. Repeat the full `data-item-*` set everywhere the product can be added (listing cards too).
544
+ 4. **Header controls.** Add both the cart button (`class="snipcart-checkout"` with `snipcart-items-count`) and the customer sign-in link (`class="snipcart-customer-signin"`) to the site header.
545
+
546
+ ## 14. Critical Rules Checklist
474
547
 
475
548
  1. **`+` is part of the key.** A collection's `+` appears on the model, the view, and the content file/folder: `models/pages/blog+.model`, `pages/blog+.ejs`, `content/pages/blog+/<slug>.json`. A file without the `+` is a *different*, single-type key.
476
549
  2. Models first: a content field that isn't in the `.model` won't sync. Update the model, then the content.
477
550
  3. Blocks never have content files. Block values live inline in the page/entry JSON that declares them.
478
- 4. Never write `video` or `file` values, and never edit `content/videos.json` — these are user-driven uploads.
479
- 5. `richtext` → `<%- item.x %>`. `markdown` → `<%- marked(item.x) %>`.
480
- 6. CSS/JS via `link()`/`script()` only; `css/tailwind.css` is auto-injected — never `link()` it.
481
- 7. Editable copy belongs in `content/`, not hard-coded in `.ejs`. Fixed micro-labels may stay in views.
482
- 8. In page keys, `-` separates words and `_` means `/`. `_index` is the home page only.
483
- 9. After edits, check `sync-errors.log` in the workspace root. New line = failure for that file; line removed = fixed.
484
- 10. Image values: prefer shortcuts (`"pexels:doctor|alt text"`); use `"cms:<handle>"` + `images.json` for images reused across pages.
485
- 11. Image shortcuts (`"iconify:..."`, `"pexels:..."`, etc.) resolve **only** in `image`-typed fields. Icons are images: declare `icon: image`, never `icon: text`.
551
+ 4. `file` field values are plain file names ending in a supported extension (`"brochure.pdf"`) — the CMS attaches or placeholder-creates the file server-side; never write objects or URLs.
552
+ 5. Never invent `video_id`/`embed_url` objects — render videos with `embed()` only. Videos are set via refs: `content/videos.json` handles for shared videos, and `cms:<handle>` / provider refs in `video` fields (§7); values that read back as embed URLs or objects are user uploads — leave them untouched.
553
+ 6. `richtext` → `<%- item.x %>`. `markdown` → `<%- marked(item.x) %>`.
554
+ 7. CSS/JS via `link()`/`script()` only; `css/tailwind.css` is auto-injected — never `link()` it. When writing tailwind.css, `@import "tailwindcss";` must stay as its first statement — dropping it silently removes all Tailwind styling.
555
+ 8. Editable copy belongs in `content/`, not hard-coded in `.ejs`. Fixed micro-labels may stay in views.
556
+ 9. In page keys, `-` separates words and `_` means `/`. `_index` is the home page only; a section index page uses the plain section key.
557
+ 10. After edits, check `sync-errors.log` in the workspace root. New line = failure for that file; line removed = fixed.
558
+ 11. Images are content-first: author each image as an `image`-field value (`"pexels:doctor|alt text"` — resolves on save) or as a `content/images.json` handle for images reused across pages; templates pass the value straight to the helper (`img(item.field)`, `img('cms:handle')`).
559
+ 12. Image shortcuts (`"iconify:..."`, `"pexels:..."`, etc.) resolve **only** in `image`/`icon`-typed fields. CMS-editable icons are declared `icon: icon`, never `icon: text` — render with `svg(item.icon)`; fixed UI icons need no field: `svg('iconify:...')` directly in the view.
560
+ 13. Use entries (`entry(key+)`) for shared content (authors, testimonials, nav, footer) — not blocks. Block models cannot contain `block(...)` or `stack` fields; use blocks only when a group of fields also benefits from a reusable view.
561
+ 14. Always create an RSS feed for blogs (`rss.xml` key) and make every site strongly SEO- and sharing-friendly (reusable `seo` block rendered once from the layout head).
562
+ 15. Site-wide setup — default page title, Tailwind on/off, sitemap on/off — lives in `setup.json` (§4). Change values there rather than hard-coding a site title into the layout, and never delete the file. Sitemap page exclusions are UI-only.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleekcms/sync",
3
- "version": "2.7.2",
3
+ "version": "2.9.0",
4
4
  "description": "Edit SleekCMS sites locally — models, content, templates, images — with live two-way sync and AI agent support.",
5
5
  "keywords": [
6
6
  "sleekcms",