@sleekcms/sync 2.7.2 → 2.8.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 (2) hide show
  1. package/dist/AGENT.md +92 -30
  2. package/package.json +1 -1
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,27 @@ 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
89
  ```
90
90
 
91
+ **Collection storage differs by kind — do not confuse them:**
92
+ - **Pages collection** (`blog+`) → a *folder* of per-slug files: `content/pages/blog+/post-1.json`, `post-2.json`, … Each item has its own URL.
93
+ - **Entries collection** (`testimonials+`) → a *single file* containing a JSON array: `content/entries/testimonials+.json` holds `[{...}, {...}]`. Items are shared data with no URLs.
94
+
95
+ **Path spaces** — three different things use `/`-style paths; don't confuse them:
96
+ - **Workspace file path** (what you edit under `src/`): `pages/about.ejs`, `css/styles.css`.
97
+ - **Public URL** (what `link()`/`script()` and HTML `href`/`src` take): `/css/styles.css`, `/js/app.js`.
98
+ - **Page route** (what visitors see): `/about`, `/blog/<slug>`. Computed from the page key.
99
+
91
100
  ## 5. Common Tasks
92
101
 
93
102
  ### Create a single page (e.g. `/about`)
@@ -103,7 +112,7 @@ content/videos.json Site-level videos: handle → embed_url. READ-ON
103
112
  2. `pages/blog+.ejs` — renders **one** post; `item` is that post's content.
104
113
  3. One content file per post: `content/pages/blog+/<slug>.json` (or `.md` when the model qualifies, §7).
105
114
  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.
115
+ 5. For blogs, also create an RSS feed (§12) and link it for autodiscovery.
107
116
 
108
117
  ### Change what a page says
109
118
 
@@ -130,7 +139,7 @@ Like JSON but with **no quotes**; each value is a field type name:
130
139
  features: [
131
140
  {
132
141
  title: text,
133
- icon: image
142
+ icon: icon
134
143
  }
135
144
  ]
136
145
  }
@@ -155,9 +164,10 @@ Like JSON but with **no quotes**; each value is a field type name:
155
164
  | `date` | `"YYYY-MM-DD"` string |
156
165
  | `datetime` | ISO 8601 string |
157
166
  | `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` |
167
+ | `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 |
168
+ | `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 |
169
+ | `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 |
170
+ | `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
171
  | `json` | Any JSON |
162
172
  | `sheet` | Array of arrays |
163
173
  | `location` | `{ "markers": [{ "lat", "lng" }], "img": "<google static map url>" }` — `img` supports Google Static Maps params; markers are already part of the URL |
@@ -168,6 +178,8 @@ Like JSON but with **no quotes**; each value is a field type name:
168
178
  | Group `{ ... }` | Nested object |
169
179
  | Collection `[{ ... }]` | Array of nested objects |
170
180
 
181
+ > **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.
182
+
171
183
  ### Entry references in content
172
184
 
173
185
  The value is the **0-based index** of the target record in `content/entries/<key>+.json`, written as a string:
@@ -225,7 +237,7 @@ Content files hold the values for model fields. Keys must match the model. Savin
225
237
 
226
238
  ### Images — shortcut strings
227
239
 
228
- When writing an `image` value, prefer the shortcut form `"<source>:<search>"`. Sources: `unsplash`, `pexels`, `pixabay`, `iconify`, `url` (direct URL), `cms` (reusable handle, below).
240
+ 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
241
 
230
242
  - `"pexels:doctor"` — stock search
231
243
  - `"url:https://example.com/photo.jpg"` — specific asset
@@ -234,21 +246,33 @@ When writing an `image` value, prefer the shortcut form `"<source>:<search>"`. S
234
246
 
235
247
  On save, the sync engine resolves each shortcut to a real `{ "url", "alt" }` object.
236
248
 
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.
249
+ 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.
250
+
251
+ **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.
238
252
 
239
- **Reusable images:** for images used in multiple places (logo, recurring icons), declare them once in `content/images.json` as `handle → shortcut`:
253
+ **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
254
 
241
255
  ```json
242
- { "logo": "url:https://cdn.example.com/logo.svg", "hero": "pexels:mountain sunrise" }
256
+ { "logo": "iconify:lucide:sparkles", "hero": "pexels:mountain sunrise" }
243
257
  ```
244
258
 
245
- then reference with `"cms:logo"` in any image field, or fetch in templates via `getImage('logo')`.
259
+ 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
260
 
247
261
  **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
262
 
249
- ### Videos — read-only
263
+ ### Videos
264
+
265
+ `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>`:
266
+
267
+ ```json
268
+ { "hero-loop": "pexels:_ocean waves|Hero background loop" }
269
+ ```
270
+
271
+ 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.
272
+
273
+ Render in templates: `<%- embed(getVideo('hero-loop'), { size: '1280x720' }) %>`. Never hand-roll an `<iframe>`.
250
274
 
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>`.
275
+ 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
276
 
