@escape-game-over/atlas 0.1.1

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.
Files changed (63) hide show
  1. package/README.md +364 -0
  2. package/bin/use-project.mjs +131 -0
  3. package/docs/NOT-BUILT.md +329 -0
  4. package/docs/checks.md +139 -0
  5. package/docs/share-images.md +52 -0
  6. package/docs/toolchain.md +83 -0
  7. package/package.json +51 -0
  8. package/src/analytics/google.ts +351 -0
  9. package/src/analytics/index.ts +102 -0
  10. package/src/analytics/tags.ts +57 -0
  11. package/src/analytics/umami.ts +285 -0
  12. package/src/astro/MetaTags.astro +87 -0
  13. package/src/astro/consent.ts +165 -0
  14. package/src/astro/images.ts +315 -0
  15. package/src/astro/index.ts +44 -0
  16. package/src/astro/public-files.ts +129 -0
  17. package/src/astro/site-routes.ts +307 -0
  18. package/src/config.ts +218 -0
  19. package/src/contact.ts +233 -0
  20. package/src/file.ts +16 -0
  21. package/src/files.ts +39 -0
  22. package/src/hours.ts +312 -0
  23. package/src/i18n/define.ts +217 -0
  24. package/src/i18n/placeholders.ts +94 -0
  25. package/src/i18n/translate.ts +190 -0
  26. package/src/image.ts +29 -0
  27. package/src/index.ts +222 -0
  28. package/src/jsonld/article.ts +165 -0
  29. package/src/jsonld/breadcrumb.ts +34 -0
  30. package/src/jsonld/business.ts +196 -0
  31. package/src/jsonld/ids.ts +106 -0
  32. package/src/jsonld/index.ts +59 -0
  33. package/src/jsonld/node.ts +78 -0
  34. package/src/jsonld/organization.ts +154 -0
  35. package/src/jsonld/place.ts +96 -0
  36. package/src/jsonld/product.ts +172 -0
  37. package/src/jsonld/quantity.ts +55 -0
  38. package/src/jsonld/service.ts +237 -0
  39. package/src/jsonld/video.ts +239 -0
  40. package/src/jsonld/website.ts +58 -0
  41. package/src/llms.ts +160 -0
  42. package/src/meta/content.ts +190 -0
  43. package/src/meta/index.ts +432 -0
  44. package/src/meta/robots.ts +212 -0
  45. package/src/meta/share-image.ts +232 -0
  46. package/src/meta/tag.ts +133 -0
  47. package/src/meta/verification.ts +53 -0
  48. package/src/money.ts +237 -0
  49. package/src/project.ts +249 -0
  50. package/src/redirects.ts +266 -0
  51. package/src/robots.ts +80 -0
  52. package/src/routes/define.ts +412 -0
  53. package/src/routes/family.ts +251 -0
  54. package/src/routes/resolve.ts +266 -0
  55. package/src/site/api.ts +354 -0
  56. package/src/site/create.ts +660 -0
  57. package/src/site/index.ts +32 -0
  58. package/src/site/page.ts +148 -0
  59. package/src/sitemap.ts +257 -0
  60. package/src/types.ts +160 -0
  61. package/src/url.ts +144 -0
  62. package/src/warn.ts +88 -0
  63. package/src/xml.ts +103 -0
