@escape-game-over/atlas 0.1.23 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -45
- package/bin/use-project.mjs +18 -13
- package/docs/NOT-BUILT.md +1 -1
- package/docs/client-scripts.md +73 -141
- package/docs/rich-text.md +25 -20
- package/package.json +5 -12
- package/src/analytics/google.ts +6 -6
- package/src/analytics/index.ts +4 -3
- package/src/analytics/tags.ts +14 -60
- package/src/analytics/umami.ts +8 -8
- package/src/astro/ConsentBanner.astro +25 -0
- package/src/astro/ConsentElement.astro +61 -0
- package/src/astro/Document.astro +44 -0
- package/src/astro/Image.astro +102 -0
- package/src/astro/MetaTags.astro +3 -26
- package/src/astro/RichText.astro +71 -0
- package/src/astro/Zoom.astro +61 -0
- package/src/astro/client.ts +19 -9
- package/src/astro/consent.ts +20 -0
- package/src/astro/dev-log.ts +8 -14
- package/src/astro/element.ts +111 -112
- package/src/astro/filters-view.ts +48 -64
- package/src/astro/filters.ts +46 -37
- package/src/astro/images.ts +27 -26
- package/src/astro/index.ts +2 -9
- package/src/astro/markup.ts +6 -6
- package/src/astro/site-routes.ts +10 -15
- package/src/config.ts +23 -36
- package/src/content/index.ts +2 -1
- package/src/content/marks.ts +13 -13
- package/src/content/rich.ts +58 -29
- package/src/hours.ts +48 -11
- package/src/i18n/define.ts +14 -74
- package/src/index.ts +41 -57
- package/src/jsonld/faq.ts +2 -1
- package/src/jsonld/node.ts +4 -14
- package/src/meta/index.ts +7 -13
- package/src/meta/share-image.ts +6 -19
- package/src/meta/tag.ts +1 -45
- package/src/money.ts +161 -6
- package/src/project.ts +84 -73
- package/src/routes/define.ts +8 -44
- package/src/routes/resolve.ts +1 -1
- package/src/site/api.ts +7 -33
- package/src/site/create.ts +6 -10
- package/src/site/define.ts +120 -0
- package/src/site/index.ts +2 -5
- package/src/site/page.ts +4 -2
- package/src/sitemap.ts +2 -35
- package/src/warn.ts +16 -17
- package/src/astro/dom.ts +0 -35
package/src/content/rich.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
joinUrl,
|
|
23
23
|
type UrlPath,
|
|
24
24
|
} from "../url.ts";
|
|
25
|
+
import { escapeXml } from "../xml.ts";
|
|
25
26
|
import { type LinkNames, type ParsedSpan, parseMarks } from "./marks.ts";
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -52,7 +53,7 @@ export type Span =
|
|
|
52
53
|
* edit rather than a sweep through fifty-four deployments' copy.
|
|
53
54
|
*/
|
|
54
55
|
| {
|
|
55
|
-
readonly kind: "
|
|
56
|
+
readonly kind: "variant";
|
|
56
57
|
readonly text: string;
|
|
57
58
|
readonly variant: string;
|
|
58
59
|
}
|
|
@@ -104,12 +105,12 @@ export type Span =
|
|
|
104
105
|
readonly href: Hash;
|
|
105
106
|
}
|
|
106
107
|
| {
|
|
107
|
-
readonly kind: "
|
|
108
|
+
readonly kind: "mail";
|
|
108
109
|
readonly text: string;
|
|
109
110
|
readonly href: MailtoUrl;
|
|
110
111
|
}
|
|
111
112
|
| {
|
|
112
|
-
readonly kind: "
|
|
113
|
+
readonly kind: "tel";
|
|
113
114
|
readonly text: string;
|
|
114
115
|
readonly href: TelUrl;
|
|
115
116
|
}
|
|
@@ -229,7 +230,7 @@ export type RichTextFor<Catalog, RouteId extends string> = <
|
|
|
229
230
|
...args: RichArgsOf<Catalog, K, RouteId>
|
|
230
231
|
) => RichText;
|
|
231
232
|
|
|
232
|
-
/** `plain()`, keyed and parameterised exactly as `t()` is. */
|
|
233
|
+
/** `site.plain()`, keyed and parameterised exactly as `t()` is. */
|
|
233
234
|
export type PlainTextFor<Catalog> = <K extends StringKeys<Catalog>>(
|
|
234
235
|
...args: TranslateArgsOf<Catalog, K>
|
|
235
236
|
) => string;
|
|
@@ -285,8 +286,8 @@ function resolve(
|
|
|
285
286
|
case "text":
|
|
286
287
|
case "bold":
|
|
287
288
|
return { kind: span.kind, text };
|
|
288
|
-
case "
|
|
289
|
-
return { kind: "
|
|
289
|
+
case "variant":
|
|
290
|
+
return { kind: "variant", text, variant: span.variant };
|
|
290
291
|
case "link":
|
|
291
292
|
return resolveLink(
|
|
292
293
|
text,
|
|
@@ -295,10 +296,10 @@ function resolve(
|
|
|
295
296
|
link,
|
|
296
297
|
origin
|
|
297
298
|
);
|
|
298
|
-
case "
|
|
299
|
-
return { kind: "
|
|
300
|
-
case "
|
|
301
|
-
return { kind: "
|
|
299
|
+
case "mail":
|
|
300
|
+
return { kind: "mail", text, href: mailto(text, at) };
|
|
301
|
+
case "tel":
|
|
302
|
+
return { kind: "tel", text, href: tel(text, at) };
|
|
302
303
|
}
|
|
303
304
|
}
|
|
304
305
|
|
|
@@ -473,19 +474,52 @@ function tel(text: string, at: string): TelUrl {
|
|
|
473
474
|
}
|
|
474
475
|
|
|
475
476
|
/**
|
|
476
|
-
* The
|
|
477
|
+
* The runs as inline HTML, for a field that takes markup rather than a page.
|
|
477
478
|
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
* string — so without this each project flattens the runs by hand, and they
|
|
482
|
-
* drift. The implementation this replaces did exactly that: a chain of `if`s
|
|
483
|
-
* per kind, `return ""` for the ones it could not render, and a `throw` on
|
|
484
|
-
* anything it had not been taught.
|
|
479
|
+
* Written for a `FAQPage` answer, whose `text` accepts a handful of tags —
|
|
480
|
+
* `<a>`, `<strong>`, `<br>` among them. Only those are emitted: a `[v:]` role
|
|
481
|
+
* is presentation, which structured data has no use for, so it is its words.
|
|
485
482
|
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
483
|
+
* Links carry their absolute `url`, since nothing reading this has a page to
|
|
484
|
+
* resolve a path against. An anchor has none, so it is its words too.
|
|
485
|
+
*
|
|
486
|
+
* Every piece of copy is escaped: the field is read as HTML, so an `&` or a `<`
|
|
487
|
+
* in a translation would otherwise be read as markup.
|
|
488
|
+
*/
|
|
489
|
+
export function html(rich: RichText): string {
|
|
490
|
+
return rich
|
|
491
|
+
.map((span) => {
|
|
492
|
+
switch (span.kind) {
|
|
493
|
+
case "text":
|
|
494
|
+
case "variant":
|
|
495
|
+
return escapeXml(span.text);
|
|
496
|
+
case "bold":
|
|
497
|
+
return `<strong>${escapeXml(span.text)}</strong>`;
|
|
498
|
+
case "link":
|
|
499
|
+
return span.to === "anchor"
|
|
500
|
+
? escapeXml(span.text)
|
|
501
|
+
: anchor(span.url, span.text);
|
|
502
|
+
case "mail":
|
|
503
|
+
case "tel":
|
|
504
|
+
return anchor(span.href, span.text);
|
|
505
|
+
case "break":
|
|
506
|
+
return "<br>";
|
|
507
|
+
default: {
|
|
508
|
+
const unreachable: never = span;
|
|
509
|
+
return unreachable;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
})
|
|
513
|
+
.join("");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function anchor(href: string, text: string): string {
|
|
517
|
+
return `<a href="${escapeXml(href)}">${escapeXml(text)}</a>`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* The words of runs already built — for a message whose values `rich()` was
|
|
522
|
+
* given in code. A `[br]` becomes a space and whitespace collapses.
|
|
489
523
|
*/
|
|
490
524
|
export function plain(rich: RichText): string {
|
|
491
525
|
return collapse(
|
|
@@ -505,14 +539,9 @@ function collapse(parts: readonly string[]): string {
|
|
|
505
539
|
}
|
|
506
540
|
|
|
507
541
|
/**
|
|
508
|
-
* The
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
* that want words rather than runs are the ones furthest from a renderer:
|
|
512
|
-
* `llms()`'s `describe`, a `<meta name="description">`, a structured-data
|
|
513
|
-
* `description`. Those are already assembling several strings at once, and
|
|
514
|
-
* `plain(rich("about.intro", { company }))` reads as two operations there when
|
|
515
|
-
* it is one question — what does this message say.
|
|
542
|
+
* The words of a message alone, for everywhere that takes a string rather than
|
|
543
|
+
* markup: a meta description, an `llms.txt` summary, a structured-data
|
|
544
|
+
* `description`. A `[br]` becomes a space and whitespace collapses.
|
|
516
545
|
*
|
|
517
546
|
* It also answers that question for a message with no marks at all, which `t()`
|
|
518
547
|
* would too. That overlap is deliberate: a description built this way keeps
|
package/src/hours.ts
CHANGED
|
@@ -164,18 +164,22 @@ export function groupHours(
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* Day names and "closed" are copy: they are translated, and lib holds no copy.
|
|
170
|
-
* Everything else about the line — which days collapse into a span, what order
|
|
171
|
-
* they come in, where the dashes go — is mechanical and is done here, because
|
|
172
|
-
* otherwise every project rewrites the same joins in a component.
|
|
167
|
+
* How to print a week. Day names come from `Intl` in `locale`; "closed" is
|
|
168
|
+
* copy, so the caller translates it.
|
|
173
169
|
*/
|
|
174
170
|
export interface HoursFormat {
|
|
175
|
-
/**
|
|
176
|
-
|
|
171
|
+
/** The language the days are named in, e.g. `"el-GR"`. */
|
|
172
|
+
locale: string;
|
|
173
|
+
/** `Mon` or `Monday`. Defaults to `"short"`. */
|
|
174
|
+
weekday?: "short" | "long";
|
|
177
175
|
/** The word for a day the venue is shut. */
|
|
178
176
|
closed: string;
|
|
177
|
+
/**
|
|
178
|
+
* Times print as written (`14:00`) unless this is `"h12"` (`2:00 PM`).
|
|
179
|
+
* Not the locale's default, which gives Greek a 12-hour clock nobody there
|
|
180
|
+
* uses on a door.
|
|
181
|
+
*/
|
|
182
|
+
hourCycle?: "h12";
|
|
179
183
|
/** Between the ends of a span: `Mon–Thu`, `14:00–23:30`. */
|
|
180
184
|
between?: string;
|
|
181
185
|
/** Between spans: `Mon–Thu, Sun`. */
|
|
@@ -207,20 +211,53 @@ export function formatHours(
|
|
|
207
211
|
): readonly FormattedHours[] {
|
|
208
212
|
const between = format.between ?? "–";
|
|
209
213
|
const and = format.and ?? ", ";
|
|
214
|
+
// 2024-01-01 was a Monday, so `WEEK[i]` falls on the 1st plus `i`.
|
|
215
|
+
const names = new Intl.DateTimeFormat(format.locale, {
|
|
216
|
+
weekday: format.weekday ?? "short",
|
|
217
|
+
timeZone: "UTC",
|
|
218
|
+
});
|
|
219
|
+
const dayName = (day: Weekday): string =>
|
|
220
|
+
names.format(new Date(Date.UTC(2024, 0, 1 + WEEK.indexOf(day))));
|
|
221
|
+
const clock =
|
|
222
|
+
format.hourCycle === "h12"
|
|
223
|
+
? new Intl.DateTimeFormat(format.locale, {
|
|
224
|
+
hour: "numeric",
|
|
225
|
+
minute: "2-digit",
|
|
226
|
+
hourCycle: "h12",
|
|
227
|
+
timeZone: "UTC",
|
|
228
|
+
})
|
|
229
|
+
: undefined;
|
|
230
|
+
const time = (at: TimeOfDay): string =>
|
|
231
|
+
clock === undefined
|
|
232
|
+
? at
|
|
233
|
+
: clock.format(
|
|
234
|
+
new Date(
|
|
235
|
+
Date.UTC(
|
|
236
|
+
2024,
|
|
237
|
+
0,
|
|
238
|
+
1,
|
|
239
|
+
Number(at.slice(0, 2)),
|
|
240
|
+
Number(at.slice(3, 5))
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
);
|
|
210
244
|
|
|
211
245
|
return groupHours(hours, format.weekStart).map((group) => ({
|
|
212
246
|
days: group.runs
|
|
213
247
|
.map(([first, last]) =>
|
|
214
248
|
first === last
|
|
215
|
-
?
|
|
216
|
-
: `${
|
|
249
|
+
? dayName(first)
|
|
250
|
+
: `${dayName(first)}${between}${dayName(last)}`
|
|
217
251
|
)
|
|
218
252
|
.join(and),
|
|
219
253
|
times:
|
|
220
254
|
group.hours === "closed"
|
|
221
255
|
? format.closed
|
|
222
256
|
: group.hours
|
|
223
|
-
.map(
|
|
257
|
+
.map(
|
|
258
|
+
(range) =>
|
|
259
|
+
`${time(range.opens)}${between}${time(range.closes)}`
|
|
260
|
+
)
|
|
224
261
|
.join(and),
|
|
225
262
|
}));
|
|
226
263
|
}
|
package/src/i18n/define.ts
CHANGED
|
@@ -11,15 +11,15 @@ import type {
|
|
|
11
11
|
* `true` when `Catalog` declares every key in `Required`; otherwise an object
|
|
12
12
|
* type naming the ones it does not.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Check it with `satisfies`. A missing message then fails on that line, and the
|
|
15
|
+
* error text lists exactly which keys are absent, rather than surfacing later
|
|
16
|
+
* as a page with no `<title>`:
|
|
17
17
|
*
|
|
18
18
|
* ```ts
|
|
19
|
-
*
|
|
19
|
+
* true satisfies CatalogCovers<
|
|
20
20
|
* typeof baseMessages,
|
|
21
21
|
* `route.${RouteId}.${"nav" | "title"}`
|
|
22
|
-
*
|
|
22
|
+
* >;
|
|
23
23
|
* ```
|
|
24
24
|
*
|
|
25
25
|
* The pattern earns its keep over a plain `Required extends StringKeys<Catalog>`
|
|
@@ -67,7 +67,7 @@ type SelfConsistent<E, K> = [MismatchedLocales<E, EntryTokens<E>>] extends [
|
|
|
67
67
|
MismatchedLocales<E, EntryTokens<E>> & string
|
|
68
68
|
>;
|
|
69
69
|
|
|
70
|
-
type ValidateBase<T> = { [K in keyof T]: SelfConsistent<T[K], K> };
|
|
70
|
+
export type ValidateBase<T> = { [K in keyof T]: SelfConsistent<T[K], K> };
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Rejects locale keys the site does not ship.
|
|
@@ -76,7 +76,7 @@ type ValidateBase<T> = { [K in keyof T]: SelfConsistent<T[K], K> };
|
|
|
76
76
|
* from the literal rather than checked against a fixed target, so an extra key
|
|
77
77
|
* structurally satisfies the constraint and would sit in the catalog unread.
|
|
78
78
|
*/
|
|
79
|
-
type NoExtraLocales<T, L extends string> = {
|
|
79
|
+
export type NoExtraLocales<T, L extends string> = {
|
|
80
80
|
[K in keyof T]: NoExcessKeys<T[K], L>;
|
|
81
81
|
};
|
|
82
82
|
|
|
@@ -102,21 +102,7 @@ export function defineMessages<
|
|
|
102
102
|
return catalog;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/**
|
|
106
|
-
* Everything a copy overlay must satisfy — and the only place it is stated.
|
|
107
|
-
*
|
|
108
|
-
* The mirror of `RouteOverlayKeys` / `RouteOverlayShape`, for the same reason:
|
|
109
|
-
* `defineMessageOverrides` and `defineProject` both accept an overlay, and a
|
|
110
|
-
* check added to one and not the other makes that one silently the safer place
|
|
111
|
-
* to write copy. Both name these, so a new rule reaches both.
|
|
112
|
-
*
|
|
113
|
-
* Excess keys go in the constraint, where a typo'd message id reports on that
|
|
114
|
-
* key; the placeholder and locale rules go in the parameter, because they map
|
|
115
|
-
* the whole object.
|
|
116
|
-
*/
|
|
117
|
-
export type MessageOverlayKeys<Base, T> = NoExcessKeys<T, StringKeys<Base>>;
|
|
118
|
-
|
|
119
|
-
/** @see {@link MessageOverlayKeys} — the parameter-position half. */
|
|
105
|
+
/** What `defineProject` checks an `overrideMessages` against: placeholders and locales. */
|
|
120
106
|
export type ValidateOverrideCatalog<
|
|
121
107
|
T,
|
|
122
108
|
Base,
|
|
@@ -135,63 +121,17 @@ type ValidateOverrides<T, Base> = {
|
|
|
135
121
|
};
|
|
136
122
|
|
|
137
123
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* Both the message ids and the locales are inferred from the base catalog you
|
|
141
|
-
* pass in, so overriding one locale of one string needs no type arguments.
|
|
142
|
-
*
|
|
143
|
-
* Enforced at compile time:
|
|
144
|
-
* - the key must exist in the base catalog, so a typo is rejected rather than
|
|
145
|
-
* silently becoming a string nothing reads;
|
|
146
|
-
* - the replacement must use exactly the `{placeholders}` and `[marks]` of the
|
|
147
|
-
* base message, so every existing `t()` and `rich()` call site stays correct.
|
|
148
|
-
*/
|
|
149
|
-
export function defineMessageOverrides<
|
|
150
|
-
const C extends SiteConfigShape,
|
|
151
|
-
const Base extends BaseCatalog<LocalesOf<C>>,
|
|
152
|
-
const T extends OverrideCatalog<LocalesOf<C>> & MessageOverlayKeys<Base, T>,
|
|
153
|
-
>(
|
|
154
|
-
// Read for its type only, and taken first like every other definer here.
|
|
155
|
-
// It supplies the locale universe directly rather than having it inferred
|
|
156
|
-
// from the catalog's entries: `LocalesOfCatalog` reads the locales actually
|
|
157
|
-
// *used*, which is the same union for a well-formed catalog and a narrower
|
|
158
|
-
// one for a partial or empty draft.
|
|
159
|
-
//
|
|
160
|
-
// Note what this does not do: `config` declares which locales the site has,
|
|
161
|
-
// not which ones a project publishes. Rejecting copy for a language a given
|
|
162
|
-
// deployment has switched off is `MessageOverrides`' job, because only the
|
|
163
|
-
// project knows its own `enabledLocales`.
|
|
164
|
-
_config: C,
|
|
165
|
-
_base: Base,
|
|
166
|
-
overrides: T & ValidateOverrides<T, Base> & NoExtraLocales<T, LocalesOf<C>>
|
|
167
|
-
): T {
|
|
168
|
-
return overrides;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* The shape of one project's copy overrides: known keys, published locales.
|
|
173
|
-
*
|
|
174
|
-
* For annotating the object itself, which is what puts the error on the line
|
|
175
|
-
* that is wrong:
|
|
124
|
+
* One project's copy overrides — known keys, published locales — for an overlay
|
|
125
|
+
* kept in its own file:
|
|
176
126
|
*
|
|
177
127
|
* ```ts
|
|
178
|
-
* const
|
|
128
|
+
* export const romeMessages = {
|
|
179
129
|
* "site.cta": { "en-US": "Book a room" },
|
|
180
|
-
* } satisfies MessageOverrides<typeof defaultMessages, EnabledLocale>;
|
|
181
|
-
*
|
|
182
|
-
* export const overrideMessages = defineMessageOverrides(
|
|
183
|
-
* config,
|
|
184
|
-
* defaultMessages,
|
|
185
|
-
* overrides
|
|
186
|
-
* );
|
|
130
|
+
* } as const satisfies MessageOverrides<typeof defaultMessages, EnabledLocale>;
|
|
187
131
|
* ```
|
|
188
132
|
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* but it cannot compare an override's *text* with the default's, so it cannot
|
|
192
|
-
* see a dropped `{placeholder}`. `defineMessageOverrides` is the reverse: it
|
|
193
|
-
* reads both texts, and knows nothing about which locales this deployment
|
|
194
|
-
* publishes. Annotate with one, wrap in the other.
|
|
133
|
+
* A dropped `{placeholder}` is not visible to a type like this; `defineProject`
|
|
134
|
+
* reports it, naming the key and locale.
|
|
195
135
|
*/
|
|
196
136
|
export type MessageOverrides<Catalog, L extends string> = {
|
|
197
137
|
readonly [K in StringKeys<Catalog>]?: Readonly<Partial<Record<L, string>>>;
|
package/src/index.ts
CHANGED
|
@@ -1,27 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The public API. A consuming project should need nothing outside this module.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Everything starts from `defineSite`:
|
|
5
5
|
*
|
|
6
6
|
* ```ts
|
|
7
|
-
*
|
|
8
|
-
* export
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* export const
|
|
12
|
-
* export const baseRoutes = defineRoutes(config, {...})
|
|
13
|
-
*
|
|
14
|
-
* // 3. projects/<name>/project.ts — one deployment's overlay
|
|
15
|
-
* export default defineProject(config, baseMessages, baseRoutes, {...})
|
|
16
|
-
*
|
|
17
|
-
* // 4. src/site.ts — the API the pages use
|
|
18
|
-
* export const site = createSite(config, baseMessages, baseRoutes, project)
|
|
7
|
+
* export const atlas = defineSite({ locales: {...}, defaultRouting: {...} }) // site.config.ts
|
|
8
|
+
* export const defaultMessages = atlas.messages({...}) // messages.ts
|
|
9
|
+
* export const defaultRoutes = atlas.routes({...}) // routes.ts
|
|
10
|
+
* export default atlas.project(defaultMessages, defaultRoutes, {...}) // projects/<name>/project.ts
|
|
11
|
+
* export const site = atlas.site(defaultMessages, defaultRoutes, project) // site.ts
|
|
19
12
|
* ```
|
|
20
13
|
*/
|
|
21
14
|
|
|
22
15
|
export {
|
|
23
16
|
type AnalyticsSettings,
|
|
24
|
-
type AnalyticsTag,
|
|
25
17
|
type AnalyticsTags,
|
|
26
18
|
CONSENT_UPDATE_GLOBAL,
|
|
27
19
|
type ConsentDefaults,
|
|
@@ -32,14 +24,13 @@ export {
|
|
|
32
24
|
type UmamiSettings,
|
|
33
25
|
type UmamiTracker,
|
|
34
26
|
} from "./analytics/index.ts";
|
|
35
|
-
export {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
type SiteConfigShape,
|
|
27
|
+
export type {
|
|
28
|
+
LanguageTag,
|
|
29
|
+
LocaleMeta,
|
|
30
|
+
LocalesOf,
|
|
31
|
+
ResolvedLocaleMeta,
|
|
32
|
+
RoutingConfig,
|
|
33
|
+
SiteConfigShape,
|
|
43
34
|
} from "./config.ts";
|
|
44
35
|
export {
|
|
45
36
|
assertCoordinates,
|
|
@@ -72,6 +63,7 @@ export {
|
|
|
72
63
|
// a slot filled with a route id resolves against nothing there. `site.rich(locale)` is
|
|
73
64
|
// the only way in, and `Span` is what a renderer switches on.
|
|
74
65
|
export {
|
|
66
|
+
html,
|
|
75
67
|
type LinkTarget,
|
|
76
68
|
type PlainTextFor,
|
|
77
69
|
plain,
|
|
@@ -99,13 +91,11 @@ export {
|
|
|
99
91
|
type Weekday,
|
|
100
92
|
type WeeklyHours,
|
|
101
93
|
} from "./hours.ts";
|
|
102
|
-
export {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
type MessageOverrides,
|
|
108
|
-
type OverrideCatalog,
|
|
94
|
+
export type {
|
|
95
|
+
BaseCatalog,
|
|
96
|
+
CatalogCovers,
|
|
97
|
+
MessageOverrides,
|
|
98
|
+
OverrideCatalog,
|
|
109
99
|
} from "./i18n/define.ts";
|
|
110
100
|
export type {
|
|
111
101
|
EntryPlaceholders,
|
|
@@ -170,10 +160,8 @@ export {
|
|
|
170
160
|
type ArticleContent,
|
|
171
161
|
buildMeta,
|
|
172
162
|
buildNotFoundMeta,
|
|
173
|
-
type ImageFormat,
|
|
174
163
|
type MetaContent,
|
|
175
164
|
type MetaTag,
|
|
176
|
-
type OgImage,
|
|
177
165
|
type OpenGraphType,
|
|
178
166
|
type PageKind,
|
|
179
167
|
type RobotsPolicy,
|
|
@@ -196,7 +184,7 @@ export {
|
|
|
196
184
|
type TieredPrice,
|
|
197
185
|
tierRange,
|
|
198
186
|
} from "./money.ts";
|
|
199
|
-
export {
|
|
187
|
+
export type { ProjectInput } from "./project.ts";
|
|
200
188
|
export {
|
|
201
189
|
buildCloudflareRedirects,
|
|
202
190
|
buildRedirects,
|
|
@@ -211,15 +199,14 @@ export {
|
|
|
211
199
|
type ValidateRedirectTargets,
|
|
212
200
|
} from "./redirects.ts";
|
|
213
201
|
export type { RobotsConfig, RobotsGroup } from "./robots.ts";
|
|
214
|
-
export {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
type RouteSitemap,
|
|
202
|
+
export type {
|
|
203
|
+
ChangeFreq,
|
|
204
|
+
EnabledRouteIdFor,
|
|
205
|
+
RouteData,
|
|
206
|
+
RouteOverride,
|
|
207
|
+
RouteOverrides,
|
|
208
|
+
RouteRegistry,
|
|
209
|
+
RouteSitemap,
|
|
223
210
|
} from "./routes/define.ts";
|
|
224
211
|
export {
|
|
225
212
|
enableRoutes,
|
|
@@ -228,24 +215,21 @@ export {
|
|
|
228
215
|
type FamilyOptions,
|
|
229
216
|
familyGuard,
|
|
230
217
|
type RouteIdsOf,
|
|
231
|
-
routeFamily,
|
|
232
218
|
} from "./routes/family.ts";
|
|
233
219
|
export type { RouteEntry } from "./routes/resolve.ts";
|
|
234
|
-
export {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
type StaticPath,
|
|
248
|
-
type WhenEnabled,
|
|
220
|
+
export { defineSite } from "./site/define.ts";
|
|
221
|
+
export type {
|
|
222
|
+
Alternate,
|
|
223
|
+
Crumb,
|
|
224
|
+
LinkOptions,
|
|
225
|
+
LlmsOptions,
|
|
226
|
+
LocaleLink,
|
|
227
|
+
PageContent,
|
|
228
|
+
PageMeta,
|
|
229
|
+
PageProps,
|
|
230
|
+
PerRoute,
|
|
231
|
+
Site,
|
|
232
|
+
StaticPath,
|
|
249
233
|
} from "./site/index.ts";
|
|
250
234
|
export type { Sitemap, SitemapConfig } from "./sitemap.ts";
|
|
251
235
|
export type { IsoDate, Letter, StringKeys } from "./types.ts";
|
package/src/jsonld/faq.ts
CHANGED
|
@@ -11,7 +11,8 @@ export interface FaqEntry {
|
|
|
11
11
|
* `<ul>`, `<li>`, `<a>`, `<b>`, `<strong>`, `<i>`, `<em>` — and drops
|
|
12
12
|
* everything else. Plain text is what a caller passing a translated string
|
|
13
13
|
* has anyway, and it cannot be silently half-rendered, so nothing here
|
|
14
|
-
* builds markup for you.
|
|
14
|
+
* builds markup for you. An answer with links in it wants `html(rich(…))`,
|
|
15
|
+
* which emits only tags from that list and escapes the copy.
|
|
15
16
|
*
|
|
16
17
|
* Checked August 2026.
|
|
17
18
|
*/
|
package/src/jsonld/node.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* kind of node and imports these to do it.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { literal } from "../analytics/tags.ts";
|
|
9
|
+
|
|
8
10
|
/** A node in the graph. Deliberately loose: `@type` is what a consumer varies. */
|
|
9
11
|
export interface JsonLdNode {
|
|
10
12
|
readonly "@type": string | readonly string[];
|
|
@@ -60,19 +62,7 @@ export function alternateName(
|
|
|
60
62
|
: { alternateName: alternate };
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
/**
|
|
64
|
-
* The graph, serialised for a `<script type="application/ld+json">`.
|
|
65
|
-
*
|
|
66
|
-
* Every `<` is replaced by its unicode escape, and that is the whole reason
|
|
67
|
-
* this is a function rather than a `JSON.stringify` at the call site. A script element ends at the first
|
|
68
|
-
* `</script` in its text, so a business name, a room description or an alt text
|
|
69
|
-
* containing one would close the block early and spill the rest of the graph
|
|
70
|
-
* into the page as markup. The escape is invisible to a JSON parser and to
|
|
71
|
-
* anything reading the data.
|
|
72
|
-
*/
|
|
65
|
+
/** The graph, serialised for a `<script type="application/ld+json">`. */
|
|
73
66
|
export function serializeJsonLd(nodes: readonly JsonLdNode[]): string {
|
|
74
|
-
return
|
|
75
|
-
"@context": "https://schema.org",
|
|
76
|
-
"@graph": nodes,
|
|
77
|
-
}).replaceAll("<", "\\u003c");
|
|
67
|
+
return literal({ "@context": "https://schema.org", "@graph": nodes });
|
|
78
68
|
}
|
package/src/meta/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
notFoundAnalytics,
|
|
30
30
|
} from "../analytics/index.ts";
|
|
31
31
|
import type { ResolvedLocaleMeta } from "../config.ts";
|
|
32
|
+
import type { ImageAsset } from "../image.ts";
|
|
32
33
|
import {
|
|
33
34
|
article,
|
|
34
35
|
type BreadcrumbStep,
|
|
@@ -42,11 +43,10 @@ import { robotsContent, warnAboutRobots } from "./robots.ts";
|
|
|
42
43
|
import {
|
|
43
44
|
iconLinks,
|
|
44
45
|
type OgImage,
|
|
45
|
-
type SiteIcon,
|
|
46
46
|
warnAboutShareImage,
|
|
47
47
|
warnIfClipped,
|
|
48
48
|
} from "./share-image.ts";
|
|
49
|
-
import {
|
|
49
|
+
import { link, type MetaTag, meta, preamble } from "./tag.ts";
|
|
50
50
|
import { type SiteVerification, verificationTags } from "./verification.ts";
|
|
51
51
|
|
|
52
52
|
export type {
|
|
@@ -57,13 +57,7 @@ export type {
|
|
|
57
57
|
ThemeColor,
|
|
58
58
|
} from "./content.ts";
|
|
59
59
|
export type { RobotsDirective, RobotsPolicy } from "./robots.ts";
|
|
60
|
-
export {
|
|
61
|
-
type ImageFormat,
|
|
62
|
-
type OgImage,
|
|
63
|
-
resolveShareImage,
|
|
64
|
-
type ShareImage,
|
|
65
|
-
type SiteIcon,
|
|
66
|
-
} from "./share-image.ts";
|
|
60
|
+
export { resolveShareImage, type ShareImage } from "./share-image.ts";
|
|
67
61
|
export type { MetaTag } from "./tag.ts";
|
|
68
62
|
export type { SiteVerification } from "./verification.ts";
|
|
69
63
|
|
|
@@ -155,7 +149,7 @@ interface MetaInputDerived<L extends string> extends ChromeInput {
|
|
|
155
149
|
*/
|
|
156
150
|
export interface ChromeInput {
|
|
157
151
|
/** The site's square icon, used for every icon link. */
|
|
158
|
-
readonly icon:
|
|
152
|
+
readonly icon: ImageAsset;
|
|
159
153
|
/**
|
|
160
154
|
* Tints the browser UI — Android Chrome's bar, iOS Safari, an installed PWA.
|
|
161
155
|
* Two values emit one tag per `prefers-color-scheme`.
|
|
@@ -250,8 +244,8 @@ export function buildMeta<L extends string>(input: MetaInput<L>): DocumentTags {
|
|
|
250
244
|
// that must precede every tag reading them, and a tag that got there first
|
|
251
245
|
// would silently apply the permissive implicit default instead.
|
|
252
246
|
const analytics = analyticsScripts(input.analytics, input.canonical);
|
|
253
|
-
|
|
254
|
-
const bodyTags: MetaTag[] = analytics.body
|
|
247
|
+
tags.push(...analytics.head);
|
|
248
|
+
const bodyTags: MetaTag[] = [...analytics.body];
|
|
255
249
|
|
|
256
250
|
if (input.llmsUrl !== undefined) {
|
|
257
251
|
// Points a client at the file describing this page, per llmstxt.org.
|
|
@@ -473,7 +467,7 @@ export function buildNotFoundMeta(input: NotFoundInput): MetaTag[] {
|
|
|
473
467
|
name: "robots",
|
|
474
468
|
content: robotsContent({ index: false, follow: true }),
|
|
475
469
|
}),
|
|
476
|
-
...notFoundAnalytics(input.analytics)
|
|
470
|
+
...notFoundAnalytics(input.analytics),
|
|
477
471
|
...chromeTags(input),
|
|
478
472
|
];
|
|
479
473
|
}
|