@escape-game-over/atlas 0.1.115 → 0.1.117

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 (62) hide show
  1. package/README.md +1 -1
  2. package/docs/NOT-BUILT.md +2 -2
  3. package/package.json +2 -2
  4. package/src/analytics/ids.ts +1 -13
  5. package/src/analytics/index.ts +18 -18
  6. package/src/analytics/storage.ts +34 -39
  7. package/src/analytics/vendor-settings.ts +162 -0
  8. package/src/astro/axon-event.ts +2 -4
  9. package/src/astro/axon-pixel.ts +3 -6
  10. package/src/astro/clarity-event.ts +2 -4
  11. package/src/astro/clarity.ts +3 -6
  12. package/src/astro/consent.ts +3 -6
  13. package/src/astro/drip-event.ts +2 -4
  14. package/src/astro/drip.ts +2 -7
  15. package/src/astro/escape-navigator-consent.ts +2 -6
  16. package/src/astro/escapeall-calendar.ts +2 -5
  17. package/src/astro/google-event.ts +3 -7
  18. package/src/astro/google-map.ts +3 -5
  19. package/src/astro/images.ts +12 -26
  20. package/src/astro/index.ts +2 -9
  21. package/src/astro/meta-event.ts +2 -4
  22. package/src/astro/meta-pixel.ts +4 -8
  23. package/src/astro/public-files.ts +11 -52
  24. package/src/astro/site-routes.ts +11 -55
  25. package/src/astro/snap-event.ts +2 -4
  26. package/src/astro/snap-pixel.ts +3 -6
  27. package/src/astro/tiktok-event.ts +2 -4
  28. package/src/astro/tiktok-pixel.ts +70 -39
  29. package/src/astro/umami-event.ts +2 -6
  30. package/src/astro/vendors.ts +64 -0
  31. package/src/config.ts +8 -17
  32. package/src/content/rich.ts +15 -18
  33. package/src/i18n/catalog.ts +20 -1
  34. package/src/i18n/define.ts +0 -5
  35. package/src/i18n/translate.ts +9 -48
  36. package/src/index.ts +24 -166
  37. package/src/jsonld/article.ts +7 -17
  38. package/src/jsonld/business.ts +27 -28
  39. package/src/jsonld/creative-work.ts +8 -20
  40. package/src/jsonld/node.ts +37 -7
  41. package/src/jsonld/organization.ts +21 -30
  42. package/src/jsonld/place.ts +2 -6
  43. package/src/jsonld/product.ts +16 -22
  44. package/src/jsonld/service.ts +21 -37
  45. package/src/jsonld/video.ts +4 -8
  46. package/src/jsonld/website.ts +3 -5
  47. package/src/meta/content.ts +14 -66
  48. package/src/meta/index.ts +10 -32
  49. package/src/project.ts +4 -18
  50. package/src/routes/define.ts +0 -13
  51. package/src/routes/family.ts +135 -3
  52. package/src/site/api.ts +1 -21
  53. package/src/site/create.ts +17 -26
  54. package/src/site/define.ts +13 -9
  55. package/src/site/page.ts +1 -10
  56. package/src/warn.ts +6 -6
  57. package/src/analytics/axon-pixel.ts +0 -24
  58. package/src/analytics/clarity.ts +0 -29
  59. package/src/analytics/drip.ts +0 -20
  60. package/src/analytics/meta-pixel.ts +0 -29
  61. package/src/analytics/snap-pixel.ts +0 -28
  62. package/src/analytics/tiktok-pixel.ts +0 -29
package/src/meta/index.ts CHANGED
@@ -37,7 +37,7 @@ import {
37
37
  serializeJsonLd,
38
38
  } from "../jsonld/index.ts";
39
39
  import type { HttpsUrl } from "../url.ts";
40
- import type { MetaContentBase, PageKind, ThemeColor } from "./content.ts";
40
+ import type { MetaContent, ThemeColor } from "./content.ts";
41
41
  import { robotsContent, warnAboutRobots } from "./robots.ts";
