@sleekcms/sync 4.3.0 → 4.4.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/README.md CHANGED
@@ -398,7 +398,7 @@ Models describe the shape of your content. They're JSON-like files with unquoted
398
398
  **Forms** (no backend needed)
399
399
 
400
400
  ```html
401
- <form data-sleekcms="contact">
401
+ <form data-sleeksite-form="Contact">
402
402
  <input name="name" type="text" required>
403
403
  <input name="email" type="email" required>
404
404
  <textarea name="message"></textarea>
@@ -406,7 +406,7 @@ Models describe the shape of your content. They're JSON-like files with unquoted
406
406
  </form>
407
407
  ```
408
408
 
409
- Any form with `data-sleekcms="<name>"` captures and stores submissions in the CMS dashboard automatically. File uploads work out of the box too — add an `<input type="file">` and visitors can attach documents (PDF, Word, Excel, CSV, text) and images:
409
+ Any form with `data-sleeksite-form="<name>"` captures and stores submissions in the CMS dashboard automatically (`data-sleeksite-book` makes it a booking form with a slot picker). On sync the form is created if it doesn't exist and the CMS stores the view with the name replaced by the form's key, so the published form stays linked when it's renamed in the app; your local file keeps the name as you wrote it. File uploads work out of the box too — add an `<input type="file">` and visitors can attach documents (PDF, Word, Excel, CSV, text) and images:
410
410
 
411
411
  ```html
412
412
  <input name="resume" type="file" accept=".pdf">
package/dist/AGENTS.md CHANGED
@@ -102,7 +102,7 @@ content/entries/<key>.json Content for a single entry (object)
102
102
  content/entries/<key>+.json Collection entry — ONE file holding a JSON array of all items
103
103
  content/images.json Site-level reusable images: handle → shortcut (§7)
104
104
  content/videos.json Site-level videos: handle → pexels:/pixabay: ref (user uploads show as embed_url) (§7)
105
- content/files.json The site's files (Media > Files), read-only: handle → { file, name, url } (§7)
105
+ content/files.json The site's files (Media > Files), read-only: file name → { file, url, size } (§7)
106
106
 
107
107
  setup.json Site setup (JSON, comments allowed): default page title, Tailwind on/off, sitemap on/off, cookie consent on/off, analytics tag ids — edit values in place, never delete
108
108
  ```
@@ -249,7 +249,7 @@ Like JSON but with **no quotes**; each value is a field type name:
249
249
  | `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 |
250
250
  | `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 |
251
251
  | `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 |
252
- | `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). A listed file's handle from `content/files.json` works too, and wins over a file with that name. On save the CMS attaches that file, or (for a file name nothing matches) 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` |
252
+ | `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). A file listed in `content/files.json` is written by its listed name (`"forms/booking.docx"` for one in a folder). On save the CMS attaches that file, or (for a file name nothing matches) 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` |
253
253
  | `json` | Any JSON |
254
254
  | `sheet` | Array of arrays |
255
255
  | `location` | `{ "markers": [{ "lat", "lng" }], "img": "<google static map url>" }` — `img` supports Google Static Maps params; markers are already part of the URL |
@@ -362,18 +362,18 @@ Content `video` fields accept the same refs: `"cms:<handle>"` reuses a declared
362
362
 
363
363
  ### Files
364
364
 
365
- `content/files.json` lists the site's files (Media > Files) as `handle → { file, name, url }`: `file` is the file name, `name` its display name, `url` its public download link.
365
+ `content/files.json` lists the site's files (Media > Files) as `file name → { file, url, size }`: `file` is the value a `file` field takes, `url` the path the site serves it at, `size` its bytes.
366
366
 
367
367
  ```json
368
- { "lunch-menu": { "file": "lunch-menu.pdf", "name": "Lunch Menu", "url": "https://files.sleekcms.com/…/lunch-menu.pdf" } }
368
+ { "lunch-menu.pdf": { "file": "lunch-menu.pdf", "url": "/_files/lunch-menu.pdf", "size": 48213 } }
369
369
  ```
