@sleekcms/sync 4.3.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/AGENTS.md +19 -16
- package/dist/upload.mjs +14 -4
- package/package.json +1 -1
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-
|
|
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-
|
|
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">
|
|
@@ -420,7 +420,7 @@ Content lives in `src/content/` as plain JSON. Edit any content file and save
|
|
|
420
420
|
|
|
421
421
|
## Site setup (`src/setup.json`)
|
|
422
422
|
|
|
423
|
-
Site-wide decisions made once — the default page title, whether Tailwind is compiled, whether a sitemap is published, the cookie consent switch, the chat bot switch, and analytics tag ids — live in `src/setup.json`, mirrored from **Settings → Website** in the CMS. The file is JSON with a comment above each key explaining it:
|
|
423
|
+
Site-wide decisions made once — the default page title, whether Tailwind is compiled, whether a sitemap is published, the cookie consent switch, the chat bot switch and how the chat opens, and analytics tag ids — live in `src/setup.json`, mirrored from **Settings → Website** in the CMS. The file is JSON with a comment above each key explaining it:
|
|
424
424
|
|
|
425
425
|
```json5
|
|
426
426
|
{
|
|
@@ -429,6 +429,7 @@ Site-wide decisions made once — the default page title, whether Tailwind is co
|
|
|
429
429
|
sitemap_enabled: false, // publish sitemap.xml + robots.txt on every build
|
|
430
430
|
cookie_consent: false, // consent banner; holds analytics + data-sleekcms-consent scripts until accepted
|
|
431
431
|
chat_enabled: false, // chat bot on every page, answering from the site's published pages
|
|
432
|
+
chat_mode: "POPUP", // how the chat opens: "POPUP", "SIDEBAR" or "SPOTLIGHT" (any case accepted)
|
|
432
433
|
analytics: { // tag ids injected into every page; include only the ones you use
|
|
433
434
|
ga4: "G-XXXXXXXXXX",
|
|
434
435
|
},
|
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:
|
|
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
|
```
|
|
@@ -125,6 +125,7 @@ setup.json Site setup (JSON, comments allowed): default pag
|
|
|
125
125
|
| `sitemap_enabled` | boolean | `true` publishes `sitemap.xml` + `robots.txt` on every build. Turn it on for public sites and SEO work. Which pages are *excluded* is a CMS UI setting (Manage Site > Search Visibility), never this file. |
|
|
126
126
|
| `cookie_consent` | boolean | `true` injects a consent banner into every page and holds the analytics tags, plus any script or iframe tagged `data-sleekcms-consent`, until the visitor accepts its category. A toggle only — banner text, position and styling are authored in HTML (§14). |
|
|
127
127
|
| `chat_enabled` | boolean | `true` adds the chat bot to every published page; it answers visitor questions from the site's published pages (re-read on every publish). A toggle only — the welcome message, knowledge files and instructions are managed in the CMS. `false` removes it from the next publish and stops open chats right away. |
|
|
128
|
+
| `chat_mode` | string | How the chat opens when a visitor clicks its launcher: `"POPUP"` (default, a small panel in the corner), `"SIDEBAR"` (a full-height panel docked right, over the page) or `"SPOTLIGHT"` (a centred search-style dialog). Any case is accepted and saved uppercase. On phones sidebar and spotlight open full screen. Changes with the next publish. A page can override it: `data-sleek-chat="sidebar"` (or `"spotlight"`, `"popup"`) on any button or link opens chat in that mode, and `--sleek-chat-mode` in the site's CSS sets it per page or breakpoint. |
|
|
128
129
|
| `analytics` | object | Marketing/analytics tag ids injected into every published page. |
|
|
129
130
|
|
|
130
131
|
`analytics` is a flat map — include only the keys the site uses:
|
|
@@ -249,7 +250,7 @@ Like JSON but with **no quotes**; each value is a field type name:
|
|
|
249
250
|
| `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
251
|
| `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
252
|
| `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
|
|
253
|
+
| `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
254
|
| `json` | Any JSON |
|
|
254
255
|
| `sheet` | Array of arrays |
|
|
255
256
|
| `location` | `{ "markers": [{ "lat", "lng" }], "img": "<google static map url>" }` — `img` supports Google Static Maps params; markers are already part of the URL |
|
|
@@ -362,18 +363,18 @@ Content `video` fields accept the same refs: `"cms:<handle>"` reuses a declared
|
|
|
362
363
|
|
|
363
364
|
### Files
|
|
364
365
|
|
|
365
|
-
`content/files.json` lists the site's files (Media > Files) as `
|
|
366
|
+
`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
367
|
|
|
367
368
|
```json
|
|
368
|
-
{ "lunch-menu": { "file": "lunch-menu.pdf", "
|
|
369
|
+
{ "lunch-menu.pdf": { "file": "lunch-menu.pdf", "url": "/_files/lunch-menu.pdf", "size": 48213 } }
|
|
369
370
|
```
|
|
370
371
|
|
|
371
372
|
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
373
|
|
|
373
|
-
A `file` field takes a listed
|
|
374
|
+
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
375
|
|
|
375
376
|
```ejs
|
|
376
|
-
<% const menu = getFile('lunch-menu') %>
|
|
377
|
+
<% const menu = getFile('lunch-menu.pdf') %>
|
|
377
378
|
<% if (menu) { %><a href="<%= menu.url %>" download><%= menu.name %></a><% } %>
|
|
378
379
|
```
|
|
379
380
|
|
|
@@ -391,7 +392,7 @@ node upload.mjs menu-v2.pdf --name lunch-menu.pdf # store it under another fi
|
|
|
391
392
|
```
|
|
392
393
|
|
|
393
394
|
- **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
|
|
395
|
+
- **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
396
|
- 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
397
|
- Uploading needs an EDITOR or ADMIN on the site.
|
|
397
398
|
- 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 +473,7 @@ There is exactly **one** layout: `layouts/common.ejs`. It wraps every page autom
|
|
|
472
473
|
| `getSlugs(path)` | string[] | Slugs under a collection path |
|
|
473
474
|
| `getImage(handle)` | Object \| undefined | Resolved image from `images.json` (bare handle, no `cms:` prefix) |
|
|
474
475
|
| `getVideo(handle)` | Object \| undefined | Video from `videos.json` as `{ title, source, video_id, embed_url }` — render with `embed()` |
|
|
475
|
-
| `getFile(
|
|
476
|
+
| `getFile(name)` | Object \| undefined | File from `files.json` by its name, as `{ name, url, size, type, ext }`, like a `file` field |
|
|
476
477
|
| `getContent(query?)` | Any | Full content payload, optionally filtered with JMESPath |
|
|
477
478
|
| `path(page)` | String | URL path of a page object |
|
|
478
479
|
| `url(pathOrPage?)` | String | Site origin; pass a path or page object for a full absolute URL |
|
|
@@ -572,10 +573,10 @@ Three site-level SEO settings live in `setup.json` (§4), not in views: `sitemap
|
|
|
572
573
|
|
|
573
574
|
## 10. Forms
|
|
574
575
|
|
|
575
|
-
Any `<form>` with `data-
|
|
576
|
+
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
577
|
|
|
577
578
|
```html
|
|
578
|
-
<form data-
|
|
579
|
+
<form data-sleeksite-form="Contact">
|
|
579
580
|
<input name="name" type="text" required>
|
|
580
581
|
<input name="email" type="email" required>
|
|
581
582
|
<textarea name="message"></textarea>
|
|
@@ -583,7 +584,7 @@ Any `<form>` with `data-sleekcms="<hardcoded name>"` works automatically — sub
|
|
|
583
584
|
</form>
|
|
584
585
|
```
|
|
585
586
|
|
|
586
|
-
|
|
587
|
+
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
588
|
|
|
588
589
|
**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
590
|
|
|
@@ -591,25 +592,27 @@ The `data-sleekcms` value groups submissions by form; each input's `name` is sto
|
|
|
591
592
|
<input name="resume" type="file" accept=".pdf,.doc,.docx">
|
|
592
593
|
```
|
|
593
594
|
|
|
595
|
+
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.
|
|
596
|
+
|
|
594
597
|
## 11. Bookings
|
|
595
598
|
|
|
596
599
|
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
600
|
|
|
598
|
-
**A booking is just a form
|
|
601
|
+
**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
602
|
|
|
600
603
|
```html
|
|
601
|
-
<form data-
|
|
604
|
+
<form data-sleeksite-book="Consultation">
|
|
602
605
|
<input name="name" type="text" required placeholder="Your name">
|
|
603
606
|
<input name="email" type="email" required placeholder="Email">
|
|
604
|
-
<
|
|
607
|
+
<div data-sleeksite-slot></div>
|
|
605
608
|
<textarea name="notes" placeholder="Anything we should know?"></textarea>
|
|
606
609
|
<button type="submit">Book appointment</button>
|
|
607
610
|
</form>
|
|
608
611
|
```
|
|
609
612
|
|
|
610
|
-
Rules: **include an email field** (`type="email"` or `name="email"`) — confirmation and manage links go there; the
|
|
613
|
+
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
614
|
|
|
612
|
-
The form named `
|
|
615
|
+
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
616
|
|
|
614
617
|
## 12. RSS Feeds
|
|
615
618
|
|
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
|
-
|
|
272
|
-
|
|
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: !!(
|
|
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}"
|
|
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
|
|