@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/meta/share-image.ts
CHANGED
|
@@ -3,12 +3,16 @@ import { absoluteUrl, type HttpsUrl } from "../url.ts";
|
|
|
3
3
|
import { warn } from "../warn.ts";
|
|
4
4
|
import { link, type MetaTag } from "./tag.ts";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
type ImageFormat = "png" | "jpg" | "jpeg" | "webp" | "avif" | "gif" | "svg";
|
|
7
7
|
|
|
8
8
|
const IMAGE_MIME: Readonly<Record<ImageFormat, string>> = {
|
|
9
9
|
png: "image/png",
|
|
10
10
|
jpg: "image/jpeg",
|
|
11
|
+
jpeg: "image/jpeg",
|
|
11
12
|
webp: "image/webp",
|
|
13
|
+
avif: "image/avif",
|
|
14
|
+
gif: "image/gif",
|
|
15
|
+
svg: "image/svg+xml",
|
|
12
16
|
};
|
|
13
17
|
|
|
14
18
|
/**
|
|
@@ -74,23 +78,6 @@ export interface ShareImage {
|
|
|
74
78
|
readonly alt: string;
|
|
75
79
|
}
|
|
76
80
|
|
|
77
|
-
/**
|
|
78
|
-
* The site's icon: one square asset, used for every icon link.
|
|
79
|
-
*
|
|
80
|
-
* Site-wide rather than per page, because search engines use one favicon per
|
|
81
|
-
* hostname, read from the home page — a per-page icon changes the browser tab
|
|
82
|
-
* and nothing else.
|
|
83
|
-
*
|
|
84
|
-
* One asset rather than a set, because one square PNG already covers every
|
|
85
|
-
* consumer: browsers accept PNG for `rel="icon"`, and Apple scales it for
|
|
86
|
-
* `apple-touch-icon`. A `/favicon.ico` in `public/` is still requested by
|
|
87
|
-
* browsers on its own if you want the legacy fallback.
|
|
88
|
-
*
|
|
89
|
-
* Unlike `og:image`, the href stays as written — root-relative is correct for an
|
|
90
|
-
* icon, and absolutising it would only hard-code the origin.
|
|
91
|
-
*/
|
|
92
|
-
export type SiteIcon = ImageAsset;
|
|
93
|
-
|
|
94
81
|
/** One page's resolved share image. */
|
|
95
82
|
export interface OgImage {
|
|
96
83
|
readonly url: HttpsUrl;
|
|
@@ -218,7 +205,7 @@ export function resolveShareImage(
|
|
|
218
205
|
* already has the page, so root-relative is correct and making it absolute would
|
|
219
206
|
* only hard-code the origin.
|
|
220
207
|
*/
|
|
221
|
-
export function iconLinks(icon:
|
|
208
|
+
export function iconLinks(icon: ImageAsset): MetaTag[] {
|
|
222
209
|
const mime = imageMime(icon.format);
|
|
223
210
|
const attrs = {
|
|
224
211
|
href: icon.src,
|
package/src/meta/tag.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* A head tag as data, not markup.
|
|
5
|
-
*
|
|
6
|
-
* lib does not render HTML — that would tie it to one framework and to one
|
|
7
|
-
* templating syntax. It describes the tags; the consumer spreads them.
|
|
8
|
-
*/
|
|
1
|
+
/** A head tag as data; `AtlasDocument` renders it. */
|
|
9
2
|
export type MetaTag =
|
|
10
3
|
| { readonly kind: "title"; readonly text: string }
|
|
11
4
|
| {
|
|
@@ -68,43 +61,6 @@ export const link = (attrs: Readonly<Record<string, string>>): MetaTag => ({
|
|
|
68
61
|
attrs,
|
|
69
62
|
});
|
|
70
63
|
|
|
71
|
-
/**
|
|
72
|
-
* An analytics tag as a head tag. The two vocabularies overlap but are not the
|
|
73
|
-
* same type: `analytics/` knows nothing of `MetaTag`, which is what keeps this
|
|
74
|
-
* folder free to import it without a cycle.
|
|
75
|
-
*
|
|
76
|
-
* Every kind is named rather than left to a trailing `else`, so that adding one
|
|
77
|
-
* to `AnalyticsTag` and forgetting it here is a *compile* error and not a tag
|
|
78
|
-
* that quietly disappears: `script` narrows to `never` only while the list is
|
|
79
|
-
* complete, and assigning anything else to `never` does not type-check.
|
|
80
|
-
*/
|
|
81
|
-
export function asMetaTag(script: AnalyticsTag): MetaTag {
|
|
82
|
-
if (script.kind === "inline") {
|
|
83
|
-
return { kind: "script", content: script.content };
|
|
84
|
-
}
|
|
85
|
-
if (script.kind === "external") {
|
|
86
|
-
return {
|
|
87
|
-
kind: "externalScript",
|
|
88
|
-
src: script.src,
|
|
89
|
-
attrs: script.attributes,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
if (script.kind === "noscriptFrame") {
|
|
93
|
-
return { kind: "noscriptFrame", src: script.src };
|
|
94
|
-
}
|
|
95
|
-
if (script.kind === "preconnect") {
|
|
96
|
-
// A `link` rather than a kind of its own: the head vocabulary already
|
|
97
|
-
// has the shape, and a preconnect is a link in every sense the renderer
|
|
98
|
-
// cares about. See `AnalyticsTag` for why no `crossorigin`.
|
|
99
|
-
return {
|
|
100
|
-
kind: "link",
|
|
101
|
-
attrs: { rel: "preconnect", href: script.origin },
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
const unhandled: never = script;
|
|
105
|
-
throw new Error(`Unhandled analytics tag: ${JSON.stringify(unhandled)}`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
64
|
const DEFAULT_VIEWPORT = "width=device-width, initial-scale=1";
|
|
109
65
|
|
|
110
66
|
interface Preamble {
|
package/src/money.ts
CHANGED
|
@@ -1,13 +1,168 @@
|
|
|
1
|
-
import type { Letter } from "./types.ts";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* An ISO 4217 currency code: `"EUR"`, `"RON"`, `"GBP"`.
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* A code rather than a symbol — `€`, `lei` — because two currencies can share a
|
|
5
|
+
* symbol, and `priceCurrency` is read as a code.
|
|
6
|
+
*
|
|
7
|
+
* Circulating currencies from ISO 4217 List One (SIX, published 2026-09-17):
|
|
8
|
+
* fund, precious-metal, testing and accounting-unit codes left out.
|
|
9
9
|
*/
|
|
10
|
-
export type CurrencyCode =
|
|
10
|
+
export type CurrencyCode =
|
|
11
|
+
| "AED"
|
|
12
|
+
| "AFN"
|
|
13
|
+
| "ALL"
|
|
14
|
+
| "AMD"
|
|
15
|
+
| "AOA"
|
|
16
|
+
| "ARS"
|
|
17
|
+
| "AUD"
|
|
18
|
+
| "AWG"
|
|
19
|
+
| "AZN"
|
|
20
|
+
| "BAM"
|
|
21
|
+
| "BBD"
|
|
22
|
+
| "BDT"
|
|
23
|
+
| "BHD"
|
|
24
|
+
| "BIF"
|
|
25
|
+
| "BMD"
|
|
26
|
+
| "BND"
|
|
27
|
+
| "BOB"
|
|
28
|
+
| "BRL"
|
|
29
|
+
| "BSD"
|
|
30
|
+
| "BTN"
|
|
31
|
+
| "BWP"
|
|
32
|
+
| "BYN"
|
|
33
|
+
| "BZD"
|
|
34
|
+
| "CAD"
|
|
35
|
+
| "CDF"
|
|
36
|
+
| "CHF"
|
|
37
|
+
| "CLP"
|
|
38
|
+
| "CNY"
|
|
39
|
+
| "COP"
|
|
40
|
+
| "CRC"
|
|
41
|
+
| "CUP"
|
|
42
|
+
| "CVE"
|
|
43
|
+
| "CZK"
|
|
44
|
+
| "DJF"
|
|
45
|
+
| "DKK"
|
|
46
|
+
| "DOP"
|
|
47
|
+
| "DZD"
|
|
48
|
+
| "EGP"
|
|
49
|
+
| "ERN"
|
|
50
|
+
| "ETB"
|
|
51
|
+
| "EUR"
|
|
52
|
+
| "FJD"
|
|
53
|
+
| "FKP"
|
|
54
|
+
| "GBP"
|
|
55
|
+
| "GEL"
|
|
56
|
+
| "GHS"
|
|
57
|
+
| "GIP"
|
|
58
|
+
| "GMD"
|
|
59
|
+
| "GNF"
|
|
60
|
+
| "GTQ"
|
|
61
|
+
| "GYD"
|
|
62
|
+
| "HKD"
|
|
63
|
+
| "HNL"
|
|
64
|
+
| "HTG"
|
|
65
|
+
| "HUF"
|
|
66
|
+
| "IDR"
|
|
67
|
+
| "ILS"
|
|
68
|
+
| "INR"
|
|
69
|
+
| "IQD"
|
|
70
|
+
| "IRR"
|
|
71
|
+
| "ISK"
|
|
72
|
+
| "JMD"
|
|
73
|
+
| "JOD"
|
|
74
|
+
| "JPY"
|
|
75
|
+
| "KES"
|
|
76
|
+
| "KGS"
|
|
77
|
+
| "KHR"
|
|
78
|
+
| "KMF"
|
|
79
|
+
| "KPW"
|
|
80
|
+
| "KRW"
|
|
81
|
+
| "KWD"
|
|
82
|
+
| "KYD"
|
|
83
|
+
| "KZT"
|
|
84
|
+
| "LAK"
|
|
85
|
+
| "LBP"
|
|
86
|
+
| "LKR"
|
|
87
|
+
| "LRD"
|
|
88
|
+
| "LSL"
|
|
89
|
+
| "LYD"
|
|
90
|
+
| "MAD"
|
|
91
|
+
| "MDL"
|
|
92
|
+
| "MGA"
|
|
93
|
+
| "MKD"
|
|
94
|
+
| "MMK"
|
|
95
|
+
| "MNT"
|
|
96
|
+
| "MOP"
|
|
97
|
+
| "MRU"
|
|
98
|
+
| "MUR"
|
|
99
|
+
| "MVR"
|
|
100
|
+
| "MWK"
|
|
101
|
+
| "MXN"
|
|
102
|
+
| "MYR"
|
|
103
|
+
| "MZN"
|
|
104
|
+
| "NAD"
|
|
105
|
+
| "NGN"
|
|
106
|
+
| "NIO"
|
|
107
|
+
| "NOK"
|
|
108
|
+
| "NPR"
|
|
109
|
+
| "NZD"
|
|
110
|
+
| "OMR"
|
|
111
|
+
| "PAB"
|
|
112
|
+
| "PEN"
|
|
113
|
+
| "PGK"
|
|
114
|
+
| "PHP"
|
|
115
|
+
| "PKR"
|
|
116
|
+
| "PLN"
|
|
117
|
+
| "PYG"
|
|
118
|
+
| "QAR"
|
|
119
|
+
| "RON"
|
|
120
|
+
| "RSD"
|
|
121
|
+
| "RUB"
|
|
122
|
+
| "RWF"
|
|
123
|
+
| "SAR"
|
|
124
|
+
| "SBD"
|
|
125
|
+
| "SCR"
|
|
126
|
+
| "SDG"
|
|
127
|
+
| "SEK"
|
|
128
|
+
| "SGD"
|
|
129
|
+
| "SHP"
|
|
130
|
+
| "SLE"
|
|
131
|
+
| "SOS"
|
|
132
|
+
| "SRD"
|
|
133
|
+
| "SSP"
|
|
134
|
+
| "STN"
|
|
135
|
+
| "SVC"
|
|
136
|
+
| "SYP"
|
|
137
|
+
| "SZL"
|
|
138
|
+
| "THB"
|
|
139
|
+
| "TJS"
|
|
140
|
+
| "TMT"
|
|
141
|
+
| "TND"
|
|
142
|
+
| "TOP"
|
|
143
|
+
| "TRY"
|
|
144
|
+
| "TTD"
|
|
145
|
+
| "TWD"
|
|
146
|
+
| "TZS"
|
|
147
|
+
| "UAH"
|
|
148
|
+
| "UGX"
|
|
149
|
+
| "USD"
|
|
150
|
+
| "UYU"
|
|
151
|
+
| "UZS"
|
|
152
|
+
| "VED"
|
|
153
|
+
| "VES"
|
|
154
|
+
| "VND"
|
|
155
|
+
| "VUV"
|
|
156
|
+
| "WST"
|
|
157
|
+
| "XAF"
|
|
158
|
+
| "XCD"
|
|
159
|
+
| "XCG"
|
|
160
|
+
| "XOF"
|
|
161
|
+
| "XPF"
|
|
162
|
+
| "YER"
|
|
163
|
+
| "ZAR"
|
|
164
|
+
| "ZMW"
|
|
165
|
+
| "ZWG";
|
|
11
166
|
|
|
12
167
|
/**
|
|
13
168
|
* An amount and what it is denominated in, kept together.
|
package/src/project.ts
CHANGED
|
@@ -8,16 +8,15 @@ import type {
|
|
|
8
8
|
} from "./config.ts";
|
|
9
9
|
import type {
|
|
10
10
|
BaseCatalog,
|
|
11
|
-
MessageOverlayKeys,
|
|
12
11
|
OverrideCatalog,
|
|
13
12
|
ValidateOverrideCatalog,
|
|
14
13
|
} from "./i18n/define.ts";
|
|
15
|
-
import type {
|
|
14
|
+
import type { ImageAsset } from "./image.ts";
|
|
15
|
+
import type { SiteVerification, ThemeColor } from "./meta/index.ts";
|
|
16
16
|
import type {
|
|
17
|
-
|
|
18
|
-
RouteOverlayShape,
|
|
19
|
-
RouteOverrideMap,
|
|
17
|
+
RouteOverrides,
|
|
20
18
|
RouteRegistry,
|
|
19
|
+
ValidateOverrideSlugDepth,
|
|
21
20
|
} from "./routes/define.ts";
|
|
22
21
|
import type { NoExcessKeys, StringKeys } from "./types.ts";
|
|
23
22
|
import type { HttpsUrl, ValidSiteUrl } from "./url.ts";
|
|
@@ -60,7 +59,7 @@ export interface ProjectInput<L extends string, Routes> {
|
|
|
60
59
|
* from the home page, so a per-page icon would change the browser tab and
|
|
61
60
|
* nothing else.
|
|
62
61
|
*/
|
|
63
|
-
readonly icon:
|
|
62
|
+
readonly icon: ImageAsset;
|
|
64
63
|
/**
|
|
65
64
|
* Ownership tokens from Search Console, Bing Webmaster Tools and the like.
|
|
66
65
|
*
|
|
@@ -131,7 +130,7 @@ export interface ProjectInput<L extends string, Routes> {
|
|
|
131
130
|
*/
|
|
132
131
|
readonly overrideMessages: OverrideCatalog<L>;
|
|
133
132
|
/** Slug retranslations and page on/off switches. See `overrideMessages`. */
|
|
134
|
-
readonly overrideRoutes:
|
|
133
|
+
readonly overrideRoutes: RouteOverrides<Routes, L>;
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
/**
|
|
@@ -148,67 +147,88 @@ export interface ProjectInput<L extends string, Routes> {
|
|
|
148
147
|
* - unknown top-level fields are rejected, so a typo cannot sit there unread;
|
|
149
148
|
* - locales named anywhere must be ones the site ships and this project enabled.
|
|
150
149
|
*/
|
|
150
|
+
/** Everything a project file must satisfy; see `defineSite`. */
|
|
151
|
+
export type ProjectChecks<
|
|
152
|
+
L extends string,
|
|
153
|
+
Catalog,
|
|
154
|
+
Routes,
|
|
155
|
+
T extends ProjectInput<L, Routes>,
|
|
156
|
+
> = NoExcessKeys<T, keyof ProjectInput<L, Routes>> & {
|
|
157
|
+
readonly url: ValidSiteUrl<T["url"]>;
|
|
158
|
+
readonly overrideRouting?: {
|
|
159
|
+
readonly defaultLocale?: T["enabledLocales"][number];
|
|
160
|
+
// Against `enabledLocales` rather than the site's list: a
|
|
161
|
+
// segment spelled for a language this deployment does not
|
|
162
|
+
// publish is a word that appears in no URL it builds. Mapped
|
|
163
|
+
// over the keys written, for the reason `overrideLocaleMeta`
|
|
164
|
+
// below is.
|
|
165
|
+
readonly pageSegmentByLocale?: {
|
|
166
|
+
readonly [K in StringKeys<
|
|
167
|
+
NonNullable<T["overrideRouting"]>["pageSegmentByLocale"]
|
|
168
|
+
>]: K extends T["enabledLocales"][number]
|
|
169
|
+
? string
|
|
170
|
+
: LocaleNotEnabled<K>;
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
// Two separate checks on the same field: the keys must be messages
|
|
174
|
+
// the default catalog declares, and the locales inside each must be
|
|
175
|
+
// ones this project publishes. Copy written for a locale left out of
|
|
176
|
+
// `enabledLocales` is never built, so it is always a mistake — a
|
|
177
|
+
// locale removed from the list, or a project copied from another.
|
|
178
|
+
readonly overrideMessages: NoExcessKeys<
|
|
179
|
+
T["overrideMessages"],
|
|
180
|
+
StringKeys<Catalog>
|
|
181
|
+
> & {
|
|
182
|
+
readonly [K in StringKeys<T["overrideMessages"]>]: {
|
|
183
|
+
readonly [L in StringKeys<
|
|
184
|
+
T["overrideMessages"][K]
|
|
185
|
+
>]: L extends T["enabledLocales"][number]
|
|
186
|
+
? T["overrideMessages"][K][L]
|
|
187
|
+
: LocaleNotEnabled<L>;
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
readonly overrideRoutes: NoExcessKeys<
|
|
191
|
+
T["overrideRoutes"],
|
|
192
|
+
StringKeys<Routes>
|
|
193
|
+
>;
|
|
194
|
+
// Nested, so that `dir` is rejected inside each locale's entry.
|
|
195
|
+
// Declaring the field as `Pick<LocaleMeta, "label">` is not enough:
|
|
196
|
+
// `T` is *inferred* from the literal, so a stray `dir` just becomes
|
|
197
|
+
// part of `T` and there is no fresh object for excess-property
|
|
198
|
+
// checking to fire on.
|
|
199
|
+
readonly overrideLocaleMeta?: {
|
|
200
|
+
readonly [K in StringKeys<
|
|
201
|
+
T["overrideLocaleMeta"]
|
|
202
|
+
>]: K extends T["enabledLocales"][number]
|
|
203
|
+
? NoExcessKeys<T["overrideLocaleMeta"][K], keyof LocaleMetaOverride>
|
|
204
|
+
: LocaleNotEnabled<K>;
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/** The same checks that map the whole override objects, in parameter position. */
|
|
209
|
+
export type ProjectOverrideChecks<
|
|
210
|
+
L extends string,
|
|
211
|
+
Catalog,
|
|
212
|
+
Routes,
|
|
213
|
+
T extends ProjectInput<L, Routes>,
|
|
214
|
+
> = {
|
|
215
|
+
readonly overrideMessages: ValidateOverrideCatalog<
|
|
216
|
+
T["overrideMessages"],
|
|
217
|
+
Catalog,
|
|
218
|
+
L
|
|
219
|
+
>;
|
|
220
|
+
readonly overrideRoutes: ValidateOverrideSlugDepth<
|
|
221
|
+
Routes,
|
|
222
|
+
T["overrideRoutes"]
|
|
223
|
+
>;
|
|
224
|
+
};
|
|
225
|
+
|
|
151
226
|
export function defineProject<
|
|
152
227
|
const C extends SiteConfigShape,
|
|
153
228
|
const Catalog extends BaseCatalog<LocalesOf<C>>,
|
|
154
229
|
const Routes extends RouteRegistry<LocalesOf<C>>,
|
|
155
230
|
const T extends ProjectInput<LocalesOf<C>, Routes> &
|
|
156
|
-
|
|
157
|
-
readonly url: ValidSiteUrl<T["url"]>;
|
|
158
|
-
readonly overrideRouting?: {
|
|
159
|
-
readonly defaultLocale?: T["enabledLocales"][number];
|
|
160
|
-
// Against `enabledLocales` rather than the site's list: a
|
|
161
|
-
// segment spelled for a language this deployment does not
|
|
162
|
-
// publish is a word that appears in no URL it builds. Mapped
|
|
163
|
-
// over the keys written, for the reason `overrideLocaleMeta`
|
|
164
|
-
// below is.
|
|
165
|
-
readonly pageSegmentByLocale?: {
|
|
166
|
-
readonly [K in StringKeys<
|
|
167
|
-
NonNullable<T["overrideRouting"]>["pageSegmentByLocale"]
|
|
168
|
-
>]: K extends T["enabledLocales"][number]
|
|
169
|
-
? string
|
|
170
|
-
: LocaleNotEnabled<K>;
|
|
171
|
-
};
|
|
172
|
-
};
|
|
173
|
-
// Two separate checks on the same field: the keys must be messages
|
|
174
|
-
// the default catalog declares, and the locales inside each must be
|
|
175
|
-
// ones this project publishes. Copy written for a locale left out of
|
|
176
|
-
// `enabledLocales` is never built, so it is always a mistake — a
|
|
177
|
-
// locale removed from the list, or a project copied from another.
|
|
178
|
-
readonly overrideMessages: MessageOverlayKeys<
|
|
179
|
-
Catalog,
|
|
180
|
-
T["overrideMessages"]
|
|
181
|
-
> & {
|
|
182
|
-
readonly [K in StringKeys<T["overrideMessages"]>]: {
|
|
183
|
-
readonly [L in StringKeys<
|
|
184
|
-
T["overrideMessages"][K]
|
|
185
|
-
>]: L extends T["enabledLocales"][number]
|
|
186
|
-
? T["overrideMessages"][K][L]
|
|
187
|
-
: LocaleNotEnabled<L>;
|
|
188
|
-
};
|
|
189
|
-
};
|
|
190
|
-
// Shared with `defineRouteOverrides` rather than restated — see
|
|
191
|
-
// `RouteOverlayKeys` for why the two must not drift.
|
|
192
|
-
readonly overrideRoutes: RouteOverlayKeys<
|
|
193
|
-
Routes,
|
|
194
|
-
T["overrideRoutes"]
|
|
195
|
-
>;
|
|
196
|
-
// Nested, so that `dir` is rejected inside each locale's entry.
|
|
197
|
-
// Declaring the field as `Pick<LocaleMeta, "label">` is not enough:
|
|
198
|
-
// `T` is *inferred* from the literal, so a stray `dir` just becomes
|
|
199
|
-
// part of `T` and there is no fresh object for excess-property
|
|
200
|
-
// checking to fire on.
|
|
201
|
-
readonly overrideLocaleMeta?: {
|
|
202
|
-
readonly [K in StringKeys<
|
|
203
|
-
T["overrideLocaleMeta"]
|
|
204
|
-
>]: K extends T["enabledLocales"][number]
|
|
205
|
-
? NoExcessKeys<
|
|
206
|
-
T["overrideLocaleMeta"][K],
|
|
207
|
-
keyof LocaleMetaOverride
|
|
208
|
-
>
|
|
209
|
-
: LocaleNotEnabled<K>;
|
|
210
|
-
};
|
|
211
|
-
},
|
|
231
|
+
ProjectChecks<LocalesOf<C>, Catalog, Routes, T>,
|
|
212
232
|
>(
|
|
213
233
|
// The first three are read for their types: they are how the locale union,
|
|
214
234
|
// the message keys and the route ids are inferred without type arguments.
|
|
@@ -220,16 +240,7 @@ export function defineProject<
|
|
|
220
240
|
// type in a constraint makes one unrelated mistake fail the argument
|
|
221
241
|
// outright — TypeScript then reports at this call instead of on the line
|
|
222
242
|
// that is wrong.
|
|
223
|
-
project: T &
|
|
224
|
-
readonly overrideMessages: ValidateOverrideCatalog<
|
|
225
|
-
T["overrideMessages"],
|
|
226
|
-
Catalog,
|
|
227
|
-
LocalesOf<C>
|
|
228
|
-
>;
|
|
229
|
-
// The same alias `defineRouteOverrides` uses, so writing the overlay
|
|
230
|
-
// inline is never the weaker option.
|
|
231
|
-
readonly overrideRoutes: RouteOverlayShape<Routes, T["overrideRoutes"]>;
|
|
232
|
-
}
|
|
243
|
+
project: T & ProjectOverrideChecks<LocalesOf<C>, Catalog, Routes, T>
|
|
233
244
|
): T {
|
|
234
245
|
// Checked at runtime as well as in the type, because the type is not always
|
|
235
246
|
// what fails first. An Astro config is *evaluated* before `astro check`
|
package/src/routes/define.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LocalesOf, SiteConfigShape } from "../config.ts";
|
|
2
|
-
import type { IsNever, IsoDate,
|
|
2
|
+
import type { IsNever, IsoDate, StringKeys } from "../types.ts";
|
|
3
3
|
|
|
4
4
|
export type ChangeFreq =
|
|
5
5
|
| "always"
|
|
@@ -92,7 +92,7 @@ interface SlugDepthMismatch<Id extends string, L extends string> {
|
|
|
92
92
|
* here: in parameter position one bad slug widens `T` and every *other* route
|
|
93
93
|
* then reports as malformed.
|
|
94
94
|
*/
|
|
95
|
-
type ValidateSlugDepth<T> = {
|
|
95
|
+
export type ValidateSlugDepth<T> = {
|
|
96
96
|
readonly [K in StringKeys<T>]: {
|
|
97
97
|
// Every key of the route is carried through unchanged, and only
|
|
98
98
|
// `slugByLocale` is rewritten. Mapping the whole shape rather than
|
|
@@ -149,8 +149,12 @@ export interface RouteOverride<L extends string> {
|
|
|
149
149
|
readonly sitemap?: RouteSitemap;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
/**
|
|
153
|
+
* One project's route overrides, for an overlay kept in its own file:
|
|
154
|
+
* `{ … } as const satisfies RouteOverrides<typeof defaultRoutes, EnabledLocale>`.
|
|
155
|
+
*/
|
|
156
|
+
export type RouteOverrides<Routes, L extends string> = Readonly<
|
|
157
|
+
Partial<Record<StringKeys<Routes>, RouteOverride<L>>>
|
|
154
158
|
>;
|
|
155
159
|
|
|
156
160
|
/** The slug a registry declares for route `K`, if it declares one. */
|
|
@@ -253,46 +257,6 @@ export type ValidateOverrideSlugDepth<Base, T> = {
|
|
|
253
257
|
};
|
|
254
258
|
};
|
|
255
259
|
|
|
256
|
-
/**
|
|
257
|
-
* Everything a route overlay must satisfy — and the only place it is stated.
|
|
258
|
-
*
|
|
259
|
-
* Two entry points accept an overlay: `defineRouteOverrides`, for one written in
|
|
260
|
-
* its own file, and `defineProject`, for one written inline. They must check it
|
|
261
|
-
* identically, or whichever has the extra check quietly becomes the safer place
|
|
262
|
-
* to write overrides. That is not hypothetical: the depth rule below lived in
|
|
263
|
-
* `defineRouteOverrides` alone for a while, and the inline form was weaker for
|
|
264
|
-
* it with nothing to say so.
|
|
265
|
-
*
|
|
266
|
-
* **Add new overlay checks here, never at a call site.** Both entry points name
|
|
267
|
-
* these two aliases, so anything added reaches both.
|
|
268
|
-
*
|
|
269
|
-
* Split in two because the positions are not interchangeable. Excess keys belong
|
|
270
|
-
* in the *constraint*, where a wrong id reports on that key. The depth rule
|
|
271
|
-
* belongs in the *parameter*: it maps the whole object, and a whole-object
|
|
272
|
-
* mapped type in constraint position makes one unrelated mistake fail the
|
|
273
|
-
* argument outright, moving the error off the offending line.
|
|
274
|
-
*/
|
|
275
|
-
export type RouteOverlayKeys<Base, T> = NoExcessKeys<T, StringKeys<Base>>;
|
|
276
|
-
|
|
277
|
-
/** @see {@link RouteOverlayKeys} — the parameter-position half. */
|
|
278
|
-
export type RouteOverlayShape<Base, T> = ValidateOverrideSlugDepth<Base, T>;
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Declares a project's route overlay: retranslated slugs and on/off switches.
|
|
282
|
-
*
|
|
283
|
-
* Enforced at compile time: unknown route ids are rejected, a slug override
|
|
284
|
-
* cannot invent a locale the site does not ship, and no override may leave a
|
|
285
|
-
* route sitting at different depths in different languages.
|
|
286
|
-
*/
|
|
287
|
-
export function defineRouteOverrides<
|
|
288
|
-
const C extends SiteConfigShape,
|
|
289
|
-
const Base extends RouteRegistry<LocalesOf<C>>,
|
|
290
|
-
const T extends RouteOverrideMap<LocalesOf<C>, Base> &
|
|
291
|
-
RouteOverlayKeys<Base, T>,
|
|
292
|
-
>(_config: C, _base: Base, overrides: T & RouteOverlayShape<Base, T>): T {
|
|
293
|
-
return overrides;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
260
|
/** The minimum `EnabledRouteIdFor` needs to know about a registry. */
|
|
297
261
|
export type EnabledFlagSource = Readonly<
|
|
298
262
|
Record<string, { readonly enabled: boolean }>
|
package/src/routes/resolve.ts
CHANGED
|
@@ -238,7 +238,7 @@ export function listRouteEntries<L extends string, Id extends string>(
|
|
|
238
238
|
// until the day someone turns the page on.
|
|
239
239
|
//
|
|
240
240
|
// A backstop rather than the main check: `defineRoutes` and
|
|
241
|
-
// `
|
|
241
|
+
// `defineProject` both reject this at compile time. What survives
|
|
242
242
|
// to here is a slug that was typed as plain `string` — computed, or read
|
|
243
243
|
// from an env var — where the literal comparison passed vacuously.
|
|
244
244
|
const depth = segmentCount(route.slug);
|
package/src/site/api.ts
CHANGED
|
@@ -11,11 +11,9 @@ import type {
|
|
|
11
11
|
ValidateRedirectTargets,
|
|
12
12
|
} from "../redirects.ts";
|
|
13
13
|
import type { RobotsGroup } from "../robots.ts";
|
|
14
|
-
import type { RouteEntry } from "../routes/resolve.ts";
|
|
15
14
|
import type { Sitemap } from "../sitemap.ts";
|
|
16
15
|
import type { HttpsUrl, UrlPath } from "../url.ts";
|
|
17
16
|
import type {
|
|
18
|
-
Alternate,
|
|
19
17
|
Crumb,
|
|
20
18
|
LinkOptions,
|
|
21
19
|
LocaleLink,
|
|
@@ -209,11 +207,8 @@ export interface Site<
|
|
|
209
207
|
*/
|
|
210
208
|
fileUrl(path: PublicFilePath): HttpsUrl;
|
|
211
209
|
|
|
212
|
-
/** One entry per published locale, for `hreflang` tags. */
|
|
213
|
-
alternatesFor(id: RouteId, options?: LinkOptions): readonly Alternate<L>[];
|
|
214
|
-
|
|
215
210
|
/**
|
|
216
|
-
*
|
|
211
|
+
* One link per published locale, ready to render as a language switcher: each locale's own label
|
|
217
212
|
* and writing direction, and which one the reader is on.
|
|
218
213
|
*
|
|
219
214
|
* Switching language keeps the page — `about` stays `about`, so `/about-us`
|
|
@@ -257,15 +252,10 @@ export interface Site<
|
|
|
257
252
|
*/
|
|
258
253
|
readonly orphanSegments: readonly string[];
|
|
259
254
|
|
|
260
|
-
/** Every (route, locale) pair this build emits. Throws on colliding URLs. */
|
|
261
|
-
readonly entries: readonly RouteEntry<L, RouteId>[];
|
|
262
|
-
|
|
263
255
|
/**
|
|
264
|
-
* Every page to generate,
|
|
265
|
-
*
|
|
266
|
-
* `
|
|
267
|
-
* `"route"` for `[...route].astro`. The root path is emitted as `undefined`,
|
|
268
|
-
* which is what such routers expect.
|
|
256
|
+
* Every page to generate, for `src/pages/[...route].astro`:
|
|
257
|
+
* `export const getStaticPaths = site.getStaticPaths`. The root path is
|
|
258
|
+
* emitted as `undefined`, which is what a rest parameter expects.
|
|
269
259
|
*
|
|
270
260
|
* When every locale is prefixed nothing owns `/`, and no page is generated
|
|
271
261
|
* for it: `redirects()` claims it with a 301 to the default locale's root
|
|
@@ -275,7 +265,7 @@ export interface Site<
|
|
|
275
265
|
*
|
|
276
266
|
* Returns a fresh mutable array, because SSG routers typically demand one.
|
|
277
267
|
*/
|
|
278
|
-
|
|
268
|
+
getStaticPaths(): StaticPath<L, RouteId>[];
|
|
279
269
|
|
|
280
270
|
/**
|
|
281
271
|
* The rendered sitemap: `entry` is the file to submit, `files` is everything
|
|
@@ -291,14 +281,7 @@ export interface Site<
|
|
|
291
281
|
*/
|
|
292
282
|
robots(groups?: readonly RobotsGroup[]): GeneratedFile;
|
|
293
283
|
|
|
294
|
-
/**
|
|
295
|
-
* Where this site's `llms.txt` is published, or `undefined` if the config
|
|
296
|
-
* turned it off.
|
|
297
|
-
*
|
|
298
|
-
* Public because it is a promise made in two places at once: every page's
|
|
299
|
-
* head links to this URL, and something has to write a file there. It is
|
|
300
|
-
* what `siteRoutes` checks so the two cannot disagree.
|
|
301
|
-
*/
|
|
284
|
+
/** Where this site's `llms.txt` is published, or `undefined` when the config turned it off. */
|
|
302
285
|
readonly llmsUrl: HttpsUrl | undefined;
|
|
303
286
|
|
|
304
287
|
/**
|
|
@@ -364,18 +347,9 @@ export interface Site<
|
|
|
364
347
|
}
|
|
365
348
|
|
|
366
349
|
/** The routes a given site builds, read back off the site's own type. */
|
|
367
|
-
|
|
350
|
+
type RouteIdOf<S> =
|
|
368
351
|
S extends Site<infer _L, infer _Catalog, infer Id> ? Id : never;
|
|
369
352
|
|
|
370
|
-
/**
|
|
371
|
-
* `T` when this site builds route `Id`, `never` when it does not.
|
|
372
|
-
*
|
|
373
|
-
* For a value that only makes sense alongside an optional page — a field that
|
|
374
|
-
* should vanish rather than sit there unread once the page is switched off.
|
|
375
|
-
*/
|
|
376
|
-
export type WhenEnabled<S, Id extends string, T> =
|
|
377
|
-
Id extends RouteIdOf<S> ? T : never;
|
|
378
|
-
|
|
379
353
|
/**
|
|
380
354
|
* A `T` for every route this site builds — and only those.
|
|
381
355
|
*
|