@iterant/site-runtime 3.8.2 → 3.10.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/docs/runtime-contract.md +39 -9
- package/package.json +3 -1
- package/src/config/preset.ts +21 -5
- package/styles/markdown.css +221 -0
package/docs/runtime-contract.md
CHANGED
|
@@ -50,7 +50,7 @@ runtime and says so.
|
|
|
50
50
|
|
|
51
51
|
<!-- generated: available libraries -->
|
|
52
52
|
|
|
53
|
-
_Generated from package.json by scripts/generate-kit-table.mjs. Runtime 3.
|
|
53
|
+
_Generated from package.json by scripts/generate-kit-table.mjs. Runtime 3.10.0._
|
|
54
54
|
|
|
55
55
|
**Toolchain** (this package owns the version; do NOT declare these):
|
|
56
56
|
|
|
@@ -157,7 +157,7 @@ CommonMark, stored as written.
|
|
|
157
157
|
|
|
158
158
|
const body = readMarkdown(data.body); // raw CommonMark | null
|
|
159
159
|
<div
|
|
160
|
-
class="
|
|
160
|
+
class="markdown-body"
|
|
161
161
|
data-editable="body"
|
|
162
162
|
data-path="props.body"
|
|
163
163
|
data-edit-type="markdown"
|
|
@@ -165,6 +165,22 @@ CommonMark, stored as written.
|
|
|
165
165
|
/>;
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
+
- **`markdown-body` is the wrapper class, and the stylesheet is opt-in
|
|
169
|
+
(3.10.0).** The compiler emits bare `<h2>`, `<ul>` and `<blockquote>`, and
|
|
170
|
+
Preflight flattens all three to paragraph size, so an unstyled body renders as
|
|
171
|
+
a run of identical lines. Add the import to `src/styles/globals.css` once,
|
|
172
|
+
below `@import "tailwindcss"`:
|
|
173
|
+
|
|
174
|
+
```css
|
|
175
|
+
@import "@iterant/site-runtime/styles/markdown.css";
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
It styles measure, rhythm and hierarchy from the repo's own `--it-*` and
|
|
179
|
+
`--font-*` tokens, and reads two knobs of its own:
|
|
180
|
+
`--it-markdown-measure` (the reading width, `68ch`, `none` to fill) and
|
|
181
|
+
`--it-markdown-flow` (the space between blocks, `1.25em`). Every rule sits in
|
|
182
|
+
the `base` layer, so a utility class on the wrapper still wins.
|
|
183
|
+
|
|
168
184
|
- **Headings start at `##`.** The body renders below the page's own h1; the
|
|
169
185
|
compiler does not demote levels for you.
|
|
170
186
|
- Hrefs inside the value follow the same rules as link wrappers: root-relative
|
|
@@ -617,13 +633,27 @@ The layout preloads the upright latin face of each named family and nothing
|
|
|
617
633
|
else. Every face stays in the `@font-face` block, so the browser still fetches a
|
|
618
634
|
slanted or extended-latin one the moment a glyph needs it.
|
|
619
635
|
|
|
620
|
-
**`src/site-config.ts` is watched.** Astro restarts its dev server
|
|
621
|
-
config file, `package.json` and the tsconfig sources, and on nothing
|
|
622
|
-
family added to `SITE_CONFIG.fonts` would be rendered by the layout
|
|
623
|
-
had never resolved it. The preset adds the file to
|
|
624
|
-
(`site-config-watch`), which makes the change restart the
|
|
625
|
-
font declarations. A repo that keeps its site config
|
|
626
|
-
`iterantStarter({ siteConfigPath })`.
|
|
636
|
+
**`src/site-config.ts` is watched in list mode.** Astro restarts its dev server
|
|
637
|
+
on its own config file, `package.json` and the tsconfig sources, and on nothing
|
|
638
|
+
else, so a family added to `SITE_CONFIG.fonts` would be rendered by the layout
|
|
639
|
+
while Astro had never resolved it. The preset adds the file to
|
|
640
|
+
`settings.watchFiles` (`site-config-watch`), which makes the change restart the
|
|
641
|
+
server and reach the font declarations. A repo that keeps its site config
|
|
642
|
+
elsewhere passes `iterantStarter({ siteConfigPath })`.
|
|
643
|
+
|
|
644
|
+
**Under `fonts: "catalog"` the preset registers no restart watch on it; Vite
|
|
645
|
+
still watches it as a source module.** Every
|
|
646
|
+
family is declared at boot, so a family written into `SITE_CONFIG.fonts` after
|
|
647
|
+
that is one Astro already resolved, and the layout picks it up on the first
|
|
648
|
+
render after the module's hot update has been processed. The site config is then outside the Astro
|
|
649
|
+
config's import graph too, since the config no longer reads it, so no write of
|
|
650
|
+
it restarts the server. That is the mode the platform's own starters run: the
|
|
651
|
+
direction pick writes the file on every first build, and the restart it used to
|
|
652
|
+
cause cost 11.7s of preview outage, ending in a re-attach the customer saw as
|
|
653
|
+
the waking card (measured 2026-09-12). The boot pays 6.3s of font resolution
|
|
654
|
+
instead of 1.2s on a cold tree, and nothing on a tree whose `.astro/fonts` cache
|
|
655
|
+
came with it and whose metadata is under seven days old (Astro refreshes the
|
|
656
|
+
metadata after that, through the cache's own files).
|
|
627
657
|
|
|
628
658
|
### The browser floor
|
|
629
659
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iterant/site-runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The platform layer every Iterant brand site runs on: content grammar, collection schemas, SEO head and JSON-LD, layout core, Astro config preset, dev integrations and the verify gates.",
|
|
6
6
|
"scripts": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"docs",
|
|
28
28
|
"scripts",
|
|
29
29
|
"src",
|
|
30
|
+
"styles",
|
|
30
31
|
"!**/*.test.ts",
|
|
31
32
|
"!scripts/check-fixture.mjs",
|
|
32
33
|
"!scripts/check-packed.mjs",
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"./routes": "./src/routes/index.ts",
|
|
60
61
|
"./config": "./src/config/preset.ts",
|
|
61
62
|
"./integrations/*": "./src/integrations/*.mjs",
|
|
63
|
+
"./styles/markdown.css": "./styles/markdown.css",
|
|
62
64
|
"./package.json": "./package.json"
|
|
63
65
|
},
|
|
64
66
|
"iterant": {
|
package/src/config/preset.ts
CHANGED
|
@@ -89,6 +89,11 @@ const FONT_SUBSETS: NonEmpty<string> = ["latin", "latin-ext"];
|
|
|
89
89
|
* render a face Astro never resolved. `site-config-watch` makes that restart
|
|
90
90
|
* happen, and these eleven make it unnecessary for the common pick.
|
|
91
91
|
*
|
|
92
|
+
* `"catalog"` takes the other road: with every family declared, nothing the
|
|
93
|
+
* platform writes into SITE_CONFIG.fonts can name a face Astro did not resolve,
|
|
94
|
+
* so the site config leaves the dev server's restart list entirely and a
|
|
95
|
+
* direction pick reaches the page as a hot update instead of a reboot.
|
|
96
|
+
*
|
|
92
97
|
* A name the catalog does not carry is skipped rather than declared: Astro's
|
|
93
98
|
* `<Font>` throws on a CSS variable no family registered, and the layout reads
|
|
94
99
|
* the same catalog to decide what to render.
|
|
@@ -163,9 +168,12 @@ export interface IterantStarterOptions {
|
|
|
163
168
|
*/
|
|
164
169
|
fonts?: readonly string[] | "catalog";
|
|
165
170
|
/**
|
|
166
|
-
* The repo's site config module, relative to the project root.
|
|
167
|
-
* change to `SITE_CONFIG.fonts` restarts the dev server
|
|
168
|
-
* declarations; the preset does not import the file, it
|
|
171
|
+
* The repo's site config module, relative to the project root. In list mode
|
|
172
|
+
* it is watched, so a change to `SITE_CONFIG.fonts` restarts the dev server
|
|
173
|
+
* and reaches the font declarations; the preset does not import the file, it
|
|
174
|
+
* only names it. Under `fonts: "catalog"` the preset registers no restart
|
|
175
|
+
* watch on it (Vite still watches it as a module), because every
|
|
176
|
+
* family is already declared.
|
|
169
177
|
*/
|
|
170
178
|
siteConfigPath?: string;
|
|
171
179
|
/**
|
|
@@ -226,8 +234,16 @@ export function iterantStarter({
|
|
|
226
234
|
newFileReload(),
|
|
227
235
|
// Dev-only: restart signals, the content re-arm, and the dev-state probe.
|
|
228
236
|
devServerSignals({ pagesDir, chromeDir }),
|
|
229
|
-
// Restart the dev server when the site config the fonts come from changes
|
|
230
|
-
|
|
237
|
+
// Restart the dev server when the site config the fonts come from changes,
|
|
238
|
+
// in list mode alone. The watch exists for one reason: the preset turns
|
|
239
|
+
// SITE_CONFIG.fonts into font declarations once, at server start, so a
|
|
240
|
+
// family added to that list after boot would render a face Astro never
|
|
241
|
+
// resolved. `"catalog"` declares every family up front, which answers the
|
|
242
|
+
// same question without a restart, and the restart is not free: every first
|
|
243
|
+
// build writes src/site-config.ts on the direction pick, and that cost 11.7s
|
|
244
|
+
// of preview outage, with a `preview.upstream-error` and a re-attach the
|
|
245
|
+
// customer sees as the waking card (measured 2026-09-12, run 10).
|
|
246
|
+
...(fonts === "catalog" ? [] : [siteConfigWatch(siteConfigPath)]),
|
|
231
247
|
];
|
|
232
248
|
|
|
233
249
|
const fontFamilies = catalogFontFamilies(fonts);
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The article-body stylesheet: `class="markdown-body"` around compiled
|
|
3
|
+
* `renderMarkdown` output.
|
|
4
|
+
*
|
|
5
|
+
* It exists because `renderMarkdown` is bare micromark: it emits `<h2>`, `<ul>`
|
|
6
|
+
* and `<blockquote>` with no classes, and Tailwind's Preflight flattens every
|
|
7
|
+
* one of them to the paragraph's size, weight and margin. A body compiled
|
|
8
|
+
* without this file renders as a run of identical lines, which is what a blog
|
|
9
|
+
* post looked like through 3.9.0.
|
|
10
|
+
*
|
|
11
|
+
* OPT-IN, and imported once. The package ships no stylesheet of its own and
|
|
12
|
+
* LayoutCore imports none, so nothing here reaches a brand until that brand's
|
|
13
|
+
* `globals.css` names it:
|
|
14
|
+
*
|
|
15
|
+
* @import "@iterant/site-runtime/styles/markdown.css";
|
|
16
|
+
*
|
|
17
|
+
* Put the line AFTER `@import "tailwindcss"`. Everything below lives in the
|
|
18
|
+
* `base` layer, so it wins over Preflight by order and loses to every utility
|
|
19
|
+
* class: `class="markdown-body text-lg"` still reads as the site author wrote
|
|
20
|
+
* it, and a brand rule outside a layer overrides anything here.
|
|
21
|
+
*
|
|
22
|
+
* Colors, fonts, radius and border come from the `--it-*` and `--font-*` tokens
|
|
23
|
+
* a brand's `globals.css` defines, each with a fallback so the file also works
|
|
24
|
+
* in a repo that defines none. Two knobs are its own, settable on any ancestor:
|
|
25
|
+
*
|
|
26
|
+
* --it-markdown-measure the reading width (default 68ch; `none` to fill)
|
|
27
|
+
* --it-markdown-flow the space between blocks (default 1.25em)
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
@layer base {
|
|
31
|
+
.markdown-body {
|
|
32
|
+
color: var(--it-text-primary, currentColor);
|
|
33
|
+
font-family: var(--font-body, inherit);
|
|
34
|
+
font-size: var(--font-size-base, 1rem);
|
|
35
|
+
font-weight: var(--font-body-weight, 400);
|
|
36
|
+
line-height: var(--font-body-line-height, 1.7);
|
|
37
|
+
max-width: var(--it-markdown-measure, 68ch);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* The wrapper's own spacing belongs to the page around it. */
|
|
41
|
+
.markdown-body > :first-child {
|
|
42
|
+
margin-top: 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.markdown-body > :last-child {
|
|
46
|
+
margin-bottom: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.markdown-body p {
|
|
50
|
+
margin: 0 0 var(--it-markdown-flow, 1.25em);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* A body opens at `##` under the page's own h1, so h2 carries the section
|
|
54
|
+
weight here. h1 is styled anyway: a pasted body that starts at `#` should
|
|
55
|
+
read as a heading rather than as an outsized paragraph. */
|
|
56
|
+
.markdown-body h1,
|
|
57
|
+
.markdown-body h2,
|
|
58
|
+
.markdown-body h3,
|
|
59
|
+
.markdown-body h4 {
|
|
60
|
+
color: var(--it-text-primary, currentColor);
|
|
61
|
+
font-family: var(--font-heading, inherit);
|
|
62
|
+
font-weight: var(--font-heading-weight, 700);
|
|
63
|
+
letter-spacing: var(--font-heading-letter-spacing, normal);
|
|
64
|
+
line-height: var(--font-heading-line-height, 1.2);
|
|
65
|
+
margin: 2em 0 0.6em;
|
|
66
|
+
text-wrap: balance;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.markdown-body h1 {
|
|
70
|
+
font-size: var(--font-size-3xl, 1.875rem);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.markdown-body h2 {
|
|
74
|
+
font-size: var(--font-size-2xl, 1.5rem);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.markdown-body h3 {
|
|
78
|
+
font-size: var(--font-size-xl, 1.25rem);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.markdown-body h4 {
|
|
82
|
+
font-size: var(--font-size-lg, 1.125rem);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.markdown-body ul,
|
|
86
|
+
.markdown-body ol {
|
|
87
|
+
margin: 0 0 var(--it-markdown-flow, 1.25em);
|
|
88
|
+
padding-left: 1.5em;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.markdown-body ul {
|
|
92
|
+
list-style: disc;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.markdown-body ol {
|
|
96
|
+
list-style: decimal;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.markdown-body ul ul {
|
|
100
|
+
list-style: circle;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.markdown-body li {
|
|
104
|
+
margin-block: 0.35em;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.markdown-body li::marker {
|
|
108
|
+
color: var(--it-text-secondary, currentColor);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* A nested list is part of its parent item, not a new block. */
|
|
112
|
+
.markdown-body li > ul,
|
|
113
|
+
.markdown-body li > ol {
|
|
114
|
+
margin-block: 0.35em;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.markdown-body li > p {
|
|
118
|
+
margin-bottom: 0.35em;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.markdown-body blockquote {
|
|
122
|
+
border-left: var(--border-width, 1px) var(--border-style, solid)
|
|
123
|
+
var(--it-border-primary, currentColor);
|
|
124
|
+
color: var(--it-text-secondary, currentColor);
|
|
125
|
+
margin: var(--it-markdown-flow, 1.25em) 0;
|
|
126
|
+
padding-left: 1.25em;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.markdown-body blockquote > :last-child {
|
|
130
|
+
margin-bottom: 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.markdown-body code {
|
|
134
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
135
|
+
font-size: 0.9em;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* A code span is a chip; a fence is a surface. */
|
|
139
|
+
.markdown-body :not(pre) > code {
|
|
140
|
+
background-color: var(--it-background-secondary, transparent);
|
|
141
|
+
border-radius: calc(var(--radius, 0.5rem) / 2);
|
|
142
|
+
padding: 0.15em 0.4em;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.markdown-body pre {
|
|
146
|
+
background-color: var(--it-background-secondary, transparent);
|
|
147
|
+
border-radius: var(--radius, 0.5rem);
|
|
148
|
+
line-height: 1.55;
|
|
149
|
+
margin: var(--it-markdown-flow, 1.25em) 0;
|
|
150
|
+
overflow-x: auto;
|
|
151
|
+
padding: 1em 1.15em;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.markdown-body pre code {
|
|
155
|
+
background-color: transparent;
|
|
156
|
+
font-size: inherit;
|
|
157
|
+
padding: 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* The link keeps the body's color: an accent token is a background color in
|
|
161
|
+
some brand palettes, and a link mid-sentence has to stay readable in all of
|
|
162
|
+
them. The underline is what marks it. */
|
|
163
|
+
.markdown-body a {
|
|
164
|
+
color: inherit;
|
|
165
|
+
text-decoration: underline;
|
|
166
|
+
text-decoration-thickness: from-font;
|
|
167
|
+
text-underline-offset: 0.2em;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.markdown-body a:hover {
|
|
171
|
+
text-decoration-thickness: 2px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.markdown-body strong {
|
|
175
|
+
color: var(--it-text-primary, currentColor);
|
|
176
|
+
font-weight: var(--font-body-bold-weight, 600);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.markdown-body hr {
|
|
180
|
+
border: 0;
|
|
181
|
+
border-top: var(--border-width, 1px) var(--border-style, solid)
|
|
182
|
+
var(--it-border-primary, currentColor);
|
|
183
|
+
margin: 2.5em 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.markdown-body img {
|
|
187
|
+
display: block;
|
|
188
|
+
height: auto;
|
|
189
|
+
margin: var(--it-markdown-flow, 1.25em) 0;
|
|
190
|
+
max-width: 100%;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* GFM tables. The table itself scrolls, so a wide one never widens the page. */
|
|
194
|
+
.markdown-body table {
|
|
195
|
+
border-collapse: collapse;
|
|
196
|
+
display: block;
|
|
197
|
+
font-size: 0.95em;
|
|
198
|
+
margin: var(--it-markdown-flow, 1.25em) 0;
|
|
199
|
+
max-width: 100%;
|
|
200
|
+
overflow-x: auto;
|
|
201
|
+
width: max-content;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.markdown-body th,
|
|
205
|
+
.markdown-body td {
|
|
206
|
+
border: var(--border-width, 1px) var(--border-style, solid)
|
|
207
|
+
var(--it-border-primary, currentColor);
|
|
208
|
+
padding: 0.5em 0.75em;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* gfm writes `align` on a column the author aligned; only the rest defaults. */
|
|
212
|
+
.markdown-body th:not([align]),
|
|
213
|
+
.markdown-body td:not([align]) {
|
|
214
|
+
text-align: left;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.markdown-body th {
|
|
218
|
+
background-color: var(--it-background-secondary, transparent);
|
|
219
|
+
font-weight: var(--font-body-bold-weight, 600);
|
|
220
|
+
}
|
|
221
|
+
}
|