@@ -0,0 +1,217 @@
1
+ import type { LocalesOf, SiteConfigShape } from "../config.ts";
2
+ import type { IsNever, NoExcessKeys, StringKeys } from "../types.ts";
3
+ import type {
4
+ EntryPlaceholders,
5
+ MismatchedLocales,
6
+ OverridePlaceholderMismatch,
7
+ PlaceholderMismatch,
8
+ } from "./placeholders.ts";
9
+
10
+ /**
11
+ * `true` when `Catalog` declares every key in `Required`; otherwise an object
12
+ * type naming the ones it does not.
13
+ *
14
+ * Annotate a `const … = true` with it. A missing message then fails at that
15
+ * declaration, and the error text lists exactly which keys are absent, rather
16
+ * than surfacing later as a page with no `<title>`:
17
+ *
18
+ * ```ts
19
+ * export const routeMessagesAreComplete: CatalogCovers<
20
+ * typeof baseMessages,
21
+ * `route.${RouteId}.${"nav" | "title"}`
22
+ * > = true;
23
+ * ```
24
+ *
25
+ * The pattern earns its keep over a plain `Required extends StringKeys<Catalog>`
26
+ * constraint, which reports only that the constraint was not satisfied and
27
+ * leaves you to work out which of the keys is missing.
28
+ *
29
+ * lib supplies the mechanism and never the key names: which messages a project
30
+ * considers mandatory is that project's convention to state.
31
+ */
32
+ export type CatalogCovers<Catalog, Required extends string> =
33
+ IsNever<Exclude<Required, StringKeys<Catalog>>> extends true
34
+ ? true
35
+ : {
36
+ readonly __MISSING_MESSAGES__: Exclude<
37
+ Required,
38
+ StringKeys<Catalog>
39
+ >;
40
+ };
41
+
42
+ /** A base message: every locale is mandatory. */
43
+ export type BaseEntry<L extends string> = Readonly<Record<L, string>>;
44
+ export type BaseCatalog<L extends string> = Readonly<
45
+ Record<string, BaseEntry<L>>
46
+ >;
47
+
48
+ /** A project override: any subset of locales for an existing key. */
49
+ export type OverrideEntry<L extends string> = Readonly<
50
+ Partial<Record<L, string>>
51
+ >;
52
+ export type OverrideCatalog<L extends string> = Readonly<
53
+ Record<string, OverrideEntry<L>>
54
+ >;
55
+
56
+ /** The locale union a catalog covers, read back off its own entries. */
57
+ export type LocalesOfCatalog<Catalog> = StringKeys<
58
+ Catalog[StringKeys<Catalog>]
59
+ >;
60
+
61
+ type SelfConsistent<E, K> = [
62
+ MismatchedLocales<E, EntryPlaceholders<E>>,
63
+ ] extends [never]
64
+ ? unknown
65
+ : PlaceholderMismatch<
66
+ K & string,
67
+ MismatchedLocales<E, EntryPlaceholders<E>> & string
68
+ >;
69
+
70
+ type ValidateBase<T> = { [K in keyof T]: SelfConsistent<T[K], K> };
71
+
72
+ /**
73
+ * Rejects locale keys the site does not ship.
74
+ *
75
+ * `Record<L, string>` alone does not catch these: the entry type is inferred
76
+ * from the literal rather than checked against a fixed target, so an extra key
77
+ * structurally satisfies the constraint and would sit in the catalog unread.
78
+ */
79
+ type NoExtraLocales<T, L extends string> = {
80
+ [K in keyof T]: NoExcessKeys<T[K], L>;
81
+ };
82
+
83
+ /**
84
+ * Declares the base copy.
85
+ *
86
+ * The config argument is what supplies the locale union — pass `site.config.ts`
87
+ * and the locales follow, with no type arguments to write.
88
+ *
89
+ * Enforced at compile time:
90
+ * - every key defines every locale the site ships, and no others;
91
+ * - every locale of a key uses exactly the same `{placeholders}`.
92
+ */
93
+ export function defineMessages<
94
+ const C extends SiteConfigShape,
95
+ const T extends BaseCatalog<LocalesOf<C>>,
96
+ >(
97
+ // Read for its type only: it is how `L` is inferred without a type argument.
98
+ _config: C,
99
+ catalog: T & ValidateBase<T> & NoExtraLocales<T, LocalesOf<C>>
100
+ ): T {
101
+ return catalog;
102
+ }
103
+
104
+ /**
105
+ * Everything a copy overlay must satisfy — and the only place it is stated.
106
+ *
107
+ * The mirror of `RouteOverlayKeys` / `RouteOverlayShape`, for the same reason:
108
+ * `defineMessageOverrides` and `defineProject` both accept an overlay, and a
109
+ * check added to one and not the other makes that one silently the safer place
110
+ * to write copy. Both name these, so a new rule reaches both.
111
+ *
112
+ * Excess keys go in the constraint, where a typo'd message id reports on that
113
+ * key; the placeholder and locale rules go in the parameter, because they map
114
+ * the whole object.
115
+ */
116
+ export type MessageOverlayKeys<Base, T> = NoExcessKeys<T, StringKeys<Base>>;
117
+
118
+ /** @see {@link MessageOverlayKeys} — the parameter-position half. */
119
+ export type ValidateOverrideCatalog<
120
+ T,
121
+ Base,
122
+ L extends string,
123
+ > = ValidateOverrides<T, Base> & NoExtraLocales<T, L>;
124
+
125
+ type ValidateOverrides<T, Base> = {
126
+ [K in keyof T]: K extends keyof Base
127
+ ? [MismatchedLocales<T[K], EntryPlaceholders<Base[K]>>] extends [never]
128
+ ? unknown
129
+ : OverridePlaceholderMismatch<
130
+ K & string,
131
+ MismatchedLocales<T[K], EntryPlaceholders<Base[K]>> & string
132
+ >
133
+ : unknown;
134
+ };
135
+
136
+ /**
137
+ * Declares a project's copy overlay: any subset of keys, any subset of locales.
138
+ *
139
+ * Both the message ids and the locales are inferred from the base catalog you
140
+ * pass in, so overriding one locale of one string needs no type arguments.
141
+ *
142
+ * Enforced at compile time:
143
+ * - the key must exist in the base catalog, so a typo is rejected rather than
144
+ * silently becoming a string nothing reads;
145
+ * - the replacement must use exactly the `{placeholders}` of the base message,
146
+ * so every existing `t()` call site stays correct.
147
+ */
148
+ export function defineMessageOverrides<
149
+ const C extends SiteConfigShape,
150
+ const Base extends BaseCatalog<LocalesOf<C>>,
151
+ const T extends OverrideCatalog<LocalesOf<C>> & MessageOverlayKeys<Base, T>,
152
+ >(
153
+ // Read for its type only, and taken first like every other definer here.
154
+ // It supplies the locale universe directly rather than having it inferred
155
+ // from the catalog's entries: `LocalesOfCatalog` reads the locales actually
156
+ // *used*, which is the same union for a well-formed catalog and a narrower
157
+ // one for a partial or empty draft.
158
+ //
159
+ // Note what this does not do: `config` declares which locales the site has,
160
+ // not which ones a project publishes. Rejecting copy for a language a given
161
+ // deployment has switched off is `MessageOverrides`' job, because only the
162
+ // project knows its own `enabledLocales`.
163
+ _config: C,
164
+ _base: Base,
165
+ overrides: T & ValidateOverrides<T, Base> & NoExtraLocales<T, LocalesOf<C>>
166
+ ): T {
167
+ return overrides;
168
+ }
169
+
170
+ /**
171
+ * The shape of one project's copy overrides: known keys, published locales.
172
+ *
173
+ * For annotating the object itself, which is what puts the error on the line
174
+ * that is wrong:
175
+ *
176
+ * ```ts
177
+ * const overrides = {
178
+ * "site.cta": { "en-US": "Book a room" },
179
+ * } satisfies MessageOverrides<typeof defaultMessages, EnabledLocale>;
180
+ *
181
+ * export const overrideMessages = defineMessageOverrides(
182
+ * config,
183
+ * defaultMessages,
184
+ * overrides
185
+ * );
186
+ * ```
187
+ *
188
+ * The two halves do different jobs and both are needed. This type knows the
189
+ * project's locale set, so it rejects copy for a language that is never built —
190
+ * but it cannot compare an override's *text* with the default's, so it cannot
191
+ * see a dropped `{placeholder}`. `defineMessageOverrides` is the reverse: it
192
+ * reads both texts, and knows nothing about which locales this deployment
193
+ * publishes. Annotate with one, wrap in the other.
194
+ */
195
+ export type MessageOverrides<Catalog, L extends string> = {
196
+ readonly [K in StringKeys<Catalog>]?: Readonly<Partial<Record<L, string>>>;
197
+ };
198
+
199
+ /** Merges a project's override catalog over the base catalog, locale by locale. */
200
+ export function mergeCatalog<L extends string>(
201
+ base: BaseCatalog<L>,
202
+ overrides: OverrideCatalog<L>
203
+ ): Readonly<Record<string, Readonly<Record<L, string>>>> {
204
+ const merged: Record<string, Record<string, string>> = {};
205
+ for (const [key, entry] of Object.entries(base)) {
206
+ merged[key] = { ...entry };
207
+ }
208
+ for (const [key, entry] of Object.entries(overrides)) {
209
+ const target = merged[key];
210
+ // Unreachable via the typed API: override keys are constrained to base keys.
211
+ if (target === undefined) continue;
212
+ for (const [locale, text] of Object.entries(entry)) {
213
+ if (typeof text === "string") target[locale] = text;
214
+ }
215
+ }
216
+ return merged as Readonly<Record<string, Readonly<Record<L, string>>>>;
217
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Placeholder analysis over message templates. Pure types — no locale or copy
3
+ * data of any kind lives in `lib/`.
4
+ */
5
+
6
+ import type { Digit, Letter, StringKeys } from "../types.ts";
7
+
8
+ /**
9
+ * One character a placeholder name may be spelled with.
10
+ *
11
+ * **This is the type-level half of `PLACEHOLDER` in `translate.ts`, and the two
12
+ * have to agree.** They are a hundred lines apart in different files and each
13
+ * reads correct alone, which is how they came to differ: this side used to
14
+ * accept anything between braces while the regex substituted only these, so a
15
+ * message written `"Use {b c} here"` compiled into a *required* `t()` parameter
16
+ * named `"b c"` that the runtime never consumed — the call site was forced to
17
+ * pass a value and the brace still shipped in the markup. The whole point of
18
+ * this machinery is that a literal brace cannot reach a page.
19
+ *
20
+ * `Letter` is uppercase only, so its lowercase twin is spelled out via
21
+ * `Lowercase`.
22
+ */
23
+ type NameChar = Letter | Lowercase<Letter> | Digit | "_";
24
+
25
+ /** Whether every character of `S` may appear in a placeholder name. */
26
+ type IsName<S extends string> = S extends `${infer Head}${infer Tail}`
27
+ ? Head extends NameChar
28
+ ? Tail extends ""
29
+ ? true
30
+ : IsName<Tail>
31
+ : false
32
+ : false;
33
+
34
+ /**
35
+ * Extracts `{placeholder}` names from a message template as a union of literals.
36
+ *
37
+ * `Placeholders<'Hi {name}, {n} new'>` -> `'name' | 'n'`
38
+ * `Placeholders<'Hello'>` -> `never`
39
+ * `Placeholders<'Literal {{name}}'>` -> `never` — the escape, not a name
40
+ * `Placeholders<string>` -> `never`
41
+ *
42
+ * An escaped brace asks for nothing, which falls out of `IsName` rather than
43
+ * needing a case of its own: the candidate extracted from `{{name}}` is
44
+ * `"{name"`, and `{` is not a `NameChar`. The two halves agree by the same rule
45
+ * from opposite ends.
46
+ *
47
+ * What this cannot say is that a *malformed* brace is an error — `render` in
48
+ * `translate.ts` throws on `"{color: red}"`, and this only declines to ask for
49
+ * a parameter. That asymmetry is deliberate: the type's job is to get the
50
+ * call site's arguments right, and the parser's is to refuse copy nobody meant.
51
+ * Both fail the build, one at compile time and one the first time the message
52
+ * renders.
53
+ *
54
+ * Recursion is per character of the name rather than of the message, so it is
55
+ * bounded by how long a placeholder is spelled and not by how long the copy runs.
56
+ */
57
+ export type Placeholders<S extends string> =
58
+ S extends `${string}{${infer Name}}${infer Rest}`
59
+ ? (IsName<Name> extends true ? Name : never) | Placeholders<Rest>
60
+ : never;
61
+
62
+ /** The message strings present on an entry, as a union of string literals. */
63
+ export type TextOf<E> = Extract<E[keyof E], string>;
64
+
65
+ /** Every placeholder used by any locale of an entry. */
66
+ export type EntryPlaceholders<E> = Placeholders<TextOf<E>>;
67
+
68
+ /**
69
+ * The locales of `E` whose placeholder set differs from `Expected`.
70
+ *
71
+ * This is what turns a half-updated translation into a compile error instead of
72
+ * a literal `{name}` rendered on a production page.
73
+ */
74
+ export type MismatchedLocales<E, Expected extends string> = {
75
+ [L in StringKeys<E>]: [
76
+ | Exclude<Expected, Placeholders<Extract<E[L], string>>>
77
+ | Exclude<Placeholders<Extract<E[L], string>>, Expected>,
78
+ ] extends [never]
79
+ ? never
80
+ : L;
81
+ }[StringKeys<E>];
82
+
83
+ /** Editor-facing error when placeholders drift between locales of one message. */
84
+ export interface PlaceholderMismatch<Key extends string, L extends string> {
85
+ readonly __PLACEHOLDER_MISMATCH__: `Message "${Key}" uses different {placeholders} in locale "${L}" than in its other locales`;
86
+ }
87
+
88
+ /** Editor-facing error when an override drops or invents a placeholder. */
89
+ export interface OverridePlaceholderMismatch<
90
+ Key extends string,
91
+ L extends string,
92
+ > {
93
+ readonly __OVERRIDE_PLACEHOLDER_MISMATCH__: `Override of "${Key}" in locale "${L}" must use exactly the same {placeholders} as the base message`;
94
+ }
@@ -0,0 +1,190 @@
1
+ import type { StringKeys } from "../types.ts";
2
+ import type { EntryPlaceholders } from "./placeholders.ts";
3
+
4
+ /**
5
+ * What may be spelled between the braces — the pair of `NameChar` in
6
+ * `placeholders.ts`, which decides the same thing at the type level.
7
+ *
8
+ * Change one and change the other. They answer the same question from opposite
9
+ * ends, and when they disagreed the type demanded a parameter the runtime never
10
+ * consumed, so the call site filled it in and the brace shipped anyway. Kept as
11
+ * a character class rather than a hand-rolled check precisely so it still reads
12
+ * as the obvious mirror of the union over there.
13
+ */
14
+ const NAME = /^[a-zA-Z0-9_]+$/;
15
+
16
+ /**
17
+ * Splits a template into text and placeholders, and refuses anything else.
18
+ *
19
+ * A parser rather than one `replace` over `/\{(name)\}/g`, and the difference
20
+ * is what happens to a brace that is *not* a placeholder. The pattern simply
21
+ * did not match one, so `"CSS: {color: red}"` passed through as text — which
22
+ * looks like an escape hatch and is not one. Nothing declared that brace
23
+ * literal; it survived because it happened to contain a space. Rename the
24
+ * example to `{color}` and the same copy starts demanding a parameter.
25
+ *
26
+ * So the escape is explicit — `{{` and `}}`, as `format!` and `str.format` use
27
+ * — and anything else is an error naming the message and telling you to double
28
+ * the brace. Every brace in a template is now a decision someone made rather
29
+ * than a decision made for them by a character class.
30
+ *
31
+ * A lone `}` stays text. Only `{` opens anything, so there is nothing for an
32
+ * unmatched closer to be ambiguous about, and rejecting it would fail copy that
33
+ * is merely writing a bracket.
34
+ */
35
+ function render(
36
+ template: string,
37
+ at: string,
38
+ value: (name: string, whole: string) => string
39
+ ): string {
40
+ let out = "";
41
+ let index = 0;
42
+
43
+ while (index < template.length) {
44
+ const char = template[index] as string;
45
+
46
+ if (char === "{" && template[index + 1] === "{") {
47
+ out += "{";
48
+ index += 2;
49
+ } else if (char === "}" && template[index + 1] === "}") {
50
+ out += "}";
51
+ index += 2;
52
+ } else if (char === "{") {
53
+ const close = template.indexOf("}", index + 1);
54
+ const name = close === -1 ? "" : template.slice(index + 1, close);
55
+ if (close === -1 || !NAME.test(name)) {
56
+ throw new Error(
57
+ `${at} has a "{" that opens neither a placeholder nor an escape: ${describeBrace(template, index, close)}. A placeholder is {name} — letters, digits and underscores. To write a literal brace, double it: "{{".`
58
+ );
59
+ }
60
+ out += value(name, template.slice(index, close + 1));
61
+ index = close + 1;
62
+ } else {
63
+ out += char;
64
+ index += 1;
65
+ }
66
+ }
67
+ return out;
68
+ }
69
+
70
+ /** The offending brace, quoted, so the error points at something findable. */
71
+ function describeBrace(template: string, open: number, close: number): string {
72
+ const end = close === -1 ? Math.min(open + 20, template.length) : close + 1;
73
+ return `"${template.slice(open, end)}"`;
74
+ }
75
+
76
+ export type MergedCatalog<L extends string> = Readonly<
77
+ Record<string, Readonly<Record<L, string>>>
78
+ >;
79
+
80
+ /** Untyped translation lookup, wrapped below in a key-aware signature. */
81
+ export type RawTranslate = (
82
+ key: string,
83
+ params?: Readonly<Record<string, string>>
84
+ ) => string;
85
+
86
+ export interface RawTranslateOptions<L extends string> {
87
+ readonly catalog: MergedCatalog<L>;
88
+ readonly locale: L;
89
+ }
90
+
91
+ /**
92
+ * Builds the runtime half of `t()`.
93
+ *
94
+ * Every failure here is a build-time failure by design: a static site should not
95
+ * ship a page with a literal `{name}` in the markup, so a missing message or an
96
+ * unfilled placeholder throws rather than degrading.
97
+ */
98
+ export function createRawTranslate<L extends string>(
99
+ options: RawTranslateOptions<L>
100
+ ): RawTranslate {
101
+ const { catalog, locale } = options;
102
+
103
+ return (key, params) => {
104
+ const entry = catalog[key];
105
+ if (entry === undefined) {
106
+ throw new Error(`Unknown message key "${key}".`);
107
+ }
108
+ const template = entry[locale];
109
+ if (typeof template !== "string") {
110
+ throw new Error(
111
+ `Message "${key}" has no copy for locale "${locale}".`
112
+ );
113
+ }
114
+ return render(
115
+ template,
116
+ `Message "${key}" (${locale})`,
117
+ (name, whole) => {
118
+ // `Object.hasOwn` rather than reading the index straight, for the
119
+ // reason `familyGuard` uses it: `__proto__` is a legal name by the
120
+ // pattern above, so `params["__proto__"]` finds `Object.prototype`
121
+ // on any plain object, is not `undefined`, and would be substituted
122
+ // into the page as "[object Object]" instead of throwing.
123
+ const value =
124
+ params !== undefined && Object.hasOwn(params, name)
125
+ ? params[name]
126
+ : undefined;
127
+ if (value === undefined) {
128
+ throw new Error(
129
+ `Message "${key}" (${locale}) is missing a value for "${whole}".`
130
+ );
131
+ }
132
+ return value;
133
+ }
134
+ );
135
+ };
136
+ }
137
+
138
+ /** The `{placeholders}` message `K` needs from the caller. */
139
+ export type MessageParamsOf<
140
+ Catalog,
141
+ K extends keyof Catalog,
142
+ > = EntryPlaceholders<Catalog[K]>;
143
+
144
+ /**
145
+ * `[key]` for a message with no placeholders, `[key, params]` otherwise.
146
+ *
147
+ * The tuple wrappers around `MessageParamsOf` stop the conditional from
148
+ * distributing, so a union of keys is answered once rather than per member.
149
+ */
150
+ export type TranslateArgsOf<Catalog, K extends StringKeys<Catalog>> = [
151
+ MessageParamsOf<Catalog, K>,
152
+ ] extends [never]
153
+ ? [key: K]
154
+ : [key: K, params: Readonly<Record<MessageParamsOf<Catalog, K>, string>>];
155
+
156
+ export type TranslateFor<Catalog> = <K extends StringKeys<Catalog>>(
157
+ ...args: TranslateArgsOf<Catalog, K>
158
+ ) => string;
159
+
160
+ /**
161
+ * Builds the typed `t()` factory for one catalog type.
162
+ *
163
+ * Curried for the usual reason — `Catalog` must be supplied explicitly while the
164
+ * locale stays inferred, and TypeScript has no partial type-argument inference.
165
+ * `Catalog` is the *base* catalog's type (it carries the literal strings the
166
+ * placeholders are read from); the value passed in is the merged runtime
167
+ * catalog, which only needs to be indexable by key and locale.
168
+ */
169
+ export function createTranslateFactory<Catalog, L extends string>() {
170
+ return function createTranslate(
171
+ catalog: MergedCatalog<L>,
172
+ locale: L
173
+ ): TranslateFor<Catalog> {
174
+ const raw = createRawTranslate({ catalog, locale });
175
+
176
+ function t<K extends StringKeys<Catalog>>(
177
+ ...args: TranslateArgsOf<Catalog, K>
178
+ ): string {
179
+ // The conditional tuple cannot be destructured directly; the runtime
180
+ // shape is always `[key]` or `[key, params]`.
181
+ const [key, params] = args as [
182
+ K,
183
+ Readonly<Record<string, string>>?,
184
+ ];
185
+ return raw(key, params);
186
+ }
187
+
188
+ return t;
189
+ };
190
+ }
package/src/image.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * What a bundled image looks like to this library.
3
+ *
4
+ * Its own module because both the head tags and the structured data need it,
5
+ * and `meta/` already imports from `jsonld/` — leaving it in `meta/` would have
6
+ * the graph depend on the head to describe a picture.
7
+ */
8
+
9
+ /**
10
+ * A bundled image asset.
11
+ *
12
+ * Structurally Astro's `ImageMetadata`, so an imported image assigns directly.
13
+ * `src` matters: a bundler rewrites imported images to hashed paths like
14
+ * `/_astro/hero.abc123.png`, which no convention can predict — it must be read
15
+ * off the asset, never derived.
16
+ */
17
+ export interface ImageAsset {
18
+ readonly src: string;
19
+ /**
20
+ * Required, and always present on a bundler's own image metadata.
21
+ *
22
+ * Crawlers use these to lay out a preview without fetching the image first;
23
+ * omitting them is a slower, flakier preview. An external URL therefore has
24
+ * to state its own size rather than being passed as a bare string.
25
+ */
26
+ readonly width: number;
27
+ readonly height: number;
28
+ readonly format?: string;
29
+ }