@escape-game-over/atlas 0.1.24 → 0.1.26
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 +27 -44
- package/bin/use-project.mjs +18 -13
- package/docs/NOT-BUILT.md +1 -1
- package/docs/client-scripts.md +96 -193
- package/docs/rich-text.md +18 -20
- package/package.json +9 -15
- package/src/analytics/google.ts +6 -6
- package/src/analytics/index.ts +4 -3
- package/src/analytics/tags.ts +13 -58
- package/src/analytics/umami.ts +8 -8
- package/src/astro/AtlasElement.astro +26 -0
- package/src/astro/ConsentBanner.astro +25 -0
- package/src/astro/ConsentElement.astro +67 -0
- package/src/astro/Document.astro +44 -0
- package/src/astro/Image.astro +101 -0
- package/src/astro/MetaTags.astro +30 -40
- package/src/astro/RichText.astro +88 -0
- package/src/astro/Zoom.astro +61 -0
- package/src/astro/client.ts +18 -9
- package/src/astro/consent.ts +20 -0
- package/src/astro/dev-log.ts +8 -14
- package/src/astro/element.ts +76 -112
- package/src/astro/filters-view.ts +48 -64
- package/src/astro/filters.ts +42 -35
- package/src/astro/index.ts +2 -9
- package/src/astro/ref.ts +46 -0
- package/src/astro/site-routes.ts +9 -15
- package/src/config.ts +23 -36
- package/src/content/index.ts +1 -1
- package/src/content/marks.ts +13 -13
- package/src/content/rich.ts +26 -42
- package/src/hours.ts +48 -11
- package/src/i18n/define.ts +14 -74
- package/src/index.ts +40 -57
- package/src/meta/index.ts +7 -13
- package/src/meta/share-image.ts +2 -26
- 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/astro/markup.ts +0 -656
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
|
*
|
package/src/site/create.ts
CHANGED
|
@@ -13,12 +13,12 @@ import type { GeneratedFile } from "../file.ts";
|
|
|
13
13
|
import type { PublicFilePath } from "../files.ts";
|
|
14
14
|
import { type BaseCatalog, mergeCatalog } from "../i18n/define.ts";
|
|
15
15
|
import { createTranslateFactory } from "../i18n/translate.ts";
|
|
16
|
+
import type { ImageAsset } from "../image.ts";
|
|
16
17
|
import { buildLlms, type LlmsItem, type LlmsSection } from "../llms.ts";
|
|
17
18
|
import {
|
|
18
19
|
buildMeta,
|
|
19
20
|
buildNotFoundMeta,
|
|
20
21
|
resolveShareImage,
|
|
21
|
-
type SiteIcon,
|
|
22
22
|
} from "../meta/index.ts";
|
|
23
23
|
import type { ProjectInput } from "../project.ts";
|
|
24
24
|
import {
|
|
@@ -171,9 +171,6 @@ export function createSite<
|
|
|
171
171
|
| undefined,
|
|
172
172
|
};
|
|
173
173
|
|
|
174
|
-
// Declared config, not a call to `llms()`: the head is built page by page
|
|
175
|
-
// and cannot know whether some other module generated the file, whereas
|
|
176
|
-
// stating `llms` in the site config is a promise that one is published.
|
|
177
174
|
const llmsName =
|
|
178
175
|
(config_.llms === false ? undefined : config_.llms?.name) ?? "llms.txt";
|
|
179
176
|
// Annotated, not inferred: a template expression widens to `string` unless
|
|
@@ -291,15 +288,16 @@ export function createSite<
|
|
|
291
288
|
entries.map((entry) => entry.path)
|
|
292
289
|
);
|
|
293
290
|
|
|
294
|
-
function
|
|
291
|
+
function getStaticPaths(): StaticPath<L, RouteId>[] {
|
|
295
292
|
const paths: StaticPath<L, RouteId>[] = entries.map((entry) => ({
|
|
296
293
|
params: {
|
|
297
|
-
|
|
294
|
+
route: entry.path === "/" ? undefined : entry.path.slice(1),
|
|
298
295
|
},
|
|
299
296
|
props: {
|
|
300
297
|
routeId: entry.routeId,
|
|
301
298
|
locale: entry.locale,
|
|
302
299
|
page: entry.page,
|
|
300
|
+
pages: resolved[entry.routeId]?.pages ?? 1,
|
|
303
301
|
},
|
|
304
302
|
}));
|
|
305
303
|
|
|
@@ -362,7 +360,7 @@ export function createSite<
|
|
|
362
360
|
* a bad icon still fails the build, at the first page that renders.
|
|
363
361
|
*/
|
|
364
362
|
let iconChecked = false;
|
|
365
|
-
function checkedIcon():
|
|
363
|
+
function checkedIcon(): ImageAsset {
|
|
366
364
|
if (iconChecked) return project.icon;
|
|
367
365
|
if (project.icon.width !== project.icon.height) {
|
|
368
366
|
throw new Error(
|
|
@@ -795,10 +793,8 @@ export function createSite<
|
|
|
795
793
|
fileUrl,
|
|
796
794
|
breadcrumbFor,
|
|
797
795
|
orphanSegments,
|
|
798
|
-
alternatesFor,
|
|
799
796
|
localeLinksFor,
|
|
800
|
-
|
|
801
|
-
staticPaths,
|
|
797
|
+
getStaticPaths,
|
|
802
798
|
sitemap,
|
|
803
799
|
robots,
|
|
804
800
|
llms,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineSiteConfig,
|
|
3
|
+
type LocalesOf,
|
|
4
|
+
type SiteConfigChecks,
|
|
5
|
+
type SiteConfigShape,
|
|
6
|
+
} from "../config.ts";
|
|
7
|
+
import {
|
|
8
|
+
type BaseCatalog,
|
|
9
|
+
defineMessages,
|
|
10
|
+
type NoExtraLocales,
|
|
11
|
+
type ValidateBase,
|
|
12
|
+
} from "../i18n/define.ts";
|
|
13
|
+
import {
|
|
14
|
+
defineProject,
|
|
15
|
+
type ProjectChecks,
|
|
16
|
+
type ProjectInput,
|
|
17
|
+
type ProjectOverrideChecks,
|
|
18
|
+
} from "../project.ts";
|
|
19
|
+
import {
|
|
20
|
+
defineRoutes,
|
|
21
|
+
type RouteRegistry,
|
|
22
|
+
type ValidateSlugDepth,
|
|
23
|
+
} from "../routes/define.ts";
|
|
24
|
+
import {
|
|
25
|
+
type FamilyMemberInput,
|
|
26
|
+
type FamilyOptions,
|
|
27
|
+
routeFamily,
|
|
28
|
+
} from "../routes/family.ts";
|
|
29
|
+
import { createSite } from "./create.ts";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A site: which languages exist and how URLs are shaped, and everything built
|
|
33
|
+
* on that, as methods — so no other file restates the config.
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* // config/site.config.ts
|
|
37
|
+
* export const atlas = defineSite({ locales: {…}, defaultRouting: {…} });
|
|
38
|
+
*
|
|
39
|
+
* // config/routes.ts, messages.ts, projects/<name>/project.ts, site.ts
|
|
40
|
+
* const rooms = atlas.family("rooms", ROOMS, { enabled: true });
|
|
41
|
+
* export const defaultRoutes = atlas.routes({ ...rooms, home: {…} });
|
|
42
|
+
* export const defaultMessages = atlas.messages({…});
|
|
43
|
+
* export default atlas.project(defaultMessages, defaultRoutes, {…});
|
|
44
|
+
* export const site = atlas.site(defaultMessages, defaultRoutes, project);
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* Every check each step makes is the same one it makes as a function: a typo'd
|
|
48
|
+
* key, a dropped placeholder or a slug at the wrong depth is an error on the
|
|
49
|
+
* line that is wrong.
|
|
50
|
+
*/
|
|
51
|
+
export function defineSite<
|
|
52
|
+
const C extends SiteConfigShape & SiteConfigChecks<C>,
|
|
53
|
+
>(input: C) {
|
|
54
|
+
const config = defineSiteConfig(input as never) as C;
|
|
55
|
+
type L = LocalesOf<C>;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
config,
|
|
59
|
+
|
|
60
|
+
/** A route per member, under one prefix: `rooms/red-room` for `rooms`. */
|
|
61
|
+
family<
|
|
62
|
+
const Prefix extends string,
|
|
63
|
+
const Members extends readonly FamilyMemberInput<L>[],
|
|
64
|
+
const Options extends FamilyOptions<L> = Record<string, never>,
|
|
65
|
+
>(
|
|
66
|
+
prefix: Prefix,
|
|
67
|
+
members: Members,
|
|
68
|
+
options?: Options
|
|
69
|
+
): ReturnType<typeof routeFamily<C, Prefix, Members, Options>> {
|
|
70
|
+
return routeFamily(config, prefix, members, options);
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/** The route table every project starts from. */
|
|
74
|
+
routes<const T extends RouteRegistry<L>>(
|
|
75
|
+
routes: T & ValidateSlugDepth<T>
|
|
76
|
+
): T {
|
|
77
|
+
return defineRoutes(config, routes as never) as T;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/** The copy every project starts from, in every locale. */
|
|
81
|
+
messages<const T extends BaseCatalog<L>>(
|
|
82
|
+
catalog: T & ValidateBase<T> & NoExtraLocales<T, L>
|
|
83
|
+
): T {
|
|
84
|
+
return defineMessages(config, catalog as never) as T;
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/** One deployment: what differs from the defaults. */
|
|
88
|
+
project<
|
|
89
|
+
const Catalog extends BaseCatalog<L>,
|
|
90
|
+
const Routes extends RouteRegistry<L>,
|
|
91
|
+
const T extends ProjectInput<L, Routes> &
|
|
92
|
+
ProjectChecks<L, Catalog, Routes, T>,
|
|
93
|
+
>(
|
|
94
|
+
messages: Catalog,
|
|
95
|
+
routes: Routes,
|
|
96
|
+
project: T & ProjectOverrideChecks<L, Catalog, Routes, T>
|
|
97
|
+
): T {
|
|
98
|
+
return defineProject(
|
|
99
|
+
config,
|
|
100
|
+
messages,
|
|
101
|
+
routes,
|
|
102
|
+
project as never
|
|
103
|
+
) as T;
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
/** The API every page reads, wired for one project. */
|
|
107
|
+
site<
|
|
108
|
+
const Catalog extends BaseCatalog<L>,
|
|
109
|
+
const Routes extends RouteRegistry<L>,
|
|
110
|
+
const Published extends L,
|
|
111
|
+
const P extends ProjectInput<L, Routes>,
|
|
112
|
+
>(
|
|
113
|
+
messages: Catalog,
|
|
114
|
+
routes: Routes,
|
|
115
|
+
project: P & { readonly enabledLocales: readonly Published[] }
|
|
116
|
+
): ReturnType<typeof createSite<C, Catalog, Routes, Published, P>> {
|
|
117
|
+
return createSite(config, messages, routes, project);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
package/src/site/index.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Three files, along the seam between what the API *is* and what builds it:
|
|
5
5
|
*
|
|
6
|
-
* - `api.ts` — the `Site` interface, and
|
|
7
|
-
*
|
|
6
|
+
* - `api.ts` — the `Site` interface, and `PerRoute`, read back off it. The
|
|
7
|
+
* half worth reading.
|
|
8
8
|
* - `page.ts` — one page's vocabulary: what a view is handed and what it
|
|
9
9
|
* returns. Nothing here mentions a route registry.
|
|
10
10
|
* - `create.ts` — `createSite`, the closure that satisfies the interface. Its
|
|
@@ -15,11 +15,8 @@
|
|
|
15
15
|
export type {
|
|
16
16
|
LlmsOptions,
|
|
17
17
|
PerRoute,
|
|
18
|
-
RouteIdOf,
|
|
19
18
|
Site,
|
|
20
|
-
WhenEnabled,
|
|
21
19
|
} from "./api.ts";
|
|
22
|
-
export { createSite } from "./create.ts";
|
|
23
20
|
export type {
|
|
24
21
|
Alternate,
|
|
25
22
|
Crumb,
|
package/src/site/page.ts
CHANGED
|
@@ -40,11 +40,13 @@ export type PageProps<L extends string, Id extends string> = {
|
|
|
40
40
|
* it needs no guard.
|
|
41
41
|
*/
|
|
42
42
|
readonly page: number;
|
|
43
|
+
/** How many pages this route's list runs to; `1` for a route that does not paginate. */
|
|
44
|
+
readonly pages: number;
|
|
43
45
|
};
|
|
44
46
|
|
|
45
|
-
/** One `{ params, props }` pair
|
|
47
|
+
/** One `{ params, props }` pair for `[...route].astro`. */
|
|
46
48
|
export type StaticPath<L extends string, Id extends string> = {
|
|
47
|
-
readonly params:
|
|
49
|
+
readonly params: { readonly route: string | undefined };
|
|
48
50
|
readonly props: PageProps<L, Id>;
|
|
49
51
|
};
|
|
50
52
|
|
package/src/sitemap.ts
CHANGED
|
@@ -38,12 +38,6 @@ export interface SitemapConfig {
|
|
|
38
38
|
readonly entryLimit?: number;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/** One `{ params, props }` pair for a sitemap file, shaped for a static router. */
|
|
42
|
-
export type SitemapStaticPath = {
|
|
43
|
-
readonly params: Readonly<Record<string, string>>;
|
|
44
|
-
readonly props: { readonly file: GeneratedFile };
|
|
45
|
-
};
|
|
46
|
-
|
|
47
41
|
export interface Sitemap {
|
|
48
42
|
/**
|
|
49
43
|
* The file to submit and to reference from `robots.txt`.
|
|
@@ -55,33 +49,6 @@ export interface Sitemap {
|
|
|
55
49
|
readonly entry: GeneratedFile;
|
|
56
50
|
/** Every file that must be served, `entry` included. */
|
|
57
51
|
readonly files: readonly GeneratedFile[];
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Every file to generate, as `{ params, props }` for a dynamic route.
|
|
61
|
-
*
|
|
62
|
-
* `param` is the name of the parameter in the router's filename, e.g.
|
|
63
|
-
* `"sitemap"` for `[sitemap].ts`. One route therefore serves the single
|
|
64
|
-
* sitemap or the index plus its parts, without the caller knowing which
|
|
65
|
-
* case it is in.
|
|
66
|
-
*
|
|
67
|
-
* Returns a fresh mutable array, because SSG routers typically demand one.
|
|
68
|
-
*/
|
|
69
|
-
staticPaths(param: string): SitemapStaticPath[];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function toSitemap(
|
|
73
|
-
entry: GeneratedFile,
|
|
74
|
-
files: readonly GeneratedFile[]
|
|
75
|
-
): Sitemap {
|
|
76
|
-
return {
|
|
77
|
-
entry,
|
|
78
|
-
files,
|
|
79
|
-
staticPaths: (param) =>
|
|
80
|
-
files.map((file) => ({
|
|
81
|
-
params: { [param]: file.name },
|
|
82
|
-
props: { file },
|
|
83
|
-
})),
|
|
84
|
-
};
|
|
85
52
|
}
|
|
86
53
|
|
|
87
54
|
export interface SitemapInput<L extends string, Id extends string> {
|
|
@@ -235,7 +202,7 @@ export function buildSitemap<L extends string, Id extends string>(
|
|
|
235
202
|
|
|
236
203
|
if (chunks.length <= 1) {
|
|
237
204
|
const only = file(input.siteUrl, input.name, urlset(included));
|
|
238
|
-
return
|
|
205
|
+
return { entry: only, files: [only] };
|
|
239
206
|
}
|
|
240
207
|
|
|
241
208
|
const parts = chunks.map((entries, index) =>
|
|
@@ -253,5 +220,5 @@ export function buildSitemap<L extends string, Id extends string>(
|
|
|
253
220
|
)
|
|
254
221
|
);
|
|
255
222
|
|
|
256
|
-
return
|
|
223
|
+
return { entry: index, files: [index, ...parts] };
|
|
257
224
|
}
|