370
370
 
371
371
  It's read-only: edits to it are never saved, and every sync replaces it with the site's current list. Check it before writing a new file name.
372
372
 
373
- A `file` field takes a listed handle (`"lunch-menu"`) or file name (`"lunch-menu.pdf"`). Render a file no field points at with `getFile(handle)`, which returns the same `{ name, url, size, type, ext }` as a `file` field (or `undefined` for an unknown handle):
373
+ A `file` field takes a listed file name (`"lunch-menu.pdf"`). Render a file no field points at with `getFile(name)`, which returns the same `{ name, url, size, type, ext }` as a `file` field (or `undefined` for an unknown name):
374
374
 
375
375
  ```ejs
376
- <% const menu = getFile('lunch-menu') %>
376
+ <% const menu = getFile('lunch-menu.pdf') %>
377
377
  <% if (menu) { %><a href="<%= menu.url %>" download><%= menu.name %></a><% } %>
378
378
  ```
379
379
 
@@ -391,7 +391,7 @@ node upload.mjs menu-v2.pdf --name lunch-menu.pdf # store it under another fi
391
391
  ```
392
392
 
393
393
  - **Images** (png, jpg, jpeg, webp, gif, svg, ico, up to 8 MB) become site images under a handle. The handle is `--handle`, or by default the file name lowercased and dashed (`Team Photo.jpg` → `team-photo`). The script adds the handle to `content/images.json`. Reference the image as `"cms:<handle>"` in any `image` field, or with `img('cms:<handle>')` / `src('cms:<handle>')` in a view. Uploading to an existing handle replaces that image everywhere it's used.
394
- - **Documents** (pdf, docx, xlsx, pptx, csv, tsv, txt, md, up to 10 MB) are added to Media > Files under their file name, and `content/files.json` is rewritten to list them. Put the printed file name (or its handle) in a `file` field, or render it with `getFile('<handle>')`. Uploading the same file name again replaces that file.
394
+ - **Documents** (pdf, docx, xlsx, pptx, csv, tsv, txt, md, up to 10 MB) are added to Media > Files under their file name, and `content/files.json` is rewritten to list them. Put the printed file name in a `file` field, or render it with `getFile('<file name>')`. Uploading the same file name again replaces that file.
395
395
  - The script prints the ref to use for each file. Copy it from the output rather than guessing it, because the server cleans up file names. Add `--json` for one JSON result per line. Exit code `0` means every file uploaded, `1` means some failed (the reason is printed), and `2` means it couldn't run.
396
396
  - Uploading needs an EDITOR or ADMIN on the site.
397
397
  - Never copy images or documents into `src/`. Binary files there are not synced; the sync log just points you back to `upload.mjs`.
@@ -472,7 +472,7 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
472
472
  | `getSlugs(path)` | string[] | Slugs under a collection path |
473
473
  | `getImage(handle)` | Object \| undefined | Resolved image from `images.json` (bare handle, no `cms:` prefix) |
474
474
  | `getVideo(handle)` | Object \| undefined | Video from `videos.json` as `{ title, source, video_id, embed_url }` — render with `embed()` |
475
- | `getFile(handle)` | Object \| undefined | File from `files.json` as `{ name, url, size, type, ext }`, like a `file` field |
475
+ | `getFile(name)` | Object \| undefined | File from `files.json` by its name, as `{ name, url, size, type, ext }`, like a `file` field |
476
476
  | `getContent(query?)` | Any | Full content payload, optionally filtered with JMESPath |
477
477
  | `path(page)` | String | URL path of a page object |
478
478
  | `url(pathOrPage?)` | String | Site origin; pass a path or page object for a full absolute URL |
@@ -572,10 +572,10 @@ Three site-level SEO settings live in `setup.json` (§4), not in views: `sitemap
572
572
 
573
573
  ## 10. Forms
574
574
 