42
42
  import {
43
43
  type OgImage,
@@ -47,13 +47,7 @@ import {
47
47
  import { link, type MetaTag, meta, preamble } from "./tag.ts";
48
48
  import { type SiteVerification, verificationTags } from "./verification.ts";
49
49
 
50
- export type {
51
- ArticleContent,
52
- MetaContent,
53
- OpenGraphType,
54
- PageKind,
55
- ThemeColor,
56
- } from "./content.ts";
50
+ export type { ArticleContent, MetaContent, ThemeColor } from "./content.ts";
57
51
  export type { RobotsDirective, RobotsPolicy } from "./robots.ts";
58
52
  export {
59
53
  fitDescription,
@@ -66,15 +60,8 @@ export {
66
60
  export type { MetaTag } from "./tag.ts";
67
61
  export type { SiteVerification } from "./verification.ts";
68
62
 
69
- /**
70
- * Everything the head needs: what the page supplies, plus what lib derives.
71
- *
72
- * `PageKind` is intersected rather than inherited so that `Omit` applies to the
73
- * base alone: omitting a key from a union would flatten it and lose the
74
- * exclusivity that union exists for.
75
- */
76
- export type MetaInput<L extends string> = Omit<MetaContentBase, "image"> &
77
- PageKind &
63
+ /** Everything the head needs: what the page supplies, plus what lib derives. */
64
+ export type MetaInput<L extends string> = Omit<MetaContent, "image"> &
78
65
  MetaInputDerived<L>;
79
66
 
80
67
  interface MetaInputDerived<L extends string> extends ChromeInput {
@@ -90,14 +77,9 @@ interface MetaInputDerived<L extends string> extends ChromeInput {
90
77
  /** The share image, already resolved against the site origin. */
91
78
  readonly image: OgImage;
92
79
  /**
93
- * `ArticleContent.images`, already resolved against the site origin.
94
- *
95
- * A sibling of `article` rather than a field inside it, because `PageKind`
96
- * is a union and `Omit` on a union flattens it — which is the whole reason
97
- * `MetaInput` intersects that union separately instead of omitting through
98
- * it. So the resolved form arrives beside the page's own, exactly as
99
- * `image` does: a page states `ShareImage`, lib hands `buildMeta` an
100
- * `OgImage`.
80
+ * `ArticleContent.images`, already resolved against the site origin: the
81
+ * page states its crops, as it states `image`, and lib hands `buildMeta`
82
+ * their absolute URLs beside them.
101
83
  */
102
84
  readonly articleImages?: readonly HttpsUrl[];
103
85
  /**
@@ -288,16 +270,12 @@ export function buildMeta<L extends string>(input: MetaInput<L>): DocumentTags {
288
270
 
289
271
  // Open Graph
290
272
  //
291
- // The type follows the content rather than being stated beside it: a page
292
- // that described an article and called itself a website would be two claims
293
- // about one thing, and only one of them could be right.
273
+ // The type follows the content rather than being stated beside it: an
274
+ // article is a page that describes one.
294
275
  tags.push(
295
276
  meta({
296
277
  property: "og:type",
297
- content:
298
- input.article === undefined
299
- ? (input.type ?? "website")
300
- : "article",
278
+ content: input.article === undefined ? "website" : "article",
301
279
  })
302
280
  );
303
281
  tags.push(meta({ property: "og:title", content: input.title }));
package/src/project.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { AnalyticsSettings } from "./analytics/index.ts";
2
2
  import type {
3
- LocaleMetaOverride,
4
3
  LocaleNotEnabled,
5
4
  LocalesOf,
6
5
  RoutingConfig,
@@ -131,10 +130,6 @@ export interface ProjectInput<L extends string, Routes> {
131
130
  * reads like the whole truth when it is only a patch.
132
131
  */
133
132
  readonly overrideRouting?: Partial<RoutingConfig<L>>;
134
- /** Restates a locale's label. Its writing direction is not overridable. */
135
- readonly overrideLocaleMeta?: Readonly<
136
- Partial<Record<L, LocaleMetaOverride>>
137
- >;
138
133
  /** Adds to the site's `defaultFonts`, or replaces one by key. */
139
134
  readonly overrideFonts?: Fonts;
140
135
  /**
@@ -237,8 +232,9 @@ export type ProjectChecks<
237
232
  // Against the published locales rather than the site's list: a
238
233
  // segment spelled for a language this deployment does not
239
234
  // publish is a word that appears in no URL it builds. Mapped
240
- // over the keys written, for the reason `overrideLocaleMeta`
241
- // below is.
235
+ // over the keys written: `T` is inferred from the literal, so a
236
+ // stray key just becomes part of `T` and there is no fresh object
237
+ // for excess-property checking to fire on.
242
238
  readonly pageSegmentByLocale?: {
243
239
  readonly [K in StringKeys<
244
240
  NonNullable<T["overrideRouting"]>["pageSegmentByLocale"]
@@ -249,18 +245,8 @@ export type ProjectChecks<
249
245
  T["overrideRoutes"],
250
246
  StringKeys<Routes>
251
247
  >;
252
- // Nested, so that `dir` is rejected inside each locale's entry.
253
- // Declaring the field as `Pick<LocaleMeta, "label">` is not enough:
254
- // `T` is *inferred* from the literal, so a stray `dir` just becomes
255
- // part of `T` and there is no fresh object for excess-property
256
- // checking to fire on.
257
- readonly overrideLocaleMeta?: {
258
- readonly [K in StringKeys<T["overrideLocaleMeta"]>]: K extends Published
259
- ? NoExcessKeys<T["overrideLocaleMeta"][K], keyof LocaleMetaOverride>
260
- : LocaleNotEnabled<K>;
261
- };
262
248
  // The clock's locales must be ones this project publishes, as for
263
- // `overrideLocaleMeta` above. That every one it publishes is there is
249
+ // `pageSegmentByLocale` above. That every one it publishes is there is
264
250
  // checked in `ProjectOverrideChecks`: a conditional on `T` here would make
265
251
  // the constraint circular.
266
252
  readonly formats?: {
@@ -353,19 +353,6 @@ export type SectionPrefixOf<Routes, Overrides> = FirstSegment<
353
353
  EnabledSlugs<Routes, Overrides>
354
354
  >;
355
355
 
356
- /**
357
- * The sections with no page of their own — a prefix that heads a group but is
358
- * not itself a slug anything is published at.
359
- *
360
- * These are the only sections whose link a consumer may choose. Where an index
361
- * page exists it is the answer, and offering a second one would invite a
362
- * heading that points somewhere other than the page it names.
363
- */
364
- export type OrphanSectionPrefixOf<Routes, Overrides> = Exclude<
365
- SectionPrefixOf<Routes, Overrides>,
366
- EnabledSlugs<Routes, Overrides>
367
- >;
368
-
369
356
  /**
370
357
  * The route ids that survive a given override map, computed from literal types.
371
358
  *
@@ -43,15 +43,16 @@ type MemberId<M> = M extends string
43
43
  * The route ids a family produced.
44
44
  *
45
45
  * ```ts
46
- * const challengeRoutes = routeFamily(config, "challenges", challenges);
47
- * export type ChallengeRouteId = RouteIdsOf<typeof challengeRoutes>;
46
+ * const challenges = atlas.family("challenges", CHALLENGES);
47
+ * export type ChallengeRouteId = RouteIdsOf<typeof challenges>;
48
48
  * ```
49
49
  *
50
50
  * Reading them back off the family means the prefix is written once, in the
51
51
  * call. Spelling the ids out as `` `challenges-${string}` `` would be a second
52
52
  * copy of it, free to drift the day the URL changes.
53
53
  */
54
- export type RouteIdsOf<Family> = StringKeys<Family>;
54
+ export type RouteIdsOf<Family extends { readonly routes: unknown }> =
55
+ StringKeys<Family["routes"]>;
55
56
 
56
57
  /**
57
58
  * A URL prefix as a route-id prefix: `"news/posts"` -> `"news-posts"`.
@@ -349,3 +350,134 @@ export function familyGuard<
349
350
  return <T extends string>(id: T): id is T & StringKeys<Family> =>
350
351
  Object.hasOwn(family, id);
351
352
  }
353
+
354
+ /** A family's translated prefix, as `slugs` joins it. */
355
+ type PrefixByOf<Options> = Options extends {
356
+ readonly prefixByLocale: infer P extends ByLocale;
357
+ }
358
+ ? P
359
+ : Record<never, never>;
360
+
361
+ /**
362
+ * A family, and what a project does with it: spread its `routes` into the
363
+ * table, and ask the family — not a string — anything about its members.
364
+ *
365
+ * Every helper is bound to the prefix the family was made with, so the prefix
366
+ * is written once: a project cannot enable `"challenge"` members of a
367
+ * `"challenges"` family, or build a member's path under the wrong prefix.
368
+ */
369
+ export interface RouteFamily<
370
+ Prefix extends string,
371
+ Member extends string,
372
+ Routes,
373
+ PrefixBy extends ByLocale,
374
+ > {
375
+ /** The member routes, to spread into `atlas.routes`. */
376
+ readonly routes: Routes;
377
+
378
+ /**
379
+ * Whether a route id is one of this family's, as a type guard. See
380
+ * {@link familyGuard} for why it is generic in the input.
381
+ */
382
+ is<T extends string>(id: T): id is T & StringKeys<Routes>;
383
+
384
+ /** A member's route id: `routeIdOf("blue-room")` is `"rooms-blue-room"`. */
385
+ routeIdOf(member: Member): StringKeys<Routes>;
386
+
387
+ /** The member a route id of this family is the page of. */
388
+ memberOf(routeId: StringKeys<Routes>): Member;
389
+
390
+ /**
391
+ * One override for many members, for a project's `overrideRoutes`: by
392
+ * default, `{ enabled: true }`. Keys stay literal; see {@link enableRoutes}.
393
+ */
394
+ enable<
395
+ const Picked extends readonly (Member | { readonly id: Member })[],
396
+ const Override extends { readonly enabled?: boolean } = {
397
+ readonly enabled: true;
398
+ },
399
+ >(
400
+ members: Picked,
401
+ override?: Override
402
+ ): {
403
+ readonly [M in Picked[number] as `${Dashed<Prefix>}-${MemberId<M> &
404
+ string}`]: Override;
405
+ };
406
+
407
+ /**
408
+ * A member's paths at a segment of a project's own, in every locale,
409
+ * under the family's prefix and its translations. See
410
+ * {@link familyMemberSlugs}.
411
+ */
412
+ slugs<
413
+ const Segment extends string,
414
+ const SegmentBy extends ByLocale = Record<never, never>,
415
+ >(
416
+ segment: Segment,
417
+ segmentByLocale?: SegmentBy
418
+ ): FamilyMemberSlugs<Prefix, Segment, PrefixBy, SegmentBy>;
419
+ }
420
+
421
+ /** Builds a {@link RouteFamily}: what `atlas.family` returns. */
422
+ export function createFamily<
423
+ const C extends SiteConfigShape,
424
+ const Prefix extends string,
425
+ const Members extends readonly FamilyMemberInput<LocalesOf<C>>[],
426
+ const Options extends FamilyOptions<LocalesOf<C>> = Record<string, never>,
427
+ >(
428
+ config: C,
429
+ prefix: Prefix,
430
+ members: Members,
431
+ options?: Options
432
+ ): RouteFamily<
433
+ Prefix,
434
+ MemberId<Members[number]> & string,
435
+ ReturnType<typeof routeFamily<C, Prefix, Members, Options>>,
436
+ PrefixByOf<Options>
437
+ > {
438
+ const routes = routeFamily(config, prefix, members, options);
439
+ const enable = enableRoutes(prefix);
440
+ const ids = members.map((member) =>
441
+ typeof member === "string" ? member : member.id
442
+ );
443
+ const routeId = (member: string) =>
444
+ `${prefix.replaceAll("/", "-")}-${member}`;
445
+ const byRouteId = new Map(ids.map((member) => [routeId(member), member]));
446
+
447
+ return {
448
+ routes,
449
+ is: familyGuard(routes),
450
+ routeIdOf: (member) => {
451
+ if (!ids.includes(member)) {
452
+ throw new Error(
453
+ `"${member}" is not a member of the "${prefix}" family.`
454
+ );
455
+ }
456
+ return routeId(member) as never;
457
+ },
458
+ memberOf: (id) => {
459
+ const member = byRouteId.get(id);
460
+ if (member === undefined) {
461
+ throw new Error(
462
+ `"${id}" is not a route of the "${prefix}" family.`
463
+ );
464
+ }
465
+ return member as never;
466
+ },
467
+ enable: (picked, override) => enable(picked, override),
468
+ slugs: (segment, segmentByLocale) => {
469
+ // The join `familyMemberSlugs` makes, with the family's own prefix
470
+ // translations rather than a copy of them.
471
+ const slugByLocale = joinByLocale(
472
+ prefix,
473
+ segment,
474
+ options?.prefixByLocale,
475
+ segmentByLocale
476
+ );
477
+ return {
478
+ slug: `${prefix}/${segment}`,
479
+ ...(slugByLocale === undefined ? {} : { slugByLocale }),
480
+ } as never;
481
+ },
482
+ };
483
+ }
package/src/site/api.ts CHANGED
@@ -51,7 +51,6 @@ export interface LlmsOptions<
51
51
  RouteId extends string,
52
52
  L extends string,
53
53
  Prefix extends string = string,
54
- OrphanPrefix extends string = Prefix,
55
54
  > {
56
55
  /**
57
56
  * This route's name and one-line summary, in the file's language.
@@ -88,21 +87,6 @@ export interface LlmsOptions<
88
87
  * comparison that quietly never matches and leaves the default in place.
89
88
  */
90
89
  sectionHeading?(prefix: Prefix, locale: L): string | undefined;
91
- /**
92
- * Where a section with no index page should link.
93
- *
94
- * Separate from the heading because it is an enrichment rather than half of
95
- * one decision: a section can be renamed without being linked, and linked
96
- * without being renamed.
97
- *
98
- * Only sections that *have* no page of their own are offered. Where an index
99
- * page exists it is already the link, and `OrphanPrefix` excludes it — so
100
- * pointing a named family somewhere other than the page it is named after is
101
- * a compile error rather than a choice. Returning `undefined` leaves the
102
- * heading unlinked, which is the right answer when those pages are listed
103
- * nowhere else.
104
- */
105
- sectionLink?(prefix: OrphanPrefix, locale: L): RouteId | undefined;
106
90
  /** The `>` block under the title: what this site is, in a sentence or two. */
107
91
  readonly summary?: string;
108
92
  /**
@@ -132,8 +116,6 @@ export interface Site<
132
116
  * be, and no annotation should have to state it.
133
117
  */
134
118
  Prefix extends string = string,
135
- /** Of those, the ones with no index page of their own. */
136
- OrphanPrefix extends string = Prefix,
137
119
  > {
138
120
  readonly locales: readonly L[];
139
121
  readonly defaultLocale: L;
@@ -356,9 +338,7 @@ export interface Site<
356
338
  * the first time a route is switched off or a slug is retranslated, and
357
339
  * nothing reports it, because nothing reads the file during a build.
358
340
  */
359
- llms(
360
- options?: LlmsOptions<RouteId, L, Prefix, OrphanPrefix>
361
- ): GeneratedFile;
341
+ llms(options?: LlmsOptions<RouteId, L, Prefix>): GeneratedFile;
362
342
 
363
343
  /**
364
344
  * Old URLs that must keep working, resolved — for `siteRoutes()` only.
@@ -2,7 +2,7 @@ import { consentVendors } from "../analytics/index.ts";
2
2
  import {
3
3
  type LocaleMeta,
4
4
  type LocalesOf,
5
- mergeLocaleMeta,
5
+ resolveLocaleMeta,
6
6
  type SiteConfigShape,
7
7
  } from "../config.ts";
8
8
  import {
@@ -21,7 +21,7 @@ import {
21
21
  mergeCatalog,
22
22
  type OverrideCatalog,
23
23
  } from "../i18n/define.ts";
24
- import { createTranslateFactory } from "../i18n/translate.ts";
24
+ import { createTranslate, type TranslateFor } from "../i18n/translate.ts";
25
25
  import type { ImageAsset } from "../image.ts";
26
26
  import { buildLlms, type LlmsItem, type LlmsSection } from "../llms.ts";
27
27
  import {
@@ -43,11 +43,7 @@ import {
43
43
  statusFor,
44
44
  } from "../redirects.ts";
45
45
  import { buildRobots, type RobotsGroup } from "../robots.ts";
46
- import type {
47
- OrphanSectionPrefixOf,
48
- RouteRegistry,
49
- SectionPrefixOf,
50
- } from "../routes/define.ts";
46
+ import type { RouteRegistry, SectionPrefixOf } from "../routes/define.ts";
51
47
  import {
52
48
  buildPath,
53
49
  listRouteEntries,
@@ -122,8 +118,7 @@ export function createSite<
122
118
  Published,
123
119
  MessagesOf<Cat>,
124
120
  ProjectRouteId<Routes, P>,
125
- SectionPrefixOf<Routes, P["overrideRoutes"]>,
126
- OrphanSectionPrefixOf<Routes, P["overrideRoutes"]>
121
+ SectionPrefixOf<Routes, P["overrideRoutes"]>
127
122
  > {
128
123
  // The locales this *project* publishes, from its catalog, not the
129
124
  // universe the site config declares: `pathFor("home", "el-GR")` must not
@@ -171,10 +166,10 @@ export function createSite<
171
166
  // narrowing inside the closures below, which a property access would not.
172
167
  const siteUrl = project.url;
173
168
 
174
- const localeMeta = mergeLocaleMeta<L>(
169
+ const localeMeta = resolveLocaleMeta<L>(
175
170
  config_.locales as Readonly<Record<L, LocaleMeta>>,
176
- project.overrideLocaleMeta,
177
- locales
171
+ locales,
172
+ catalog.labels
178
173
  );
179
174
  const fonts = mergeFonts(config_.defaultFonts, project.overrideFonts);
180
175
  const pathContext: PathContext<L> = {
@@ -205,7 +200,6 @@ export function createSite<
205
200
  catalog.override,
206
201
  catalog.add
207
202
  );
208
- const translateFactory = createTranslateFactory<Full, L>();
209
203
 
210
204
  const resolved = mergeRoutes<L, Routes>(routes, project.overrideRoutes);
211
205
 
@@ -283,9 +277,7 @@ export function createSite<
283
277
  * Three callers, and two of them want one string out of it: `robots()` and
284
278
  * `llms()` each advertise `entry.url`, so an unmemoised call derived every
285
279
  * URL on the site — each with its alternates, each joined to the origin —
286
- * three times over to answer a question about a filename. `siteRoutes`
287
- * then does the whole thing again on every dev-server request that hits a
288
- * generated file.
280
+ * three times over to answer a question about a filename.
289
281
  *
290
282
  * Safe to hold because everything it reads is settled before this closure
291
283
  * exists: `entries`, `localeMeta` and the origin are fixed for the life of
@@ -783,17 +775,11 @@ export function createSite<
783
775
  options.sectionHeading?.(parent, locale) ??
784
776
  (owner === undefined ? parent : describe(owner, locale).name);
785
777
 
786
- // The owning page, or the one nominated for a section that has
787
- // none. Both may be absent — a family with no index page listed
788
- // nowhere else — and the heading is then left unlinked rather than
789
- // pointed at a URL this build never wrote.
790
- const target = owner ?? options.sectionLink?.(parent, locale);
791
-
778
+ // Linked to the owning page; a section with none, a family with no
779
+ // index page, is left unlinked rather than pointed somewhere else.
792
780
  sections.push({
793
781
  heading,
794
- ...(target === undefined
795
- ? {}
796
- : { url: urlFor(target, locale) }),
782
+ ...(owner === undefined ? {} : { url: urlFor(owner, locale) }),
797
783
  items: members.map(item),
798
784
  });
799
785
  }
@@ -888,7 +874,12 @@ export function createSite<
888
874
  translate: <const Values extends string = never>(
889
875
  locale: L,
890
876
  bind?: { readonly values?: Readonly<Record<Values, string>> }
891
- ) => translateFactory<Values>(messages, locale, bind?.values),
877
+ ): TranslateFor<Full, Values> =>
878
+ createTranslate({
879
+ catalog: messages,
880
+ locale,
881
+ values: bind?.values,
882
+ }),
892
883
  rich: <
893
884
  const Links extends string = never,
894
885
  const Values extends string = never,
@@ -40,9 +40,9 @@ import {
40
40
  type ValidateSlugDepth,
41
41
  } from "../routes/define.ts";
42
42
  import {
43
+ createFamily,
43
44
  type FamilyMemberInput,
44
45
  type FamilyOptions,
45
- routeFamily,
46
46
  } from "../routes/family.ts";
47
47
  import { createSite } from "./create.ts";
48
48
 
@@ -56,7 +56,7 @@ import { createSite } from "./create.ts";
56
56
  *
57
57
  * // config/routes.ts, messages.ts, projects/<name>/project.ts, site.ts
58
58
  * const rooms = atlas.family("rooms", ROOMS, { enabled: true });
59
- * export const defaultRoutes = atlas.routes({ ...rooms, home: {…} });
59
+ * export const defaultRoutes = atlas.routes({ ...rooms.routes, home: {…} });
60
60
  * export const defaultMessages = atlas.messages({…});
61
61
  * export const messages = atlas.catalog(defaultMessages, { locales: […], override: {…} });
62
62
  * export const { project, redirects } = atlas.project(messages, defaultRoutes, {…});
@@ -76,17 +76,21 @@ export function defineSite<
76
76
  return {
77
77
  config,
78
78
 
79
- /** A route per member, under one prefix: `rooms/red-room` for `rooms`. */
79
+ /**
80
+ * A route per member, under one prefix: `rooms/red-room` for `rooms`.
81
+ * Spread its `routes` into `routes()`; ask it the rest.
82
+ */
80
83
  family<
81
84
  const Prefix extends string,
82
85
  const Members extends readonly FamilyMemberInput<L>[],
83
86
  const Options extends FamilyOptions<L> = Record<string, never>,
84
- >(
85
- prefix: Prefix,
86
- members: Members,
87
- options?: Options
88
- ): ReturnType<typeof routeFamily<C, Prefix, Members, Options>> {
89
- return routeFamily(config, prefix, members, options);
87
+ >(prefix: Prefix, members: Members, options?: Options) {
88
+ return createFamily<C, Prefix, Members, Options>(
89
+ config,
90
+ prefix,
91
+ members,
92
+ options
93
+ );
90
94
  },
91
95
 
92
96
  /** The route table every project starts from. */
package/src/site/page.ts CHANGED
@@ -89,16 +89,7 @@ export interface PageMeta {
89
89
  * the trail and had to be handed ids this project actually builds. The trail
90
90
  * now arrives finished, so nothing here mentions a route.
91
91
  */
92
- export type PageContent = MetaContent & PageExtras;
93
-
94
- /**
95
- * The two things a document cannot answer about itself.
96
- *
97
- * Split out because `MetaContent` is an intersection with a union — an
98
- * interface cannot extend one — and the union is what stops a page being both
99
- * an article and something else.
100
- */
101
- interface PageExtras {
92
+ export interface PageContent extends MetaContent {
102
93
  /**
103
94
  * Which page of a paginated list this is, from 1.
104
95
  *
package/src/warn.ts CHANGED
@@ -43,12 +43,12 @@ const RESET = `${ESC}[0m`;
43
43
  * `process` and simply gets colour, which is what a browser console renders
44
44
  * anyway.
45
45
  */
46
- const env = () =>
47
- (
48
- globalThis as {
49
- process?: { env?: Readonly<Record<string, string | undefined>> };
50
- }
51
- ).process?.env;
46
+ const env = () => {
47
+ const host: typeof globalThis & {
48
+ process?: { env?: Readonly<Record<string, string | undefined>> };
49
+ } = globalThis;
50
+ return host.process?.env;
51
+ };
52
52
 
53
53
  // Functions, not values worked out at import: a browser bundle that imports
54
54
  // anything near this file would otherwise run the reads on every page.
@@ -1,24 +0,0 @@
1
- import { invalidId, UUID, type Uuid } from "./ids.ts";
2
-
3
- /**
4
- * AppLovin's Axon pixel. Marketing: it loads only once a visitor grants that.
5
- *
6
- * @see https://support.applovin.com/en/growth/promoting-your-websites/axon-pixel-integration/axon-pixel-native-js
7
- * @see https://support.applovin.com/en/growth/promoting-your-websites/axon-pixel-integration/events-and-objects
8
- */
9
- export interface AxonPixelSettings {
10
- /** From the Axon dashboard: a UUID. */
11
- readonly eventKey: Uuid;
12
- }
13
-
14
- /** Fails the build on a key that is not an event key: it would report nowhere. */
15
- export function checkAxonPixel(
16
- axon: AxonPixelSettings | undefined,
17
- at: string
18
- ): void {
19
- if (axon === undefined || UUID.test(axon.eventKey)) return;
20
- invalidId(
21
- at,
22
- `Axon event key "${axon.eventKey}" is not a UUID, so it would report nowhere. Copy it from the Axon dashboard.`
23
- );
24
- }
@@ -1,29 +0,0 @@
1
- import { CLARITY_PROJECT_ID, invalidId } from "./ids.ts";
2
-
3
- /**
4
- * Microsoft Clarity. It has a cookieless mode, so like Google it loads for
5
- * every visitor: without consent it records each page view on its own and sets
6
- * no cookie, and the visitor's answer switches its cookies on or off.
7
- *
8
- * @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-setup
9
- * @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/consent-mode
10
- * @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-consent-api-v2
11
- */
12
- export interface ClaritySettings {
13
- /** From the Clarity project's settings: 10 lowercase letters and digits. */
14
- readonly projectId: string;
15
- }
16
-
17
- /** Fails the build on an id that is not a project id: it would report nowhere. */
18
- export function checkClarity(
19
- clarity: ClaritySettings | undefined,
20
- at: string
21
- ): void {
22
- if (clarity === undefined || CLARITY_PROJECT_ID.test(clarity.projectId)) {
23
- return;
24
- }
25
- invalidId(
26
- at,
27
- `Clarity project id "${clarity.projectId}" is not 10 lowercase letters and digits, so it would report nowhere. Copy it from the Clarity project's settings.`
28
- );
29
- }
@@ -1,20 +0,0 @@
1
- import { DRIP_ACCOUNT_ID, invalidId, type NumericId } from "./ids.ts";
2
-
3
- /**
4
- * Drip's site tracking. Marketing: it loads only once a visitor grants that.
5
- *
6
- * @see https://help.drip.com/hc/en-us/articles/4424702539789-Install-Your-JavaScript-Snippet
7
- */
8
- export interface DripSettings {
9
- /** From Drip's Site Setup: digits, the `_dcs.account` of its snippet. */
10
- readonly accountId: NumericId;
11
- }
12
-
13
- /** Fails the build on an id that is not an account id: it would report nowhere. */
14
- export function checkDrip(drip: DripSettings | undefined, at: string): void {
15
- if (drip === undefined || DRIP_ACCOUNT_ID.test(drip.accountId)) return;
16
- invalidId(
17
- at,
18
- `Drip account id "${drip.accountId}" is not all digits, so it would report nowhere. Copy it from Drip's Site Setup.`
19
- );
20
- }
@@ -1,29 +0,0 @@
1
- import type { NonEmpty } from "../types.ts";
2
- import { invalidId, META_PIXEL_ID, type NumericId } from "./ids.ts";
3
-
4
- /**
5
- * Meta's pixel. Marketing: it loads only once a visitor grants that.
6
- *
7
- * @see https://developers.facebook.com/docs/meta-pixel/get-started
8
- * @see https://developers.facebook.com/docs/meta-pixel/reference
9
- * @see https://developers.facebook.com/docs/meta-pixel/implementation/gdpr
10
- */
11
- export interface MetaPixelSettings {
12
- /** From Events Manager: 15 or 16 digits. Several share one script. */
13
- readonly pixelIds: NonEmpty<NumericId>;
14
- }
15
-
16
- /** Fails the build on an id that is not a pixel id: it would report nowhere. */
17
- export function checkMetaPixel(
18
- meta: MetaPixelSettings | undefined,
19
- at: string
20
- ): void {
21
- for (const id of meta?.pixelIds ?? []) {
22
- if (!META_PIXEL_ID.test(id)) {
23
- invalidId(
24
- at,
25
- `Meta pixel id "${id}" is not 15 or 16 digits, so it would report nowhere. Copy it from Events Manager.`
26
- );
27
- }
28
- }
29
- }