@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 +2 -2
- package/dist/AGENTS.md +18 -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">
|
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
|
```
|
|
@@ -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
|
|
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 `
|
|
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", "
|
|
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
|
|
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
|
|
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(
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
-
<
|
|
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
|
|
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 `
|
|
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
|
-
|
|
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
|
|