@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.
Files changed (50) hide show
  1. package/README.md +27 -44
  2. package/bin/use-project.mjs +18 -13
  3. package/docs/NOT-BUILT.md +1 -1
  4. package/docs/client-scripts.md +96 -193
  5. package/docs/rich-text.md +18 -20
  6. package/package.json +9 -15
  7. package/src/analytics/google.ts +6 -6
  8. package/src/analytics/index.ts +4 -3
  9. package/src/analytics/tags.ts +13 -58
  10. package/src/analytics/umami.ts +8 -8
  11. package/src/astro/AtlasElement.astro +26 -0
  12. package/src/astro/ConsentBanner.astro +25 -0
  13. package/src/astro/ConsentElement.astro +67 -0
  14. package/src/astro/Document.astro +44 -0
  15. package/src/astro/Image.astro +101 -0
  16. package/src/astro/MetaTags.astro +30 -40
  17. package/src/astro/RichText.astro +88 -0
  18. package/src/astro/Zoom.astro +61 -0
  19. package/src/astro/client.ts +18 -9
  20. package/src/astro/consent.ts +20 -0
  21. package/src/astro/dev-log.ts +8 -14
  22. package/src/astro/element.ts +76 -112
  23. package/src/astro/filters-view.ts +48 -64
  24. package/src/astro/filters.ts +42 -35
  25. package/src/astro/index.ts +2 -9
  26. package/src/astro/ref.ts +46 -0
  27. package/src/astro/site-routes.ts +9 -15
  28. package/src/config.ts +23 -36
  29. package/src/content/index.ts +1 -1
  30. package/src/content/marks.ts +13 -13
  31. package/src/content/rich.ts +26 -42
  32. package/src/hours.ts +48 -11
  33. package/src/i18n/define.ts +14 -74
  34. package/src/index.ts +40 -57
  35. package/src/meta/index.ts +7 -13
  36. package/src/meta/share-image.ts +2 -26
  37. package/src/meta/tag.ts +1 -45
  38. package/src/money.ts +161 -6
  39. package/src/project.ts +84 -73
  40. package/src/routes/define.ts +8 -44
  41. package/src/routes/resolve.ts +1 -1
  42. package/src/site/api.ts +7 -33
  43. package/src/site/create.ts +6 -10
  44. package/src/site/define.ts +120 -0
  45. package/src/site/index.ts +2 -5
  46. package/src/site/page.ts +4 -2
  47. package/src/sitemap.ts +2 -35
  48. package/src/warn.ts +16 -17
  49. package/src/astro/dom.ts +0 -35
  50. 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 { SiteIcon, SiteVerification, ThemeColor } from "./meta/index.ts";
14
+ import type { ImageAsset } from "./image.ts";
15
+ import type { SiteVerification, ThemeColor } from "./meta/index.ts";
16
16
  import type {
17
- RouteOverlayKeys,
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: SiteIcon;
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: RouteOverrideMap<L, Routes>;
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
- NoExcessKeys<T, keyof ProjectInput<LocalesOf<C>, 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: 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`
@@ -1,5 +1,5 @@
1
1
  import type { LocalesOf, SiteConfigShape } from "../config.ts";
2
- import type { IsNever, IsoDate, NoExcessKeys, StringKeys } from "../types.ts";
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
- export type RouteOverrideMap<L extends string, Base> = Readonly<
153
- Partial<Record<StringKeys<Base>, RouteOverride<L>>>
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 }>
@@ -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
- // `defineRouteOverrides` both reject this at compile time. What survives
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
- * The same, ready to render as a language switcher: each locale's own label
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, as `{ params, props }` for a catch-all route.
265
- *
266
- * `param` is the name of the rest parameter in the router's filename, e.g.
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
- staticPaths(param: string): StaticPath<L, RouteId>[];
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
- export type RouteIdOf<S> =
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
  *
@@ -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 staticPaths(param: string): StaticPath<L, RouteId>[] {
291
+ function getStaticPaths(): StaticPath<L, RouteId>[] {
295
292
  const paths: StaticPath<L, RouteId>[] = entries.map((entry) => ({
296
293
  params: {
297
- [param]: entry.path === "/" ? undefined : entry.path.slice(1),
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(): SiteIcon {
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
- entries,
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 the types read back off it
7
- * (`RouteIdOf`, `PerRoute`, `WhenEnabled`). The half worth reading.
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, shaped for a static-site router. */
47
+ /** One `{ params, props }` pair for `[...route].astro`. */
46
48
  export type StaticPath<L extends string, Id extends string> = {
47
- readonly params: Readonly<Record<string, string | undefined>>;
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 toSitemap(only, [only]);
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 toSitemap(index, [index, ...parts]);
223
+ return { entry: index, files: [index, ...parts] };
257
224
  }