@escape-game-over/atlas 0.1.1 → 0.1.2
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 +48 -7
- package/docs/NOT-BUILT.md +84 -0
- package/docs/toolchain.md +49 -22
- package/package.json +10 -6
- package/src/astro/MetaTags.astro +2 -4
- package/src/astro/carousel.ts +342 -0
- package/src/astro/dev-log.ts +128 -0
- package/src/astro/dom.ts +53 -0
- package/src/astro/element.ts +259 -0
- package/src/astro/site-routes.ts +111 -21
- package/src/astro/tsconfig.json +8 -0
- package/src/contact-form.ts +177 -0
- package/src/index.ts +7 -0
- package/src/routes/define.ts +3 -1
- package/src/routes/resolve.ts +28 -3
- package/src/site/api.ts +12 -0
- package/src/site/create.ts +132 -42
package/src/routes/resolve.ts
CHANGED
|
@@ -129,6 +129,16 @@ export function pageSegmentFor<L extends string>(
|
|
|
129
129
|
return ctx.pageSegmentByLocale?.[locale] ?? ctx.pageSegment;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/** A list's base path with a page number hung under the page segment. */
|
|
133
|
+
function underPageSegment(
|
|
134
|
+
base: UrlPath,
|
|
135
|
+
segment: string,
|
|
136
|
+
page: number
|
|
137
|
+
): UrlPath {
|
|
138
|
+
// The locale root is `/`, and joining onto it would double the slash.
|
|
139
|
+
return base === "/" ? `/${segment}/${page}` : `${base}/${segment}/${page}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
132
142
|
/**
|
|
133
143
|
* The path of one page of a route's list.
|
|
134
144
|
*
|
|
@@ -144,9 +154,24 @@ export function pagePath<L extends string>(
|
|
|
144
154
|
): UrlPath {
|
|
145
155
|
if (page <= 1) return base;
|
|
146
156
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
157
|
+
return underPageSegment(base, pageSegmentFor(locale, ctx), page);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The `.../page/1` a list is deliberately *not* published at.
|
|
162
|
+
*
|
|
163
|
+
* The counterpart of the rule above: giving page one the bare path leaves the
|
|
164
|
+
* numbered form unclaimed, and unclaimed is not unasked-for — a reader edits
|
|
165
|
+
* `/news/page/2` down to `1`, or a crawler assumes a sequence starts where its
|
|
166
|
+
* segment says. So this is the one path here that exists to be redirected away
|
|
167
|
+
* from rather than linked, and nothing but `redirects()` should name it.
|
|
168
|
+
*/
|
|
169
|
+
export function pageOneAlias<L extends string>(
|
|
170
|
+
base: UrlPath,
|
|
171
|
+
locale: L,
|
|
172
|
+
ctx: PathContext<L>
|
|
173
|
+
): UrlPath {
|
|
174
|
+
return underPageSegment(base, pageSegmentFor(locale, ctx), 1);
|
|
150
175
|
}
|
|
151
176
|
|
|
152
177
|
export function localePrefix<L extends string>(
|
package/src/site/api.ts
CHANGED
|
@@ -292,6 +292,18 @@ export interface Site<
|
|
|
292
292
|
* a retranslated slug moves it too. External targets are `https://` URLs and
|
|
293
293
|
* pass through untouched.
|
|
294
294
|
*
|
|
295
|
+
* **What comes back is more than what went in.** lib adds the rules the
|
|
296
|
+
* route table implies, for the URLs its own design leaves unpublished but
|
|
297
|
+
* reachable: whichever site root the routing mode does not serve — `/` when
|
|
298
|
+
* every locale is prefixed, `/en-US` when the default locale is not — and
|
|
299
|
+
* `/news/page/1` for a list whose page one is the bare path. A rule you
|
|
300
|
+
* state for one of those paths replaces the inferred one, so pass `[]` and
|
|
301
|
+
* you still get a file worth writing.
|
|
302
|
+
*
|
|
303
|
+
* Pages under an unused locale prefix are *not* claimed: that is a rule per
|
|
304
|
+
* page per language, and it still misses the reader who edits a translated
|
|
305
|
+
* slug's prefix. See `NOT-BUILT.md`.
|
|
306
|
+
*
|
|
295
307
|
* Returns the rules as data. Rendering is a separate step —
|
|
296
308
|
* `buildCloudflareRedirects` writes the `_redirects` that Cloudflare and
|
|
297
309
|
* Netlify read, and a host with its own syntax takes these and writes its own.
|
package/src/site/create.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
listRouteEntries,
|
|
35
35
|
mergeRoutes,
|
|
36
36
|
type PathContext,
|
|
37
|
+
pageOneAlias,
|
|
37
38
|
pagePath,
|
|
38
39
|
type RouteEntry,
|
|
39
40
|
slugFor,
|
|
@@ -218,8 +219,31 @@ export function createSite<
|
|
|
218
219
|
});
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
/**
|
|
223
|
+
* The sitemap, built once and handed to everyone who asks.
|
|
224
|
+
*
|
|
225
|
+
* Three callers, and two of them want one string out of it: `robots()` and
|
|
226
|
+
* `llms()` each advertise `entry.url`, so an unmemoised call derived every
|
|
227
|
+
* URL on the site — each with its alternates, each joined to the origin —
|
|
228
|
+
* three times over to answer a question about a filename. `siteRoutes`
|
|
229
|
+
* then does the whole thing again on every dev-server request that hits a
|
|
230
|
+
* generated file.
|
|
231
|
+
*
|
|
232
|
+
* Safe to hold because everything it reads is settled before this closure
|
|
233
|
+
* exists: `entries`, `localeMeta` and the origin are fixed for the life of
|
|
234
|
+
* a `Site`, and `alternatesFor` reads nothing else. A dev server picks up
|
|
235
|
+
* an edited route by importing the module again and getting a *new* site,
|
|
236
|
+
* not by this one answering differently — so there is nothing here for a
|
|
237
|
+
* cache to go stale against.
|
|
238
|
+
*
|
|
239
|
+
* Sharing the `Sitemap` itself is safe for the same reason, with one part
|
|
240
|
+
* that would not be: `staticPaths` maps a fresh array per call, which is
|
|
241
|
+
* what an SSG router demands of it.
|
|
242
|
+
*/
|
|
243
|
+
let built: Sitemap | undefined;
|
|
221
244
|
function sitemap(): Sitemap {
|
|
222
|
-
return
|
|
245
|
+
if (built !== undefined) return built;
|
|
246
|
+
built = buildSitemap<L, RouteId>({
|
|
223
247
|
siteUrl,
|
|
224
248
|
name: config_.sitemap?.name ?? "sitemap.xml",
|
|
225
249
|
entryLimit: config_.sitemap?.entryLimit,
|
|
@@ -227,6 +251,7 @@ export function createSite<
|
|
|
227
251
|
localeMeta,
|
|
228
252
|
alternatesFor,
|
|
229
253
|
});
|
|
254
|
+
return built;
|
|
230
255
|
}
|
|
231
256
|
|
|
232
257
|
// Registry declaration order, filtered to what this project builds.
|
|
@@ -242,12 +267,24 @@ export function createSite<
|
|
|
242
267
|
pathContext
|
|
243
268
|
) as readonly RouteEntry<L, RouteId>[];
|
|
244
269
|
|
|
245
|
-
//
|
|
246
|
-
//
|
|
270
|
+
// The page the site's root resolves to, wherever routing puts it: `/` when
|
|
271
|
+
// the default locale is unprefixed, `/en-US` when it is not. Undefined
|
|
272
|
+
// unless a route actually claims that path — nothing has to own the root.
|
|
247
273
|
const localeRootPath = buildPath("", defaultLocale, pathContext);
|
|
248
|
-
const rootEntry =
|
|
249
|
-
|
|
250
|
-
|
|
274
|
+
const rootEntry = entries.find(
|
|
275
|
+
(entry) =>
|
|
276
|
+
entry.locale === defaultLocale && entry.path === localeRootPath
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Every path this build serves.
|
|
281
|
+
*
|
|
282
|
+
* Read by `redirects()` twice over: to reject a stated rule that shadows a
|
|
283
|
+
* real page, and to keep an inferred one from doing the same.
|
|
284
|
+
*/
|
|
285
|
+
const builtPaths: ReadonlySet<string> = new Set(
|
|
286
|
+
entries.map((entry) => entry.path)
|
|
287
|
+
);
|
|
251
288
|
|
|
252
289
|
function staticPaths(param: string): StaticPath<L, RouteId>[] {
|
|
253
290
|
const paths: StaticPath<L, RouteId>[] = entries.map((entry) => ({
|
|
@@ -450,55 +487,108 @@ export function createSite<
|
|
|
450
487
|
return joinUrl(siteUrl, path);
|
|
451
488
|
}
|
|
452
489
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
490
|
+
/**
|
|
491
|
+
* The rules lib writes for itself, from the route table rather than from
|
|
492
|
+
* anything a project states.
|
|
493
|
+
*
|
|
494
|
+
* Both answer one question: which URL that this build publishes nothing at
|
|
495
|
+
* will be asked for anyway? Each is a path the design deliberately does not
|
|
496
|
+
* serve — the root the routing mode leaves unowned, and the page one that
|
|
497
|
+
* is not numbered — and a path nobody serves on purpose is still a path
|
|
498
|
+
* somebody types. Both are read off `entries`, so a retranslated slug or a
|
|
499
|
+
* switched-off page moves the rule with it instead of leaving one pointing
|
|
500
|
+
* at a 404.
|
|
501
|
+
*
|
|
502
|
+
* Two, and deliberately not a third per *page*: mirroring every URL a
|
|
503
|
+
* project builds is a rule per page, and with slugs translated per locale
|
|
504
|
+
* it is a rule per page per language — a file of hundreds against the 2000
|
|
505
|
+
* a host reads, for URLs nothing ever linked. See `NOT-BUILT.md`.
|
|
506
|
+
*
|
|
507
|
+
* Both permanent. Neither is a state that changes while the config stays as
|
|
508
|
+
* it is: page one will never live at `/page/1`, and the root will not move
|
|
509
|
+
* while `prefixDefaultLocale` is what it is.
|
|
510
|
+
*/
|
|
511
|
+
function inferredRedirects(): readonly ResolvedRedirect[] {
|
|
512
|
+
const status = statusFor("permanent");
|
|
513
|
+
|
|
514
|
+
// One root is served and the other is not, and which is which is what
|
|
515
|
+
// `prefixDefaultLocale` decides: prefixed, the site answers on `/en-US`
|
|
516
|
+
// and nothing owns `/`; unprefixed, it answers on `/` and `/en-US` is a
|
|
517
|
+
// URL nobody built — the one people still type, having seen every other
|
|
518
|
+
// language wear its prefix. Whichever of the two this build does not
|
|
519
|
+
// serve is pointed at the one it does.
|
|
458
520
|
//
|
|
459
521
|
// A redirect rather than a page: a meta-refresh stub at `/` is a soft
|
|
460
522
|
// redirect, which search engines follow slowly and weigh less, and it
|
|
461
523
|
// costs a render before the reader goes anywhere. The two cannot both
|
|
462
524
|
// exist, since a static host serves the file and the rule never fires —
|
|
463
|
-
// which is what `builtPaths`
|
|
464
|
-
|
|
465
|
-
// the route table and follows a retranslated slug.
|
|
466
|
-
const root: readonly RedirectRule<RouteId, L>[] =
|
|
525
|
+
// which is what `builtPaths` rejects.
|
|
526
|
+
const root: readonly ResolvedRedirect[] =
|
|
467
527
|
rootEntry === undefined
|
|
468
528
|
? []
|
|
469
529
|
: [
|
|
470
530
|
{
|
|
471
|
-
from: "/"
|
|
472
|
-
to:
|
|
473
|
-
|
|
474
|
-
locale: rootEntry.locale,
|
|
475
|
-
},
|
|
476
|
-
// The root will never own a page again while every
|
|
477
|
-
// locale is prefixed, which is what permanent means.
|
|
478
|
-
kind: "permanent",
|
|
531
|
+
from: prefixDefaultLocale ? "/" : `/${defaultLocale}`,
|
|
532
|
+
to: rootEntry.path,
|
|
533
|
+
status,
|
|
479
534
|
},
|
|
480
535
|
];
|
|
481
536
|
|
|
537
|
+
// `/news/page/1` for every list that runs past one page, in every
|
|
538
|
+
// language it is published in — see `pageOneAlias` for who asks for it.
|
|
539
|
+
//
|
|
540
|
+
// Only a route that *has* a second page: `1` or omitted is an ordinary
|
|
541
|
+
// route, and `/about/page/1` is a URL nobody has had a reason to type.
|
|
542
|
+
// The alias for a list that once ran longer and no longer does is the
|
|
543
|
+
// one this misses, and it is not worth a rule per page of the site.
|
|
544
|
+
const pageOne = entries
|
|
545
|
+
.filter(
|
|
546
|
+
(entry) =>
|
|
547
|
+
entry.page === 1 &&
|
|
548
|
+
(resolved[entry.routeId]?.pages ?? 1) > 1
|
|
549
|
+
)
|
|
550
|
+
.map((entry) => ({
|
|
551
|
+
from: pageOneAlias(entry.path, entry.locale, pathContext),
|
|
552
|
+
to: entry.path,
|
|
553
|
+
status,
|
|
554
|
+
}));
|
|
555
|
+
|
|
556
|
+
return [...root, ...pageOne];
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function redirects(
|
|
560
|
+
rules: readonly RedirectRule<RouteId, L>[]
|
|
561
|
+
): readonly ResolvedRedirect[] {
|
|
562
|
+
const stated: readonly ResolvedRedirect[] = rules.map((rule) => ({
|
|
563
|
+
from: rule.from,
|
|
564
|
+
// Three kinds of target, told apart by shape: a string is external
|
|
565
|
+
// and passes through, `file` is served verbatim from `public/`, and
|
|
566
|
+
// a route is resolved to whatever URL it has in the locale asked
|
|
567
|
+
// for.
|
|
568
|
+
to: ((): string => {
|
|
569
|
+
if (typeof rule.to === "string") return rule.to;
|
|
570
|
+
if ("file" in rule.to) return rule.to.file;
|
|
571
|
+
return pathFor(rule.to.route, rule.to.locale ?? defaultLocale);
|
|
572
|
+
})(),
|
|
573
|
+
status: statusFor(rule.kind),
|
|
574
|
+
}));
|
|
575
|
+
|
|
576
|
+
const claimed = new Set(stated.map((rule) => rule.from));
|
|
577
|
+
const inferred = inferredRedirects().filter(
|
|
578
|
+
// Dropped rather than reported, both times, because neither is a
|
|
579
|
+
// mistake anyone made: a rule the project wrote for one of these
|
|
580
|
+
// paths is a decision and this is a default, and a page built at
|
|
581
|
+
// one of them is a page, which outranks a mirror of another page.
|
|
582
|
+
// The same clash among *stated* rules still throws below — there,
|
|
583
|
+
// both sides were written on purpose and only one can fire.
|
|
584
|
+
(rule) => !claimed.has(rule.from) && !builtPaths.has(rule.from)
|
|
585
|
+
);
|
|
586
|
+
|
|
482
587
|
return buildRedirects({
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
builtPaths
|
|
486
|
-
rules: [...
|
|
487
|
-
from: rule.from,
|
|
488
|
-
// Three kinds of target, told apart by shape: a string is
|
|
489
|
-
// external and passes through, `file` is served verbatim from
|
|
490
|
-
// `public/`, and a route is resolved to whatever URL it has in
|
|
491
|
-
// the locale asked for.
|
|
492
|
-
to: ((): string => {
|
|
493
|
-
if (typeof rule.to === "string") return rule.to;
|
|
494
|
-
if ("file" in rule.to) return rule.to.file;
|
|
495
|
-
return pathFor(
|
|
496
|
-
rule.to.route,
|
|
497
|
-
rule.to.locale ?? defaultLocale
|
|
498
|
-
);
|
|
499
|
-
})(),
|
|
500
|
-
status: statusFor(rule.kind),
|
|
501
|
-
})),
|
|
588
|
+
// So a rule that shadows a real page is rejected rather than
|
|
589
|
+
// sitting dead in the file.
|
|
590
|
+
builtPaths,
|
|
591
|
+
rules: [...inferred, ...stated],
|
|
502
592
|
});
|
|
503
593
|
}
|
|
504
594
|
|