575
- 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.
575
+ Any `<form>` with `data-sleeksite-form="<name>"` works automatically — submissions are captured and shown in the CMS dashboard. No backend, no action URL, no JS.
576
576
 
577
577
  ```html
578
- <form data-sleekcms="contact">
578
+ <form data-sleeksite-form="Contact">
579
579
  <input name="name" type="text" required>
580
580
  <input name="email" type="email" required>
581
581
  <textarea name="message"></textarea>
@@ -583,7 +583,7 @@ Any `<form>` with `data-sleekcms="<hardcoded name>"` works automatically — sub
583
583
  </form>
584
584
  ```
585
585
 
586
- The `data-sleekcms` value groups submissions by form; each input's `name` is stored as-is. Add an `<input type="datetime-local">` and the form becomes a **booking form** — see §11.
586
+ Write the form's **name** (e.g. `Contact`, `Newsletter`, `Quote request`), literally — never a template expression. On sync the form is created if it doesn't exist, and the CMS stores the view with the name replaced by the form's key (e.g. `data-sleeksite-form="mh3k2x9a"`), so the published form stays linked when the user renames it in the app. Your local file keeps the name as you wrote it. A key you find in a view is the form's id: keep it as it is. Each input's `name` is stored as-is.
587
587
 
588
588
  **File uploads** are supported out of the box — add an `<input type="file">` and visitors can attach documents (PDF, Word, Excel, CSV, text) and images (add `multiple` for several):
589
589
 
@@ -591,25 +591,27 @@ The `data-sleekcms` value groups submissions by form; each input's `name` is sto
591
591
  <input name="resume" type="file" accept=".pdf,.doc,.docx">
592
592
  ```
593
593
 
594
+ For visitors to **book a time**, use `data-sleeksite-book` instead — see §11. A walk-in queue's "Schedule a time" form uses `data-sleeksite-queue="<queue name>"` (fields `queue_date`, an empty `<select name="queue_time">`, `name`, `email`) and goes to the queue, not the inbox; the queue is created on sync, with the same name-to-key rule as forms.
595
+
594
596
  ## 11. Bookings
595
597
 
596
598
  Use this whenever visitors should **book or schedule** something — an appointment, a call, a demo, a consultation, a class, or a reservation ("pick a time / reserve a slot"). It fits appointment-based businesses: salon, spa, barber, dental/medical clinic, therapist, tutor, photographer, consultant, repair shop, restaurant reservations. **Only add a booking form when the business genuinely takes appointments and the user asks for it (or it is clearly core to the business) — never add one speculatively to an arbitrary site.**
597
599
 
598
- **A booking is just a form.** Give a normal `data-sleekcms` form an `<input type="datetime-local">` — at publish time that input is replaced by an availability slot picker, and submissions become appointments with email confirmation, calendar invites, and no-login cancel/reschedule. Every other field is yours: bookings store arbitrary fields, exactly like form submissions.
600
+ **A booking is just a form** with `data-sleeksite-book="<name>"` instead of `data-sleeksite-form` — on the page it gets an availability slot picker, and submissions become appointments with email confirmation, calendar invites, and no-login cancel/reschedule. Every other field is yours: bookings store arbitrary fields, exactly like form submissions.
599
601
 
600
602
  ```html
601
- <form data-sleekcms="consultation">
603
+ <form data-sleeksite-book="Consultation">
602
604
  <input name="name" type="text" required placeholder="Your name">
603
605
  <input name="email" type="email" required placeholder="Email">
604
- <input name="slot" type="datetime-local" required>
606
+ <div data-sleeksite-slot></div>
605
607
  <textarea name="notes" placeholder="Anything we should know?"></textarea>
606
608
  <button type="submit">Book appointment</button>
607
609
  </form>
