@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.
- package/dist/AGENT.md +92 -30
- 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
|
|
83
|
-
content/pages/<key
|
|
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
|
|
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 →
|
|
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 (§
|
|
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:
|
|
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.
|
|
159
|
-
| `
|
|
160
|
-
| `
|
|
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 ``. 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
|
-
**
|
|
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
|
|
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": "
|
|
256
|
+
{ "logo": "iconify:lucide:sparkles", "hero": "pexels:mountain sunrise" }
|
|
243
257
|
```
|
|
244
258
|
|
|
245
|
-
then reference with `"cms:logo"` in any image field, or
|
|
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: ``. Append `|<alt>` for alt text and an optional `<W>x<H>` token to set rendered size (default `600x400`): ``. Refs are rewritten to CDN URLs on save.
|
|
248
262
|
|
|
249
|
-
### Videos
|
|
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
|
-
`
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
479
|
-
5. `
|
|
480
|
-
6.
|
|
481
|
-
7.
|
|
482
|
-
8.
|
|
483
|
-
9.
|
|
484
|
-
10.
|
|
485
|
-
11.
|
|
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).
|