@sleekcms/sync 2.14.0 → 2.15.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 +195 -5
- package/package.json +1 -1
package/dist/AGENT.md
CHANGED
|
@@ -450,16 +450,16 @@ Callable from **any** view (page, block, entry, layout); duplicates are removed
|
|
|
450
450
|
| `style(css, order?)` | Add `<style>` |
|
|
451
451
|
| `script(value, order?)` | Add `<script>` — `.js` URL → external, otherwise inline |
|
|
452
452
|
|
|
453
|
-
`script()`/`link()` also accept a predefined library name — `
|
|
453
|
+
`script()`/`link()` also accept a predefined library name — `motion` (Motion, formerly Framer Motion: the `Motion` global, plus `import … from 'motion'` in a `type: 'module'` script), `gsap` (includes ScrollTrigger, pre-registered), `swiper`, `animejs`, `chartjs`, `threejs` (import map — pair with a `type: 'module'` script), `lottie`, `typedjs`, `splide`, `htmx`, `vanilla-tilt`, and legacy `aos` — either call injects all of the library's CDN assets, deduped. Prefer these names over hand-picked CDN URLs, and only add a library the site actually uses.
|
|
454
454
|
|
|
455
455
|
`script()` also takes an attributes object — `script({ src: '/js/app.js', defer: '' })`, `script({ content: '...', type: 'module' })` — so a tag that needs attributes still never needs a raw `<script>`. That is how a third-party script is put behind cookie consent: `script({ src: 'https://…', 'data-sleekcms-consent': 'marketing' })` (§14).
|
|
456
456
|
|
|
457
|
+
Animation needs no `script()` call at all — `data-motion` attributes are built in (§15):
|
|
458
|
+
|
|
457
459
|
```ejs
|
|
458
|
-
|
|
459
|
-
<section data-aos="fade-up">
|
|
460
|
+
<section data-motion="fade-up">
|
|
460
461
|
<h2><%= item.title %></h2>
|
|
461
462
|
</section>
|
|
462
|
-
<section data-aos="fade-up" data-aos-delay="150">...</section>
|
|
463
463
|
```
|
|
464
464
|
|
|
465
465
|
**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.
|
|
@@ -666,7 +666,196 @@ No config element → all defaults. One per site: a second one is ignored with a
|
|
|
666
666
|
|
|
667
667
|
**Notes** — gating happens at build time: tagged scripts ship as `type="text/plain"` with their `src` stashed, and gated iframes ship without `src`, so nothing can fire before consent even on slow connections. The banner is a fixed overlay (or modal), so no layout shift. `<noscript>` content cannot be gated, and `document.write` inside a gated script is unsupported. SleekCMS provides the consent *mechanism*; which script belongs to which category, and the privacy policy itself, are the site owner's call — ask when unsure, and link the policy page via `data-policy-url`.
|
|
668
668
|
|
|
669
|
-
## 15.
|
|
669
|
+
## 15. Motion & Animation
|
|
670
|
+
|
|
671
|
+
Motion directs attention and makes the site feel responsive; every animation has to buy back the moment it costs the visitor. A well-animated page usually has **one** composed entrance above the fold, scroll reveals with **2-3** related treatments (not one fade-up on everything), tactile hover/focus feedback on every interactive element, **at most one** showpiece scroll moment (a parallax hero, a pinned zoom, a horizontal gallery) and **at most one** ambient loop. A calm brochure site may need only the hover feedback.
|
|
672
|
+
|
|
673
|
+
**Motion that hurts:** animating everything; entrances that hold content hostage (above the fold finishes in ~800ms); re-animating on every scroll pass; animating `width`/`height`/`top`/`left`/`margin` instead of `transform`/`opacity`; scroll-hijacking that takes the scroll wheel away from the visitor (parallax driven by their own scrolling is fine); and any failure that leaves content stuck at `opacity: 0`.
|
|
674
|
+
|
|
675
|
+
**Pick the smallest tool:**
|
|
676
|
+
|
|
677
|
+
| Need | Use |
|
|
678
|
+
|---|---|
|
|
679
|
+
| Hover, focus, active feedback; a looping ambient effect | **Tailwind alone** — `transition`, `hover:`, `active:`, `animate-*` |
|
|
680
|
+
| Reveals, staggered grids, split headlines, counters, parallax, scroll-scrubbed zoom/slide/fade, a pinned horizontal gallery, a marquee, tilt or magnetic hover | **`data-motion` attributes** — built in; add the attribute and the build loads everything. No `script()` call. |
|
|
681
|
+
| An effect the attributes can't express — a click-driven sequence, a scroll callback, a cursor follower | **`script('motion')`** — Motion (formerly Framer Motion) for your own script |
|
|
682
|
+
| A swipeable carousel · typewriter · Lottie | `swiper` / `splide` · `typedjs` · `lottie` (§8) |
|
|
683
|
+
|
|
684
|
+
Never hand-write an `IntersectionObserver` reveal, a parallax scroll handler, or a count-up — the attributes are exactly that. Never load two motion libraries: `data-motion` and `script('motion')` are one (Motion); adding `aos` or `gsap` to the same page makes two.
|
|
685
|
+
|
|
686
|
+
### `data-motion` attributes
|
|
687
|
+
|
|
688
|
+
Any page whose markup uses a `data-motion*` attribute gets Motion and the SleekCMS motion runtime injected at build time. Each element reveals **once**, when its top passes 90% of the screen height. `prefers-reduced-motion` is handled for you (nothing moves; only scroll-linked fades remain), content is never left hidden (if anything fails to load, the page shows normally within 3 seconds), and elements added later are picked up too.
|
|
689
|
+
|
|
690
|
+
| Attribute | What it does |
|
|
691
|
+
|---|---|
|
|
692
|
+
| `data-motion="fade-up"` | Reveal on scroll. Presets: `fade` `fade-up` `fade-down` `fade-left` `fade-right` · `zoom-in` `zoom-out` · `blur-in` · `pop` (springy scale) · `flip-up` `flip-down` `flip-left` `flip-right` · `reveal-up` `reveal-down` `reveal-left` `reveal-right` (clip-path wipe) · `draw` (stroked SVG draws itself) |
|
|
693
|
+
| `data-motion="y: 60; rotate: -3; opacity: 0"` | A custom start state instead of a preset — keys `opacity` `x` `y` `scale` `rotate` `rotateX` `rotateY` `blur` `clip-path`; numbers are px / deg |
|
|
694
|
+
| `data-motion-delay="150"` `data-motion-duration="800"` `data-motion-easing="ease-out"` `data-motion-distance="40"` | Tuning, in ms like AOS (`"0.4s"` also works). Easing also takes `linear` `ease-in-out` `ease-out-cubic` `ease-out-back` `spring` or `[0.2,0.8,0.2,1]`. Defaults: 600ms, a soft ease-out, 24px. |
|
|
695
|
+
| `data-motion-stagger="90"` | On a container: its `data-motion` children that come into view together play in order, 90ms apart — no index math in loops. |
|
|
696
|
+
| `data-motion-text="words"` / `"chars"` | Splits the text and reveals it piece by piece with the element's own `data-motion` preset (default `fade-up`); `data-motion-stagger` sets the step. Headings and short lines. |
|
|
697
|
+
| `data-motion-counter` | Counts up to the number written inside it — `1,250+`, `$4.9M`, `98%` keep their formatting. |
|
|
698
|
+
| `data-motion-parallax="0.2"` | Drifts against the scroll — larger is slower and feels further away (0.1–0.3 is plenty); negative feels closer. |
|
|
699
|
+
| `data-motion-scroll="scale: .85 1; opacity: .4 1"` | Scrubs values with the scrollbar. Keys as above plus `scaleX` `scaleY` and any CSS property; more than two values spread evenly across the range. |
|
|
700
|
+
| `data-motion-scroll-range="enter"` | `enter` (default — while it scrolls into view) · `exit` · `cover` (its whole trip across the screen) · `pin` (while a tall section's sticky child is pinned) · `page` (the whole page) |
|
|
701
|
+
| `data-motion-scroll-target="section"` | Measure another element — the nearest ancestor matching the selector (or the first match on the page). |
|
|
702
|
+
| `data-motion-horizontal` + `data-motion-track` | Pinned horizontal gallery: vertical scrolling slides the track sideways. |
|
|
703
|
+
| `data-motion-hover="tilt"` / `"magnetic"` | Spring-physics tilt toward the pointer, or a pull toward it (`"tilt 12"`, `"magnetic 0.4"` set the strength). Mouse and trackpad only. |
|
|
704
|
+
| `data-motion-marquee="40"` | Endless ticker on the row it sits on, px per second (negative runs right); slows on hover, pauses off screen. |
|
|
705
|
+
|
|
706
|
+
**One job per element:** an element with `data-motion-parallax`, `-scroll` or `-hover` owns its `transform` — wrap it rather than stacking another motion attribute or Tailwind `translate-*`/`scale-*`/`rotate-*` on it. **Sticky needs unclipped ancestors:** `pin` and `horizontal` rely on `position: sticky`, which an `overflow-hidden` ancestor silently breaks — use `overflow-x-clip`.
|
|
707
|
+
|
|
708
|
+
### Recipes
|
|
709
|
+
|
|
710
|
+
```ejs
|
|
711
|
+
<%# Hero entrance — one composed moment, in order %>
|
|
712
|
+
<div data-motion-stagger="120" class="mx-auto max-w-4xl px-6 pt-36 pb-24 text-center">
|
|
713
|
+
<p data-motion="fade-up" class="text-sm font-semibold uppercase tracking-[0.2em] text-brand-600"><%= item.eyebrow %></p>
|
|
714
|
+
<h1 data-motion="fade-up" class="mt-5 font-display text-6xl tracking-tight md:text-8xl"><%= item.title %></h1>
|
|
715
|
+
<p data-motion="fade-up" class="mx-auto mt-6 max-w-[46ch] text-lg text-muted"><%= item.lede %></p>
|
|
716
|
+
<a data-motion="pop" href="/book" class="mt-10 inline-flex rounded-full bg-brand-600 px-8 py-4 text-white">Book a table</a>
|
|
717
|
+
</div>
|
|
718
|
+
|
|
719
|
+
<%# Staggered cards from a loop %>
|
|
720
|
+
<div data-motion-stagger="90" class="grid gap-6 md:grid-cols-3">
|
|
721
|
+
<% item.services.forEach(function (s) { %><article data-motion="fade-up" class="rounded-card bg-surface p-8">…</article><% }) %>
|
|
722
|
+
</div>
|
|
723
|
+
|
|
724
|
+
<%# Split headline — words rise out of a mask; chars + fade reads like typing %>
|
|
725
|
+
<h2 data-motion="reveal-up" data-motion-text="words" class="font-display text-6xl md:text-8xl">Bread worth waking up for.</h2>
|
|
726
|
+
<p data-motion="fade" data-motion-text="chars" data-motion-stagger="30" class="font-mono text-lg">Baked fresh at 5am.</p>
|
|
727
|
+
|
|
728
|
+
<%# Image wipe %>
|
|
729
|
+
<figure data-motion="reveal-left" data-motion-duration="1100" class="overflow-hidden rounded-card"><%- img(item.photo, { w: 1200, h: 800, class: 'w-full object-cover' }) %></figure>
|
|
730
|
+
|
|
731
|
+
<%# Parallax background — the layer drifts at most speed × half the screen height,
|
|
732
|
+
so extend it past its frame by at least that: -inset-y-[12vh] covers 0.2 %>
|
|
733
|
+
<section class="relative isolate h-[80vh] overflow-hidden">
|
|
734
|
+
<div data-motion-parallax="0.2" class="absolute inset-x-0 -inset-y-[12vh] -z-10"><%- img(item.backdrop, { w: 1920, h: 1400, class: 'h-full w-full object-cover' }) %></div>
|
|
735
|
+
<div class="mx-auto max-w-5xl px-6 pt-48 text-white">…</div>
|
|
736
|
+
</section>
|
|
737
|
+
|
|
738
|
+
<%# Layered depth — further back = larger speed; negative comes forward %>
|
|
739
|
+
<section class="relative isolate h-screen overflow-hidden">
|
|
740
|
+
<div data-motion-parallax="0.3" class="absolute inset-x-0 -inset-y-[16vh] -z-20"><%- img(item.sky, { class: 'h-full w-full object-cover' }) %></div>
|
|
741
|
+
<div data-motion-parallax="0.12" class="absolute inset-x-0 bottom-0 -z-10"><%- img(item.hills, { class: 'w-full' }) %></div>
|
|
742
|
+
<h1 data-motion-parallax="-0.06" class="relative pt-[30vh] text-center font-display text-8xl text-white"><%= item.title %></h1>
|
|
743
|
+
</section>
|
|
744
|
+
|
|
745
|
+
<%# Reading progress bar — in layouts/common.ejs %>
|
|
746
|
+
<div data-motion-scroll="scaleX: 0 1" data-motion-scroll-range="page" class="fixed inset-x-0 top-0 z-50 h-1 origin-left bg-brand-600 motion-reduce:hidden"></div>
|
|
747
|
+
|
|
748
|
+
<%# Zoom into view %>
|
|
749
|
+
<figure data-motion-scroll="scale: .85 1; opacity: .5 1" class="mx-auto max-w-6xl overflow-hidden rounded-[2rem]"><%- img(item.showcase, { w: 1600, h: 1000, class: 'w-full' }) %></figure>
|
|
750
|
+
|
|
751
|
+
<%# Sticky media with scrolling steps — CSS sticky does the pinning %>
|
|
752
|
+
<section class="mx-auto grid max-w-6xl gap-16 px-6 py-32 md:grid-cols-2">
|
|
753
|
+
<div class="md:sticky md:top-24 md:h-[70vh]"><%- img(item.media, { class: 'h-full w-full rounded-card object-cover' }) %></div>
|
|
754
|
+
<div class="space-y-[30vh] md:py-[20vh]">
|
|
755
|
+
<% item.steps.forEach(function (step) { %><article data-motion="fade-up"><h3 class="font-display text-3xl"><%= step.title %></h3><p class="mt-3 text-muted"><%= step.body %></p></article><% }) %>
|
|
756
|
+
</div>
|
|
757
|
+
</section>
|
|
758
|
+
|
|
759
|
+
<%# Pinned scrollytelling — section height sets how long it pins; each caption holds for its third (two "1" stops) and cross-fades %>
|
|
760
|
+
<section class="relative h-[300vh]">
|
|
761
|
+
<div class="sticky top-0 grid h-screen place-items-center overflow-hidden">
|
|
762
|
+
<div data-motion-scroll="scale: .6 1; opacity: .3 1" data-motion-scroll-range="pin" data-motion-scroll-target="section" class="w-[min(90vw,1100px)] overflow-hidden rounded-[2rem]"><%- img(item.feature, { w: 1600, h: 1000, class: 'w-full' }) %></div>
|
|
763
|
+
<p data-motion-scroll="opacity: 1 1 0 0 0 0" data-motion-scroll-range="pin" data-motion-scroll-target="section" class="absolute bottom-16 text-2xl">Sourced locally.</p>
|
|
764
|
+
<p data-motion-scroll="opacity: 0 0 1 1 0 0" data-motion-scroll-range="pin" data-motion-scroll-target="section" class="absolute bottom-16 text-2xl">Milled by hand.</p>
|
|
765
|
+
<p data-motion-scroll="opacity: 0 0 0 0 1 1" data-motion-scroll-range="pin" data-motion-scroll-target="section" class="absolute bottom-16 text-2xl">Baked every morning.</p>
|
|
766
|
+
</div>
|
|
767
|
+
</section>
|
|
768
|
+
|
|
769
|
+
<%# Horizontal gallery — sized by the runtime; a plain swipeable row without JS or with reduced motion %>
|
|
770
|
+
<section data-motion-horizontal class="relative">
|
|
771
|
+
<div class="sticky top-0 flex h-screen items-center overflow-x-auto">
|
|
772
|
+
<div data-motion-track class="flex gap-6 px-[8vw]">
|
|
773
|
+
<% item.projects.forEach(function (p) { %><article class="w-[80vw] shrink-0 md:w-[42vw]"><%- img(p.image, { w: 900, h: 1100, class: 'rounded-card' }) %></article><% }) %>
|
|
774
|
+
</div>
|
|
775
|
+
</div>
|
|
776
|
+
</section>
|
|
777
|
+
|
|
778
|
+
<%# Oversized type sliding sideways — x in % of its own width %>
|
|
779
|
+
<div class="overflow-x-clip py-16"><p data-motion-scroll="x: 10% -35%" data-motion-scroll-range="cover" class="whitespace-nowrap font-display text-[18vw] leading-none">Fresh · Local · Daily ·</p></div>
|
|
780
|
+
|
|
781
|
+
<%# Stat counters — write the real figure: it's what search engines and reduced-motion visitors read %>
|
|
782
|
+
<dl data-motion-stagger="100" class="grid grid-cols-2 gap-8 md:grid-cols-4">
|
|
783
|
+
<% item.stats.forEach(function (s) { %><div data-motion="fade-up" class="flex flex-col-reverse"><dt class="mt-2 text-sm text-muted"><%= s.label %></dt><dd data-motion-counter class="font-display text-5xl tabular-nums"><%= s.value %></dd></div><% }) %>
|
|
784
|
+
</dl>
|
|
785
|
+
|
|
786
|
+
<%# Logo marquee — the runtime clones the row for a seamless loop %>
|
|
787
|
+
<div class="overflow-hidden py-10 [mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]">
|
|
788
|
+
<div data-motion-marquee="40" class="flex items-center gap-16"><% item.logos.forEach(function (logo) { %><%- img(logo, { h: 40, class: 'h-10 w-auto opacity-70' }) %><% }) %></div>
|
|
789
|
+
</div>
|
|
790
|
+
|
|
791
|
+
<%# Tilt card, magnetic button %>
|
|
792
|
+
<a href="/work/harbor" data-motion-hover="tilt" class="block rounded-card bg-surface p-8 shadow-xl">…</a>
|
|
793
|
+
<a href="/contact" data-motion-hover="magnetic" class="inline-flex rounded-full bg-brand-600 px-8 py-4 text-white">Start a project</a>
|
|
794
|
+
|
|
795
|
+
<%# Floating shape — an ambient loop is Tailwind: an @theme --animate-* keyframe token in css/tailwind.css %>
|
|
796
|
+
<div class="absolute -right-24 top-10 -z-10 size-72 rounded-full bg-brand-500/20 blur-3xl animate-drift motion-reduce:animate-none"></div>
|
|
797
|
+
|
|
798
|
+
<%# SVG line draw — stroked outlines only; data-motion-stagger on the <svg> draws shapes in turn %>
|
|
799
|
+
<svg data-motion="draw" data-motion-duration="1400" viewBox="0 0 400 120" fill="none" stroke="currentColor" stroke-width="3" class="w-full text-brand-600"><path d="M5 100 C 120 10, 260 10, 395 60" /></svg>
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
**Page transitions** — a cross-fade between pages with no JS, in `css/tailwind.css` (browsers without support just navigate):
|
|
803
|
+
|
|
804
|
+
```css
|
|
805
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
806
|
+
@view-transition { navigation: auto; }
|
|
807
|
+
}
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
### Custom effects with the Motion API
|
|
811
|
+
|
|
812
|
+
When the attributes can't express it, load Motion in the layout and write your own script **as a module**, importing Motion exactly as its documentation does:
|
|
813
|
+
|
|
814
|
+
```ejs
|
|
815
|
+
<%# layouts/common.ejs %>
|
|
816
|
+
<% script('motion') %>
|
|
817
|
+
<% script({ type: 'module', src: '/js/site.js' }) %>
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
```js
|
|
821
|
+
// js/site.js
|
|
822
|
+
import { animate, scroll, stagger } from 'motion';
|
|
823
|
+
|
|
824
|
+
if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
825
|
+
// Header hides on the way down, returns on the way up
|
|
826
|
+
const header = document.querySelector('.js-header');
|
|
827
|
+
let last = 0;
|
|
828
|
+
scroll((_, info) => {
|
|
829
|
+
const y = info.y.current;
|
|
830
|
+
header.classList.toggle('-translate-y-full', y > last && y > 120);
|
|
831
|
+
last = y;
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// Menu opens as a sequence: panel first, then its links in turn
|
|
835
|
+
document.querySelector('.js-menu-open')?.addEventListener('click', () => {
|
|
836
|
+
animate([
|
|
837
|
+
['.js-menu', { opacity: [0, 1], transform: ['translateY(-12px)', 'none'] }, { duration: 0.25 }],
|
|
838
|
+
['.js-menu a', { opacity: [0, 1], transform: ['translateX(-8px)', 'none'] }, { delay: stagger(0.05), at: '-0.1' }],
|
|
839
|
+
]);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
// A dot that springs after the pointer
|
|
843
|
+
const dot = document.querySelector('.js-cursor');
|
|
844
|
+
if (dot && matchMedia('(pointer: fine)').matches) {
|
|
845
|
+
addEventListener('pointermove', (e) => animate(dot, { x: e.clientX, y: e.clientY }, { type: 'spring', stiffness: 400, damping: 30 }));
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
- The API: `animate` (elements, values, or a sequence of steps), `scroll`, `inView`, `hover`, `press`, `stagger`, `spring`. A classic script works too: `script('/js/site.js')` with `const { animate, scroll } = Motion;`.
|
|
851
|
+
- **Always `script('motion')` first.** Never import Motion from a CDN URL, and never rely on the copy the build adds for `data-motion` — it has no `'motion'` import and loads after your scripts.
|
|
852
|
+
- Entrances belong to `data-motion`: it hides content before the first paint, while your own script runs after the page has already shown.
|
|
853
|
+
|
|
854
|
+
**Other libraries** (§8): `gsap` only for what Motion can't do, with one `matchMedia('(prefers-reduced-motion: reduce)')` guard around the whole file; `aos` is legacy — on a page you rework, swap `data-aos*` for the matching `data-motion*` attributes and drop `script('aos')`; never run two motion libraries on one page.
|
|
855
|
+
|
|
856
|
+
**Reduced motion & performance:** `data-motion` handles reduced motion itself; Tailwind motion takes `motion-reduce:` variants; any hand-written animation sits behind a `matchMedia` guard. With reduced motion on, every element must be visible, in place, immediately. A `data-motion` element waits for Motion (about 50 KB) before it appears, so never put an entrance on the hero's largest image — animate the words around it instead.
|
|
857
|
+
|
|
858
|
+
## 16. Critical Rules Checklist
|
|
670
859
|
|
|
671
860
|
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.
|
|
672
861
|
2. Models first: a content field that isn't in the `.model` won't sync. Update the model, then the content.
|
|
@@ -684,3 +873,4 @@ No config element → all defaults. One per site: a second one is ignored with a
|
|
|
684
873
|
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).
|
|
685
874
|
15. Site-wide setup — default page title, Tailwind on/off, sitemap on/off, cookie consent on/off, analytics tag ids — lives in `setup.json` (§4). Change values there rather than hard-coding a site title into the layout, and never delete the file. Sitemap page exclusions are UI-only.
|
|
686
875
|
16. Cookie consent (§14) is a `setup.json` switch plus `data-sleekcms-consent` attributes — tag third-party scripts through `script({ …, 'data-sleekcms-consent': 'category' })`, never a raw tag, and add a footer "Cookie settings" link. Only when the user asks for a banner.
|
|
876
|
+
17. Motion (§15): `data-motion` attributes are built in (no `script()` call) — reveals, stagger, split text, counters, parallax, scroll effects, marquees; `script('motion')` only for your own Motion code; never two motion libraries on a page; reduced motion is handled for you.
|