253
277
  ### Markdown content files (`.md`)
254
278
 
@@ -304,6 +328,7 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
304
328
  <head>
305
329
  <meta charset="UTF-8">
306
330
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
331
+ <% if (item.seo) { %><%- render(item.seo) %><% } %>
307
332
  </head>
308
333
  <body>
309
334
  <header><%- render(getEntry('header')) %></header>
@@ -321,7 +346,7 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
321
346
  | `getPages(path, opts?)` | Array | Pages whose path starts with the prefix. **Always a URL path, never a key.** `{ collection: true }` → collection pages only |
322
347
  | `getEntry(handle)` | Object \| Array | Entry by key. The `+` is optional — `getEntry('authors')` and `getEntry('authors+')` are equivalent. Single → object, collection → array |
323
348
  | `getSlugs(path)` | string[] | Slugs under a collection path |
324
- | `getImage(handle)` | Object \| undefined | Resolved image from `images.json` |
349
+ | `getImage(handle)` | Object \| undefined | Resolved image from `images.json` (bare handle, no `cms:` prefix) |
325
350
  | `getVideo(handle)` | Object \| undefined | Video from `videos.json` |
326
351
  | `getContent(query?)` | Any | Full content payload, optionally filtered with JMESPath |
327
352
  | `path(page)` | String | URL path of a page object |
@@ -345,6 +370,12 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
345
370
 
346
371
  `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
372
 
373
+ 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' })`).
374
+
375
+ > **Direct UI Icons vs. CMS-Editable Icons:**
376
+ > - **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.
377
+ > - **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)`.
378
+
348
379
  ### Helpers — video
349
380
 
350
381
  `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 +402,7 @@ Callable from **any** view (page, block, entry, layout); duplicates are removed
371
402
  <section data-aos="fade-up" data-aos-delay="150">...</section>
372
403
  ```
373
404
 
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.
405
+ **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
406
 
376
407
  ## 9. SEO
377
408
 
@@ -393,7 +424,7 @@ Make every site strongly SEO- and sharing-friendly. The standard pattern is a re
393
424
  <% if (item.image) meta({ property: 'og:image', content: src(item.image, '1200x630') }) %>
394
425
  ```
395
426
 
396
- Add `seo: block(seo)` to page models and `<%- render(item.seo) %>` in page views.
427
+ 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.
397
428
 
398
429
  ## 10. Forms
399
430
 
@@ -470,16 +501,47 @@ Always create an RSS feed for blogs. Use the page key `rss.xml` — because the
470
501
 
471
502
  Make it discoverable from the layout: `<% link({ rel: 'alternate', type: 'application/rss+xml', title: 'RSS', href: '/rss.xml' }) %>`
472
503
 
473
- ## 13. Critical Rules Checklist
504
+ ## 13. Ecommerce (Snipcart)
505
+
506
+ 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.
507
+
508
+ 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.
509
+ 2. **One line in the layout.** At the end of `<body>` in `layouts/common.ejs`:
510
+
511
+ ```ejs
512
+ <%- snipcart(getEntry('settings')?.snipcart_key) %>
513
+ ```
514
+
515
+ 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' }`.
516
+ 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:
517
+
518
+ ```ejs
519
+ <button class="snipcart-add-item"
520
+ data-item-id="<%= item._slug %>"
521
+ data-item-name="<%= item.title %>"
522
+ data-item-price="<%= Number(item.price).toFixed(2) %>"
523
+ data-item-url="<%= path(item) %>"
524
+ data-item-image="<%- src(item.image, '600x600') %>">
525
+ Add to cart
526
+ </button>
527
+ ```
528
+
529
+ `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).
530
+ 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.
531
+
532
+ ## 14. Critical Rules Checklist
474
533
 
475
534
  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
535
  2. Models first: a content field that isn't in the `.model` won't sync. Update the model, then the content.
477
536
  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`.
537
+ 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.
538
+ 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.
539
+ 6. `richtext` → `<%- item.x %>`. `markdown` → `<%- marked(item.x) %>`.
540
+ 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.
541
+ 8. Editable copy belongs in `content/`, not hard-coded in `.ejs`. Fixed micro-labels may stay in views.
542
+ 9. In page keys, `-` separates words and `_` means `/`. `_index` is the home page only; a section index page uses the plain section key.
543
+ 10. After edits, check `sync-errors.log` in the workspace root. New line = failure for that file; line removed = fixed.
544
+ 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')`).
545
+ 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.
546
+ 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.
547
+ 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).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleekcms/sync",
3
- "version": "2.7.2",
3
+ "version": "2.8.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",