@homepages/template-kit 0.3.0 → 0.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/CHANGELOG.md +71 -0
- package/README.md +39 -4
- package/dist/asset-modules.d.ts +81 -0
- package/dist/cli/check/config.js +5 -1
- package/dist/cli/check/css.js +27 -13
- package/dist/cli/check/loader.js +4 -0
- package/dist/cli/check/section-assets.js +58 -0
- package/dist/cli/check/stages/size.js +124 -8
- package/dist/cli/check/stages/tree.js +26 -27
- package/dist/cli/dev/build-css.js +91 -0
- package/dist/cli/dev/catalog.js +35 -0
- package/dist/cli/dev/compose-template.js +38 -0
- package/dist/cli/dev/content-override.js +58 -0
- package/dist/cli/dev/discover.js +35 -0
- package/dist/cli/dev/fill-state.js +75 -0
- package/dist/cli/dev/index.js +32 -0
- package/dist/cli/dev/inspect.js +38 -0
- package/dist/cli/dev/island-bootstrap.js +38 -0
- package/dist/cli/dev/island-map.js +35 -0
- package/dist/cli/dev/island-transform.js +31 -0
- package/dist/cli/dev/manifest-instances.js +31 -0
- package/dist/cli/dev/render-section.js +24 -0
- package/dist/cli/dev/screenshot-target.js +24 -0
- package/dist/cli/dev/section-page.js +54 -0
- package/dist/cli/dev/server.js +480 -0
- package/dist/cli/dev/slot-schema.js +12 -0
- package/dist/cli/dev/structure-summary.js +118 -0
- package/dist/cli/dev/tailwind.js +32 -0
- package/dist/cli/dev/vite-server.js +33 -0
- package/dist/cli/dev/workspace.js +63 -0
- package/dist/cli/link/index.js +74 -0
- package/dist/cli/link/overlay.js +59 -0
- package/dist/cli/link/watch.js +25 -0
- package/dist/cli/new/emit.js +48 -0
- package/dist/cli/new/index.js +69 -0
- package/dist/cli/new/scaffold/section/_Renderer.tsx +20 -0
- package/dist/cli/new/scaffold/section/_fill-spec.ts +18 -0
- package/dist/cli/new/scaffold/section/_fixtures.ts +13 -0
- package/dist/cli/new/scaffold/section/_schema.ts +24 -0
- package/dist/cli/new/scaffold/template/_manifest.json +9 -0
- package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +45 -0
- package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +40 -0
- package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +14 -0
- package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +29 -0
- package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +41 -0
- package/dist/cli/new/scaffold/template/sections/hero/schema.ts +60 -0
- package/dist/cli/new/scaffold/template/theme.css +24 -0
- package/dist/cli/new/scaffold/template/theme.ts +27 -0
- package/dist/cli/new/scaffold-assets.js +20 -0
- package/dist/cli/pack/collect.js +44 -0
- package/dist/cli/pack/guards.js +57 -0
- package/dist/cli/pack/index.js +66 -0
- package/dist/cli/pack/manifest.js +15 -0
- package/dist/cli/pack/zip.js +86 -0
- package/dist/cli.js +80 -14
- package/dist/dev-client/assets/index-DfEfrp6P.css +1 -0
- package/dist/dev-client/assets/index-bpMP0b0W.js +50 -0
- package/dist/dev-client/index.html +13 -0
- package/dist/eslint/is-client-file.d.ts +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/islands/detect.d.ts +16 -0
- package/dist/islands/detect.js +22 -0
- package/dist/islands/discover.d.ts +5 -0
- package/dist/islands/discover.js +19 -0
- package/dist/islands/esbuild-plugin.d.ts +13 -0
- package/dist/islands/esbuild-plugin.js +39 -0
- package/dist/islands/wrap.d.ts +11 -0
- package/dist/islands/wrap.js +48 -0
- package/dist/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js +77 -0
- package/dist/node_modules/magic-string/dist/magic-string.es.js +939 -0
- package/dist/package.js +1 -1
- package/dist/ssr.d.ts +31 -0
- package/dist/ssr.js +46 -0
- package/docs/INDEX.md +9 -1
- package/docs/assets.md +128 -0
- package/docs/check.md +6 -3
- package/docs/dev.md +187 -0
- package/docs/islands.md +5 -2
- package/docs/llms.txt +41 -3
- package/docs/new.md +27 -0
- package/docs/overview.md +57 -0
- package/docs/pack.md +46 -0
- package/docs/primitives.md +5 -6
- package/docs/quickstart.md +102 -0
- package/docs/recipes/INDEX.md +27 -0
- package/docs/recipes/bind-property-fact.md +58 -0
- package/docs/recipes/collection-slot.md +100 -0
- package/docs/recipes/fill-spec-decision.md +66 -0
- package/docs/recipes/fixture-states.md +68 -0
- package/docs/recipes/image-slot-crop.md +98 -0
- package/docs/recipes/interactive-island.md +100 -0
- package/docs/recipes/organize-section-folder.md +72 -0
- package/docs/recipes/prepare-submission.md +55 -0
- package/docs/recipes/second-template.md +49 -0
- package/docs/recipes/select-slot.md +59 -0
- package/docs/recipes/static-asset.md +103 -0
- package/docs/recipes/third-party-package.md +90 -0
- package/docs/rules/INDEX.md +4 -2
- package/docs/rules/bundle-binary-asset.md +33 -52
- package/docs/rules/bundle-incomplete.md +2 -2
- package/docs/rules/css-reason.md +3 -3
- package/docs/rules/no-bare-css-import.md +8 -8
- package/docs/rules/no-css-import-from-render-path.md +7 -8
- package/docs/rules/size-assets.md +88 -0
- package/docs/rules/size-island-bundle.md +123 -0
- package/docs/rules/size-section-css.md +19 -14
- package/docs/schema-system.md +28 -28
- package/docs/theme-and-css.md +2 -2
- package/docs/vocabulary.md +98 -0
- package/package.json +11 -3
- package/tsconfig.json +1 -1
|
@@ -1,44 +1,43 @@
|
|
|
1
1
|
---
|
|
2
2
|
purpose: The template-kit/bundle-binary-asset authoring rule — what it enforces, why, and how to fix it.
|
|
3
3
|
status: living
|
|
4
|
-
related: [INDEX.md, bundle-incomplete.md, ../primitives.md]
|
|
5
|
-
updated: 2026-07-
|
|
4
|
+
related: [INDEX.md, bundle-incomplete.md, ../primitives.md, ../assets.md]
|
|
5
|
+
updated: 2026-07-16
|
|
6
6
|
---
|
|
7
7
|
# template-kit/bundle-binary-asset
|
|
8
8
|
|
|
9
|
-
> A
|
|
10
|
-
>
|
|
9
|
+
> A static asset may live in a section folder — but only if something references it. An
|
|
10
|
+
> unreferenced binary is dead weight and can never be served.
|
|
11
11
|
|
|
12
12
|
## Rule
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
A binary asset inside a section folder is allowed **only when the section references it** —
|
|
15
|
+
imported in `Renderer.tsx` (`import crest from "./assets/crest.svg"`) or `url()`-referenced
|
|
16
|
+
from one of the section's CSS files. A binary that nothing references is flagged. The check
|
|
17
|
+
looks two ways to find candidates:
|
|
16
18
|
|
|
17
19
|
- **By extension** — `.svg`, `.png`, `.jpg`/`.jpeg`, `.gif`, `.webp`, `.avif`, `.ico`,
|
|
18
20
|
`.woff`/`.woff2`, `.ttf`, `.otf`, `.eot`, `.mp4`, `.webm`, `.mov`, `.mp3`, `.wav`,
|
|
19
21
|
`.pdf`, `.zip`.
|
|
20
|
-
- **By content** — any other file whose first bytes contain a NUL byte
|
|
21
|
-
asset type the extension list doesn't name.
|
|
22
|
-
|
|
23
|
-
This is scoped to the section folder itself (`Renderer.tsx`, `schema.ts`, `components/`,
|
|
24
|
-
everything under it) — not to the photos a listing supplies. Those arrive at runtime
|
|
25
|
-
through a slot's `src`, never as a file that ships inside your section.
|
|
22
|
+
- **By content** — any other file whose first bytes contain a NUL byte.
|
|
26
23
|
|
|
27
24
|
## Reason
|
|
28
25
|
|
|
29
|
-
A
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Everything a section needs to draw — an icon, a divider graphic, a background pattern —
|
|
35
|
-
is expressible as markup instead: an inline `<svg>`, or a shape built from CSS. That
|
|
36
|
-
markup is text, ships with the rest of your JSX, and needs no pipeline at all.
|
|
26
|
+
A referenced asset is first-class: the build content-hashes it to a stable
|
|
27
|
+
`/section-assets/` URL and the publish pipeline uploads it — identical in the `dev`
|
|
28
|
+
preview, the editor, and the published page. An *unreferenced* binary has no such URL: it
|
|
29
|
+
is never emitted or uploaded, yet it still counts toward the section's content hash (its
|
|
30
|
+
identity). So it is pure dead weight — a file that ships nowhere.
|
|
37
31
|
|
|
38
32
|
## Fix
|
|
39
33
|
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
Either reference the asset, or delete it.
|
|
35
|
+
|
|
36
|
+
- **Reference it from a component:** `import crest from "./assets/crest.svg"` and render it
|
|
37
|
+
with the `Image` primitive — `<Image src={crest} alt="…" />`. See [Static assets](../assets.md).
|
|
38
|
+
- **Reference it from CSS:** `background-image: url("./assets/crest.svg")` (or a
|
|
39
|
+
`@font-face` `src`) in the section's `styles.css`.
|
|
40
|
+
- **Delete it** if it is left over and unused.
|
|
42
41
|
|
|
43
42
|
### Before
|
|
44
43
|
|
|
@@ -49,32 +48,25 @@ sections/hero/
|
|
|
49
48
|
fill-spec.ts
|
|
50
49
|
fixtures.ts
|
|
51
50
|
assets/
|
|
52
|
-
|
|
51
|
+
crest.svg ← nothing imports or url()-references this file
|
|
53
52
|
```
|
|
54
53
|
|
|
54
|
+
### After
|
|
55
|
+
|
|
55
56
|
```tsx
|
|
56
57
|
// sections/hero/Renderer.tsx
|
|
57
|
-
import
|
|
58
|
+
import { Image } from "@homepages/template-kit";
|
|
59
|
+
import crest from "./assets/crest.svg";
|
|
58
60
|
|
|
59
|
-
export function Renderer() {
|
|
60
|
-
return <
|
|
61
|
+
export default function Renderer() {
|
|
62
|
+
return <Image src={crest} alt="" />;
|
|
61
63
|
}
|
|
62
64
|
```
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
```text
|
|
67
|
-
sections/hero/
|
|
68
|
-
Renderer.tsx
|
|
69
|
-
schema.ts
|
|
70
|
-
fill-spec.ts
|
|
71
|
-
fixtures.ts
|
|
72
|
-
components/
|
|
73
|
-
ScrollArrow.tsx ← markup, not a binary file
|
|
74
|
-
```
|
|
66
|
+
For a tiny glyph or divider, prefer inline `<svg>` markup over a file at all — it ships with
|
|
67
|
+
your JSX, needs no asset at all, and costs no extra request:
|
|
75
68
|
|
|
76
69
|
```tsx
|
|
77
|
-
// sections/hero/components/ScrollArrow.tsx
|
|
78
70
|
export function ScrollArrow() {
|
|
79
71
|
return (
|
|
80
72
|
<svg viewBox="0 0 16 16" className="h-4 w-4" fill="none" aria-hidden="true">
|
|
@@ -84,19 +76,8 @@ export function ScrollArrow() {
|
|
|
84
76
|
}
|
|
85
77
|
```
|
|
86
78
|
|
|
87
|
-
```tsx
|
|
88
|
-
// sections/hero/Renderer.tsx
|
|
89
|
-
import { ScrollArrow } from "./components/ScrollArrow.js";
|
|
90
|
-
|
|
91
|
-
export function Renderer() {
|
|
92
|
-
return <ScrollArrow />;
|
|
93
|
-
}
|
|
94
|
-
```
|
|
95
|
-
|
|
96
79
|
## See also
|
|
97
80
|
|
|
98
|
-
- [
|
|
99
|
-
|
|
100
|
-
- [Contract primitives](../primitives.md) — `Image`,
|
|
101
|
-
a listing supplies. It reads a `src` at runtime; it is not a way to ship a binary file
|
|
102
|
-
inside your section.
|
|
81
|
+
- [Static assets](../assets.md) — where an asset may live and how to reference it.
|
|
82
|
+
- [`bundle-incomplete`](bundle-incomplete.md) — the other rule on the folder's contents.
|
|
83
|
+
- [Contract primitives](../primitives.md) — `Image`, which renders an asset URL you import.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: The template-kit/bundle-incomplete authoring rule — what it enforces, why, and how to fix it.
|
|
3
3
|
status: living
|
|
4
4
|
related: [INDEX.md, ../schema-system.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-15
|
|
6
6
|
---
|
|
7
7
|
# template-kit/bundle-incomplete
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ Two near-misses do **not** satisfy it:
|
|
|
30
30
|
searched for down the tree.
|
|
31
31
|
|
|
32
32
|
Everything else in the folder is unconstrained. `components/`, `hooks/`, `utils/`, any
|
|
33
|
-
nesting, any number of support modules, islands, stylesheets
|
|
33
|
+
nesting, any number of support modules, islands, stylesheets: the check has no
|
|
34
34
|
opinion about them, and neither does the rest of the kit.
|
|
35
35
|
|
|
36
36
|
## Reason
|
package/docs/rules/css-reason.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: The template-kit/css-reason authoring rule — what it enforces, why, and how to fix it.
|
|
3
3
|
status: living
|
|
4
4
|
related: [INDEX.md, ../theme-and-css.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-15
|
|
6
6
|
---
|
|
7
7
|
# template-kit/css-reason
|
|
8
8
|
|
|
@@ -36,7 +36,7 @@ and cannot leak into another section on the same page. Hand-written CSS gives al
|
|
|
36
36
|
up.
|
|
37
37
|
|
|
38
38
|
There is exactly one thing utilities cannot do: style **DOM that Tailwind's scanner never
|
|
39
|
-
sees**. Markup an
|
|
39
|
+
sees**. Markup an island builds at runtime has no class in your source for the scanner
|
|
40
40
|
to find, and a third-party widget's own DOM was never yours to put classes on. Those are
|
|
41
41
|
the cases hand-CSS exists for.
|
|
42
42
|
|
|
@@ -67,7 +67,7 @@ classes on the markup.
|
|
|
67
67
|
### After
|
|
68
68
|
|
|
69
69
|
```css
|
|
70
|
-
/* css-reason: styles the marker DOM the neighborhood-map
|
|
70
|
+
/* css-reason: styles the marker DOM the neighborhood-map island builds at runtime —
|
|
71
71
|
the nodes are created by the map library after hydration, so Tailwind's scanner never
|
|
72
72
|
sees a class to compile. */
|
|
73
73
|
.tr-map-marker {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: The template-kit/no-bare-css-import authoring rule — what it enforces, why, and how to fix it.
|
|
3
3
|
status: living
|
|
4
4
|
related: [INDEX.md, ../theme-and-css.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-15
|
|
6
6
|
---
|
|
7
7
|
# template-kit/no-bare-css-import
|
|
8
8
|
|
|
@@ -59,11 +59,11 @@ in the whole kit: the only signal is that the styles are not there.
|
|
|
59
59
|
Delete the `@import`.
|
|
60
60
|
|
|
61
61
|
**A package's stylesheet is reached from the code that needs it**, not from CSS. Write
|
|
62
|
-
`import "swiper/css";` in the component
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
`import "swiper/css";` in the component or island that renders the thing — the CSS build
|
|
63
|
+
bundles your `Renderer.tsx` and everything it imports, transitively, precisely to harvest
|
|
64
|
+
those imports, and it compiles what they pull in into **this section's cascade layer**.
|
|
65
|
+
Package CSS goes in first and your own hand-CSS second, so at equal specificity your rules
|
|
66
|
+
win.
|
|
67
67
|
|
|
68
68
|
Two things authors get wrong immediately after:
|
|
69
69
|
|
|
@@ -79,7 +79,7 @@ Two things authors get wrong immediately after:
|
|
|
79
79
|
|
|
80
80
|
```css
|
|
81
81
|
/* sections/gallery/styles.css */
|
|
82
|
-
/* css-reason: styles the slide DOM the Swiper
|
|
82
|
+
/* css-reason: styles the slide DOM the Swiper carousel builds at runtime. */
|
|
83
83
|
@import "swiper/css"; /* template-kit/no-bare-css-import — silently dropped */
|
|
84
84
|
|
|
85
85
|
.tr-gallery .swiper-pagination-bullet-active {
|
|
@@ -95,7 +95,7 @@ The `@import` is gone from the stylesheet:
|
|
|
95
95
|
|
|
96
96
|
```css
|
|
97
97
|
/* sections/gallery/styles.css */
|
|
98
|
-
/* css-reason: overrides the Swiper pagination DOM the
|
|
98
|
+
/* css-reason: overrides the Swiper pagination DOM the carousel builds at runtime —
|
|
99
99
|
those nodes carry the library's own classes, which Tailwind's scanner never sees. */
|
|
100
100
|
.tr-gallery .swiper-pagination-bullet-active {
|
|
101
101
|
background: var(--tr-color-primary);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: The template-kit/no-css-import-from-render-path authoring rule — what it enforces, why, and how to fix it.
|
|
3
3
|
status: living
|
|
4
4
|
related: [INDEX.md, ../theme-and-css.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-15
|
|
6
6
|
---
|
|
7
7
|
# template-kit/no-css-import-from-render-path
|
|
8
8
|
|
|
@@ -31,8 +31,8 @@ server-rendered inline before it is hydrated.
|
|
|
31
31
|
## Reason
|
|
32
32
|
|
|
33
33
|
The section's CSS build works by **bundling the render path itself** — `Renderer.tsx` and
|
|
34
|
-
|
|
35
|
-
pull in. A bare specifier resolves into `node_modules`, which is exactly what that build
|
|
34
|
+
every module it imports, components and islands alike — and harvesting the CSS those
|
|
35
|
+
imports pull in. A bare specifier resolves into `node_modules`, which is exactly what that build
|
|
36
36
|
looks for: `import "swiper/css";` in `Renderer.tsx` is not a mistake, it is the mechanism.
|
|
37
37
|
|
|
38
38
|
A **local** stylesheet is a different problem. Every `.css` file that already lives in the
|
|
@@ -47,11 +47,10 @@ Delete the import. A local stylesheet already ships — the build picks up every
|
|
|
47
47
|
in the section folder on its own; importing it again only makes it double-emit.
|
|
48
48
|
|
|
49
49
|
A package's stylesheet is different: import it **by its bare specifier**, from the module
|
|
50
|
-
that needs it. The build bundles `Renderer.tsx`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
layer.
|
|
50
|
+
that needs it. The build bundles `Renderer.tsx`'s import graph **transitively**, so a bare
|
|
51
|
+
specifier is equally legal in the Renderer itself or in any component or island it
|
|
52
|
+
imports — not only at the top of the tree. That import is how the build finds the
|
|
53
|
+
stylesheet and routes it into the section's cascade layer.
|
|
55
54
|
|
|
56
55
|
### Before
|
|
57
56
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/size-assets authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, size-section-css.md, ../theme-and-css.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
|
+
---
|
|
7
|
+
# template-kit/size-assets
|
|
8
|
+
|
|
9
|
+
> A section's static assets — the svg, images, and fonts it imports or `url()`-references —
|
|
10
|
+
> must total **≤ 250 KB raw.** Raw, not gzip: images and fonts are already compressed
|
|
11
|
+
> formats, so gzipping them again buys nothing.
|
|
12
|
+
|
|
13
|
+
## Rule
|
|
14
|
+
|
|
15
|
+
A section's static assets are budgeted by **total raw bytes, 250 KB or less**. What is
|
|
16
|
+
measured is every asset the published section actually ships:
|
|
17
|
+
|
|
18
|
+
- an svg, image, or font `import`ed directly in `Renderer.tsx` (or an island), and
|
|
19
|
+
- an svg, image, or font `url()`-referenced from the section's own CSS or from a
|
|
20
|
+
package stylesheet its module graph pulls in — including a webfont `@font-face`s.
|
|
21
|
+
|
|
22
|
+
The same asset referenced twice (from both a stylesheet and a component, say) counts
|
|
23
|
+
once, not twice.
|
|
24
|
+
|
|
25
|
+
The budget is **visible in `template-kit check`'s configuration and is not
|
|
26
|
+
author-overridable**. A workspace cannot raise it, and there is no per-section exemption.
|
|
27
|
+
|
|
28
|
+
## Reason
|
|
29
|
+
|
|
30
|
+
An svg icon or a product photo a section imports, and a webfont a package's stylesheet
|
|
31
|
+
pulls in, both ship to the same page load as the section's own code and CSS — they are
|
|
32
|
+
not free just because they never appear in a byte-budget an author is used to watching.
|
|
33
|
+
|
|
34
|
+
Fonts are the sharpest case. A `url()`-referenced webfont used to inline as part of the
|
|
35
|
+
compiled stylesheet, which made a large font look like a CSS problem: trimming your own
|
|
36
|
+
rules never fixed it, because the bytes were never your CSS to begin with. This rule
|
|
37
|
+
separates that weight into its own budget — a font, an icon, or an image is measured as
|
|
38
|
+
what it is, and the stylesheet budget ([`size-section-css`](size-section-css.md)) is left
|
|
39
|
+
to measure only what it actually contains.
|
|
40
|
+
|
|
41
|
+
## Fix
|
|
42
|
+
|
|
43
|
+
Two moves, in order of how often they work:
|
|
44
|
+
|
|
45
|
+
1. **Shrink or subset the asset.** A full variable font family, shipped for three
|
|
46
|
+
weights actually used, is the usual cause — subset it to the characters and weights
|
|
47
|
+
the section renders. An unoptimized PNG is the other common case — recompress it, or
|
|
48
|
+
switch to a format built for the content (`webp`/`avif` for photos, `svg` for icons).
|
|
49
|
+
2. **Drop assets the section does not use.** A copied-in icon set or a leftover preview
|
|
50
|
+
image that never renders still ships its bytes — delete what the section does not
|
|
51
|
+
reference.
|
|
52
|
+
|
|
53
|
+
### Before
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// sections/hero/Renderer.tsx
|
|
57
|
+
import "brandfont/css"; // ← ships the package's full variable font family
|
|
58
|
+
|
|
59
|
+
export default function Renderer() {
|
|
60
|
+
return <h1 style={{ fontFamily: "BrandFont" }}>Welcome home</h1>;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### After
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// sections/hero/Renderer.tsx
|
|
68
|
+
import heroIcon from "./assets/hero-icon.svg"; // ← a small, single-purpose asset
|
|
69
|
+
|
|
70
|
+
export default function Renderer() {
|
|
71
|
+
// Uses a font the template's theme already declares (`--tr-font-<name>`),
|
|
72
|
+
// which costs nothing because it is already loaded — no package font import.
|
|
73
|
+
return (
|
|
74
|
+
<h1 className="tr-font-display">
|
|
75
|
+
<img src={heroIcon} alt="" /> Welcome home
|
|
76
|
+
</h1>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## See also
|
|
82
|
+
|
|
83
|
+
- [`size-section-css`](size-section-css.md) — the stylesheet budget this rule was split
|
|
84
|
+
out of; a `url()`-referenced font now counts here, not there.
|
|
85
|
+
- [`size-renderer-bundle`](size-renderer-bundle.md) — the same idea applied to the
|
|
86
|
+
section's JavaScript.
|
|
87
|
+
- [Theme and CSS](../theme-and-css.md) — the fonts and tokens the page already loads,
|
|
88
|
+
which cost you nothing to reuse.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/size-island-bundle authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, size-renderer-bundle.md, size-section-css.md, server-vs-client.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
|
+
---
|
|
7
|
+
# template-kit/size-island-bundle
|
|
8
|
+
|
|
9
|
+
> Each island's browser bundle must be **≤ 50 KB gzip**. The budget is not
|
|
10
|
+
> author-overridable, and it applies per island, not per section.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
An island — a component whose file opens with the `"use client"` directive (see
|
|
15
|
+
[Server vs client](server-vs-client.md)) — ships to the browser as its own bundle,
|
|
16
|
+
separate from the section's server-rendered HTML. Each island is bundled with esbuild
|
|
17
|
+
and minified, with **React treated as external** (the page's runtime chunk supplies it
|
|
18
|
+
once, shared by every island on the page, and is not counted against any one of them).
|
|
19
|
+
The gzipped result must be **50 KB or less**.
|
|
20
|
+
|
|
21
|
+
A section can have more than one island. The budget is checked **per island**: a section
|
|
22
|
+
with three islands each at 40 KB passes; a section with one island at 60 KB fails, even
|
|
23
|
+
though the same section's other files are small.
|
|
24
|
+
|
|
25
|
+
The budget is **visible in `template-kit check`'s configuration and is not
|
|
26
|
+
author-overridable**. A template workspace cannot raise it, and there is no per-island
|
|
27
|
+
exemption.
|
|
28
|
+
|
|
29
|
+
## Reason
|
|
30
|
+
|
|
31
|
+
An island is the part of the page that has to work on a phone before the visitor does
|
|
32
|
+
anything — a carousel, a filter, a form. Every one of its bytes is downloaded, parsed,
|
|
33
|
+
and executed on the client, on top of whatever the page's shared runtime already costs.
|
|
34
|
+
Unlike the section's server-rendered markup, which the browser only has to paint, an
|
|
35
|
+
island's code has to run.
|
|
36
|
+
|
|
37
|
+
The number is set to be generous, so it does not fight ordinary authoring — a real
|
|
38
|
+
interactive widget fits comfortably inside it. You hit the budget when something
|
|
39
|
+
**runaway** gets pulled into the client bundle:
|
|
40
|
+
|
|
41
|
+
- **a whole UI or icon library imported for one component**, which drags every export
|
|
42
|
+
into the bundle so the island can use a handful of them;
|
|
43
|
+
- **a heavy dependency that belongs on the server**, ported into the island because it
|
|
44
|
+
was easiest to reach for where the code was being written, not because the browser
|
|
45
|
+
needs it.
|
|
46
|
+
|
|
47
|
+
That is the shape of essentially every violation: not an island that grew, but a single
|
|
48
|
+
import that was more expensive than it looked.
|
|
49
|
+
|
|
50
|
+
## Fix
|
|
51
|
+
|
|
52
|
+
Find the import that is not paying for itself, or narrow what ships to the browser at
|
|
53
|
+
all.
|
|
54
|
+
|
|
55
|
+
- **Import named symbols, never a namespace.** `import { ArrowRight } from "…"` is
|
|
56
|
+
tree-shakeable; `import * as Icons from "…"` is not — a namespace object has to contain
|
|
57
|
+
everything, so nothing can be dropped.
|
|
58
|
+
- **Prefer the package's narrow entry** if it offers one, instead of its default
|
|
59
|
+
"everything" bundle.
|
|
60
|
+
- **Move what doesn't need to be interactive out of the island.** If only a small part of
|
|
61
|
+
a component actually needs client-side behavior — an expand/collapse toggle inside an
|
|
62
|
+
otherwise static block — split the interactive part into its own small island and leave
|
|
63
|
+
the rest server-rendered. Server-rendered markup costs nothing against this budget.
|
|
64
|
+
- **Hand-write the residue.** If you use one function out of a large utility package, copy
|
|
65
|
+
the function instead of importing the package for it.
|
|
66
|
+
|
|
67
|
+
### Before
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
// sections/gallery/Carousel.tsx
|
|
71
|
+
"use client";
|
|
72
|
+
|
|
73
|
+
import * as Icons from "lucide-react"; // ← the whole set: every icon, in this island's bundle
|
|
74
|
+
import { useState } from "react";
|
|
75
|
+
|
|
76
|
+
export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
|
|
77
|
+
const [index, setIndex] = useState(0);
|
|
78
|
+
return (
|
|
79
|
+
<div>
|
|
80
|
+
<img src={slides[index]?.url} alt="" />
|
|
81
|
+
<button onClick={() => setIndex((i) => (i - 1 + slides.length) % slides.length)}>
|
|
82
|
+
<Icons.ChevronLeft aria-hidden />
|
|
83
|
+
</button>
|
|
84
|
+
<button onClick={() => setIndex((i) => (i + 1) % slides.length)}>
|
|
85
|
+
<Icons.ChevronRight aria-hidden />
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### After
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// sections/gallery/Carousel.tsx
|
|
96
|
+
"use client";
|
|
97
|
+
|
|
98
|
+
import { ChevronLeft, ChevronRight } from "lucide-react"; // ← two icons, and the bundler can drop the rest
|
|
99
|
+
import { useState } from "react";
|
|
100
|
+
|
|
101
|
+
export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
|
|
102
|
+
const [index, setIndex] = useState(0);
|
|
103
|
+
return (
|
|
104
|
+
<div>
|
|
105
|
+
<img src={slides[index]?.url} alt="" />
|
|
106
|
+
<button onClick={() => setIndex((i) => (i - 1 + slides.length) % slides.length)}>
|
|
107
|
+
<ChevronLeft aria-hidden />
|
|
108
|
+
</button>
|
|
109
|
+
<button onClick={() => setIndex((i) => (i + 1) % slides.length)}>
|
|
110
|
+
<ChevronRight aria-hidden />
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## See also
|
|
118
|
+
|
|
119
|
+
- [`size-renderer-bundle`](size-renderer-bundle.md) — the same budget on the section's
|
|
120
|
+
server-rendered bundle.
|
|
121
|
+
- [`size-section-css`](size-section-css.md) — the same budget on the section's stylesheet.
|
|
122
|
+
- [Server vs client](server-vs-client.md) — what makes a file an island in the first
|
|
123
|
+
place.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
purpose: The template-kit/size-section-css authoring rule — what it enforces, why, and how to fix it.
|
|
3
3
|
status: living
|
|
4
|
-
related: [INDEX.md, size-renderer-bundle.md, ../theme-and-css.md]
|
|
5
|
-
updated: 2026-07-
|
|
4
|
+
related: [INDEX.md, size-renderer-bundle.md, size-assets.md, ../theme-and-css.md]
|
|
5
|
+
updated: 2026-07-15
|
|
6
6
|
---
|
|
7
7
|
# template-kit/size-section-css
|
|
8
8
|
|
|
@@ -16,7 +16,7 @@ everything that lands in the layer:
|
|
|
16
16
|
|
|
17
17
|
- the section's hand-written CSS, and
|
|
18
18
|
- **every package stylesheet its module graph imports** — the `import "swiper/css"` in a
|
|
19
|
-
component
|
|
19
|
+
component or an island.
|
|
20
20
|
|
|
21
21
|
The budget is **visible in `template-kit check`'s configuration and is not
|
|
22
22
|
author-overridable**. A workspace cannot raise it, and there is no per-section exemption.
|
|
@@ -29,14 +29,15 @@ CSS blocks the first paint.
|
|
|
29
29
|
|
|
30
30
|
"Including package CSS" is not a technicality; it is the failure this rule exists to catch.
|
|
31
31
|
Your own stylesheet is almost never the problem — it is a few kilobytes of overrides. The
|
|
32
|
-
package's is.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
`dist` listing looks alarming. The compiled layer is 400 KB.
|
|
32
|
+
package's is. A one-line `import "some-ui/dist.css"` can drag in thousands of rules for
|
|
33
|
+
components you never render — resets, theme variants, every widget's styles — and they all
|
|
34
|
+
land in your compiled layer. Nothing in your source looks large; the compiled layer is
|
|
35
|
+
400 KB.
|
|
37
36
|
|
|
38
37
|
So the number this check reports is frequently the first time anyone finds out what a
|
|
39
|
-
one-line CSS import actually cost.
|
|
38
|
+
one-line CSS import actually cost. (What this budget does **not** count is asset bytes: a
|
|
39
|
+
`url()`-referenced webfont or icon file resolves to a cached URL and is weighed separately —
|
|
40
|
+
see [`size-assets`](size-assets.md).)
|
|
40
41
|
|
|
41
42
|
## Fix
|
|
42
43
|
|
|
@@ -45,10 +46,11 @@ Three moves, in order of how often they work:
|
|
|
45
46
|
1. **Import the slimmest entry the package offers.** Most CSS-shipping packages have a core
|
|
46
47
|
stylesheet plus opt-in modules, *and* a fat "everything" bundle that is the one you get
|
|
47
48
|
from copying the quickstart. Take the core, plus only the modules you use.
|
|
48
|
-
2. **
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
2. **Drop the icon font.** If the package's stylesheet declares an icon font and you use
|
|
50
|
+
three of its glyphs, replace them with inline SVG — or with a family the template's theme
|
|
51
|
+
already declares in `fonts` (`--tr-font-<name>`), which costs nothing because it is
|
|
52
|
+
already loaded. (The font *file* is weighed under [`size-assets`](size-assets.md), but its
|
|
53
|
+
`@font-face` rules and every `.icon-*` class still land in this layer.)
|
|
52
54
|
3. **Drop the package and hand-write the residue.** If you use one component from a large
|
|
53
55
|
widget library, its stylesheet is paying for components you never render.
|
|
54
56
|
|
|
@@ -83,12 +85,15 @@ export default function Carousel({ slides }: { slides: { id: string; url: string
|
|
|
83
85
|
```
|
|
84
86
|
|
|
85
87
|
The arrows and dots that came from the bundle's icon font are drawn as inline SVG in the
|
|
86
|
-
component instead, so
|
|
88
|
+
component instead, so the layer no longer carries that font's `@font-face` and icon-class
|
|
89
|
+
rules (and the font file no longer counts against [`size-assets`](size-assets.md) either).
|
|
87
90
|
|
|
88
91
|
## See also
|
|
89
92
|
|
|
90
93
|
- [`size-renderer-bundle`](size-renderer-bundle.md) — the same budget on the section's
|
|
91
94
|
JavaScript.
|
|
95
|
+
- [`size-assets`](size-assets.md) — the separate budget on the static asset *files*
|
|
96
|
+
(fonts, images) a stylesheet or component references.
|
|
92
97
|
- [`no-bare-css-import`](no-bare-css-import.md) — why a package's stylesheet is imported from
|
|
93
98
|
code and never `@import`ed from CSS.
|
|
94
99
|
- [Theme and CSS](../theme-and-css.md) — the fonts and tokens the page already loads, which
|
package/docs/schema-system.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
purpose: The section authoring contract — schema.ts, fill-spec.ts, fixtures.ts, and the closed source/transform vocabulary.
|
|
3
3
|
status: living
|
|
4
|
-
related: [primitives.md, theme-and-css.md]
|
|
5
|
-
updated: 2026-07-
|
|
4
|
+
related: [primitives.md, theme-and-css.md, vocabulary.md]
|
|
5
|
+
updated: 2026-07-16
|
|
6
6
|
---
|
|
7
7
|
# The schema system
|
|
8
8
|
|
|
@@ -32,22 +32,17 @@ from. Without it, `SectionProps` cannot tell a required slot from an optional on
|
|
|
32
32
|
and your renderer props collapse to a useless widened shape.
|
|
33
33
|
|
|
34
34
|
```ts
|
|
35
|
-
import { direct, type
|
|
36
|
-
|
|
37
|
-
export const HERO_FRAME = {
|
|
38
|
-
breakpoint: 425,
|
|
39
|
-
desktop: { aspect: 5 / 3 },
|
|
40
|
-
} as const satisfies FrameConfig;
|
|
35
|
+
import { direct, type SectionProps, type SectionSchemaShape } from "@homepages/template-kit";
|
|
41
36
|
|
|
42
37
|
export const schema = {
|
|
43
38
|
meta: { displayName: "Hero" },
|
|
44
39
|
slots: {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
hero_image: { type: "image", required: true, produced_by: "hero_image_assign", crop: { mode: "locked", aspect: 16 / 9 } },
|
|
41
|
+
headline: { type: "text", size: "medium", required: true, source: direct("address") },
|
|
42
|
+
blurb: { type: "text", size: "long", required: true, produced_by: "blurb_text" },
|
|
48
43
|
},
|
|
49
44
|
options: {
|
|
50
|
-
align: { type: "enum", values: ["left", "center"], default: "
|
|
45
|
+
align: { type: "enum", values: ["left", "center"], default: "center" },
|
|
51
46
|
},
|
|
52
47
|
} as const satisfies SectionSchemaShape;
|
|
53
48
|
|
|
@@ -65,14 +60,20 @@ binds it to a computed transform. Both take names from a **closed vocabulary**,
|
|
|
65
60
|
a typo is a compile error at the call site — not a runtime surprise:
|
|
66
61
|
|
|
67
62
|
```ts
|
|
68
|
-
source: direct("
|
|
69
|
-
source: direct("
|
|
63
|
+
source: direct("address") // ✅
|
|
64
|
+
source: direct("addresss") // ✗ compile error: not a SOURCE_FIELDS key
|
|
70
65
|
```
|
|
71
66
|
|
|
72
|
-
The vocabulary is the kit's, not yours.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
The vocabulary is the kit's, not yours. Adding to it is a kit release, not a
|
|
68
|
+
change you can make in a template. See [the closed vocabulary](vocabulary.md)
|
|
69
|
+
for the sets and when to pick each; your editor autocompletes the exact fields.
|
|
70
|
+
|
|
71
|
+
A `select` slot binds the same way as any other type — here fact-bound rather
|
|
72
|
+
than AI-produced, with `values` declaring the closed set the fact resolves to:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
listing_status: { type: "select", required: false, source: direct("listing_intent"), values: ["for_sale", "for_rent"] },
|
|
76
|
+
```
|
|
76
77
|
|
|
77
78
|
### Grouping slots into one editor card
|
|
78
79
|
|
|
@@ -104,7 +105,6 @@ import type { Props } from "./schema.js";
|
|
|
104
105
|
|
|
105
106
|
export default function Hero({ slots, options, nav }: Props) {
|
|
106
107
|
// headline is `string` — the schema says required: true
|
|
107
|
-
// listing_status is `string | null` — required: false
|
|
108
108
|
// options.align is `"left" | "center"` — the enum's declared values
|
|
109
109
|
return (
|
|
110
110
|
<Section id={nav.selfAnchor}>
|
|
@@ -153,12 +153,12 @@ export const fillSpec = {
|
|
|
153
153
|
decisions: [
|
|
154
154
|
{ id: "hero_image_assign", type: "image-assign", produces: ["hero_image"], tag_query: "category:exterior" },
|
|
155
155
|
{
|
|
156
|
-
id: "
|
|
156
|
+
id: "blurb_text",
|
|
157
157
|
type: "text-block",
|
|
158
|
-
produces: ["
|
|
159
|
-
length_cap:
|
|
160
|
-
structure: "
|
|
161
|
-
voice_prompt: "
|
|
158
|
+
produces: ["blurb"],
|
|
159
|
+
length_cap: 400,
|
|
160
|
+
structure: "paragraph",
|
|
161
|
+
voice_prompt: "Warm, concrete paragraph a buyer would want to read.",
|
|
162
162
|
},
|
|
163
163
|
],
|
|
164
164
|
} as const satisfies FillSpecShape;
|
|
@@ -177,14 +177,14 @@ import type { FixtureModule } from "@homepages/template-kit";
|
|
|
177
177
|
const fixtures: FixtureModule = {
|
|
178
178
|
base: {
|
|
179
179
|
slots: {
|
|
180
|
-
headline: "
|
|
181
|
-
listing_status: "for_sale",
|
|
180
|
+
headline: "101 Queensway, Jamaica Plain, MA 02130",
|
|
182
181
|
hero_image: { photo_id: "p1", url: "https://example.com/a.jpg", alt: "Front elevation" },
|
|
182
|
+
blurb: "A bright corner home moments from the Emerald Necklace.",
|
|
183
183
|
},
|
|
184
|
-
options: { align: "
|
|
184
|
+
options: { align: "center" },
|
|
185
185
|
},
|
|
186
186
|
states: {
|
|
187
|
-
|
|
187
|
+
left: { label: "Left-aligned", options: { align: "left" } },
|
|
188
188
|
},
|
|
189
189
|
};
|
|
190
190
|
|