608
610
  ```
609
611
 
610
- Rules: **include an email field** (`type="email"` or `name="email"`) — confirmation and manage links go there; the datetime input's own `name` doesn't matter (it is replaced by the picker, and the chosen time is stored as the booking's slot); style the form like any other — the picker inherits the page's `--accent` CSS variable (or set `data-accent` on the form).
612
+ Rules: **include an email field** (`type="email"` or `name="email"`) — confirmation and manage links go there; the picker goes where an element with `data-sleeksite-slot` (or an `<input type="datetime-local">`) is, and replaces it — without one it goes just before the submit button, and the chosen time is stored as the booking's slot; style the form like any other — the picker inherits the page's `--accent` CSS variable (or set `data-accent` on the form).
611
613
 
612
- The form named `consultation` is **auto-created on sync** like any form, and because it has a datetime input it gets default availability: 30-minute slots, one booking at a time, Mon–Fri 9 AM–5 PM in America/New_York, bookings confirmed instantly (email-confirmation off — the owner can require it in settings). The defaults are guesses — tell the user to review the timezone and open hours in the form's **Booking settings** (Customers › Inbox); appointments appear under **Customers › Bookings**. Reusing the same name across pages shares one booking form.
614
+ The form named `Consultation` is **auto-created on sync** like any form (the same name-to-key rule as §10 applies), and as a booking form it gets default availability: 30-minute slots, one booking at a time, Mon–Fri 9 AM–5 PM in America/New_York, bookings confirmed instantly (email-confirmation off — the owner can require it in settings). The defaults are guesses — tell the user to review the timezone and open hours in the form's **Booking settings** (Customers › Inbox); appointments appear under **Customers › Bookings**. Reusing the same name (or key) across pages shares one booking form. The visitor's confirm and manage links open pages on the site's own domain (`/_book/…`) — nothing to build.
613
615
 
614
616
  ## 12. RSS Feeds
615
617
 
package/dist/upload.mjs CHANGED
@@ -268,8 +268,18 @@ export async function uploadOne(opts) {
268
268
  const ext = extOf(name);
269
269
  if (TEXT_FILE_EXTS.includes(ext) && data.length <= MAX_INLINE_TEXT_BYTES) body.content = data.toString("utf-8");
270
270
  const created = await api(base, token, "POST", "/create_asset", body, fetchImpl);
271
- if (created.upload) await sendBytes(created.upload, data, fetchImpl);
272
- writeFilesIndex(ws, created.files);
271
+ let files = created.files;
272
+ if (created.upload) {
273
+ await sendBytes(created.upload, data, fetchImpl);
274
+ // The listing create_asset returned predates the bytes (a new file is only
275
+ // listed once stored), so ask again now that they are.
276
+ try {
277
+ files = await api(base, token, "GET", "/get_files?kind=files", undefined, fetchImpl);
278
+ } catch {
279
+ // keep the earlier listing; the next sync pull corrects it
280
+ }
281
+ }
282
+ writeFilesIndex(ws, files);
273
283
  return {
274
284
  source: filePath,
275
285
  type: "file",
@@ -277,7 +287,7 @@ export async function uploadOne(opts) {
277
287
  handle: created.handle || undefined,
278
288
  replaced: !!created.replaces_existing,
279
289
  url: created.url || undefined,
280
- indexed: !!(created.files || []).some((f) => f.path === FILES_INDEX),
290
+ indexed: !!(files || []).some((f) => f.path === FILES_INDEX),
281
291
  };
282
292
  }
283
293
 
@@ -290,7 +300,7 @@ export function describe(r) {
290
300
  return lines;
291
301
  }
292
302
  const lines = [`✅ ${r.replaced ? "Replaced" : "Uploaded"} file ${path.basename(r.source)} → "${r.ref}"${r.url ? ` (${r.url})` : ""}`];
293
- lines.push(` Use "${r.ref}"${r.handle ? ` (or its handle "${r.handle}")` : ""} as a file field's value, or getFile('${r.handle || r.ref}') in a view.`);
303
+ lines.push(` Use "${r.ref}" as a file field's value, or getFile('${r.ref}') in a view.`);
294
304
  return lines;
295
305
  }
296
306
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleekcms/sync",
3
- "version": "4.3.0",
3
+ "version": "4.4.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",