@escape-game-over/atlas 0.1.1
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 +364 -0
- package/bin/use-project.mjs +131 -0
- package/docs/NOT-BUILT.md +329 -0
- package/docs/checks.md +139 -0
- package/docs/share-images.md +52 -0
- package/docs/toolchain.md +83 -0
- package/package.json +51 -0
- package/src/analytics/google.ts +351 -0
- package/src/analytics/index.ts +102 -0
- package/src/analytics/tags.ts +57 -0
- package/src/analytics/umami.ts +285 -0
- package/src/astro/MetaTags.astro +87 -0
- package/src/astro/consent.ts +165 -0
- package/src/astro/images.ts +315 -0
- package/src/astro/index.ts +44 -0
- package/src/astro/public-files.ts +129 -0
- package/src/astro/site-routes.ts +307 -0
- package/src/config.ts +218 -0
- package/src/contact.ts +233 -0
- package/src/file.ts +16 -0
- package/src/files.ts +39 -0
- package/src/hours.ts +312 -0
- package/src/i18n/define.ts +217 -0
- package/src/i18n/placeholders.ts +94 -0
- package/src/i18n/translate.ts +190 -0
- package/src/image.ts +29 -0
- package/src/index.ts +222 -0
- package/src/jsonld/article.ts +165 -0
- package/src/jsonld/breadcrumb.ts +34 -0
- package/src/jsonld/business.ts +196 -0
- package/src/jsonld/ids.ts +106 -0
- package/src/jsonld/index.ts +59 -0
- package/src/jsonld/node.ts +78 -0
- package/src/jsonld/organization.ts +154 -0
- package/src/jsonld/place.ts +96 -0
- package/src/jsonld/product.ts +172 -0
- package/src/jsonld/quantity.ts +55 -0
- package/src/jsonld/service.ts +237 -0
- package/src/jsonld/video.ts +239 -0
- package/src/jsonld/website.ts +58 -0
- package/src/llms.ts +160 -0
- package/src/meta/content.ts +190 -0
- package/src/meta/index.ts +432 -0
- package/src/meta/robots.ts +212 -0
- package/src/meta/share-image.ts +232 -0
- package/src/meta/tag.ts +133 -0
- package/src/meta/verification.ts +53 -0
- package/src/money.ts +237 -0
- package/src/project.ts +249 -0
- package/src/redirects.ts +266 -0
- package/src/robots.ts +80 -0
- package/src/routes/define.ts +412 -0
- package/src/routes/family.ts +251 -0
- package/src/routes/resolve.ts +266 -0
- package/src/site/api.ts +354 -0
- package/src/site/create.ts +660 -0
- package/src/site/index.ts +32 -0
- package/src/site/page.ts +148 -0
- package/src/sitemap.ts +257 -0
- package/src/types.ts +160 -0
- package/src/url.ts +144 -0
- package/src/warn.ts +88 -0
- package/src/xml.ts +103 -0
package/src/url.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL shapes checked at the point they are written.
|
|
3
|
+
*
|
|
4
|
+
* Shared rather than restated, because more than one thing here takes a URL from
|
|
5
|
+
* a consumer — the site's own origin, and the target of an external redirect —
|
|
6
|
+
* and they must agree about what an acceptable one looks like. Each rejects with
|
|
7
|
+
* a named error rather than a bare pattern mismatch, so the message says what is
|
|
8
|
+
* wrong instead of only that something is.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* An absolute `https://` URL, as a shape.
|
|
13
|
+
*
|
|
14
|
+
* The plain type, for a URL that is *already* known good — what `createSite`
|
|
15
|
+
* hands back, and what a structured-data node quotes. The generic validators
|
|
16
|
+
* below are for a URL arriving from a consumer, where a named error is worth
|
|
17
|
+
* the type parameter; here there is nothing to explain, only a fact to keep.
|
|
18
|
+
*
|
|
19
|
+
* Sound because `createSite` throws on anything else before this is ever
|
|
20
|
+
* exposed. A type that outran its runtime check would be worse than none.
|
|
21
|
+
*/
|
|
22
|
+
export type HttpsUrl = `https://${string}`;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The runtime half of `HttpsUrl`, written as a guard so the check that throws
|
|
26
|
+
* is also the check that narrows.
|
|
27
|
+
*
|
|
28
|
+
* A cast would assert the same thing without testing it, and would keep on
|
|
29
|
+
* asserting it after someone edited the condition above it.
|
|
30
|
+
*/
|
|
31
|
+
export function isHttpsUrl(url: string): url is HttpsUrl {
|
|
32
|
+
return url.startsWith("https://");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A path from the root of some origin: `/logo.png`, `/it/sfide`.
|
|
37
|
+
*
|
|
38
|
+
* The other half of every URL this library builds. Typed so a caller cannot
|
|
39
|
+
* hand over `logo.png` and get `https://example.comlogo.png`, which is the
|
|
40
|
+
* failure that reads as a typo in the output rather than as a bug in the join.
|
|
41
|
+
*/
|
|
42
|
+
export type UrlPath = `/${string}`;
|
|
43
|
+
|
|
44
|
+
/** The runtime half of `UrlPath`. See `isHttpsUrl`. */
|
|
45
|
+
export function isUrlPath(path: string): path is UrlPath {
|
|
46
|
+
return path.startsWith("/");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* An origin without its trailing slash.
|
|
51
|
+
*
|
|
52
|
+
* Necessary because the *type* cannot rule one out — a template literal has no
|
|
53
|
+
* way to say "does not end in `/`" — so every origin arriving from outside is
|
|
54
|
+
* normalised rather than trusted. `https://x.example/` and `https://x.example`
|
|
55
|
+
* name the same origin, so this loses nothing and is not worth refusing over.
|
|
56
|
+
*
|
|
57
|
+
* It matters most where an origin becomes an identity: `https://x.example/`
|
|
58
|
+
* and `https://x.example` yield two different `@id` strings, and two
|
|
59
|
+
* deployments that wrote it differently would describe two separate brands.
|
|
60
|
+
*/
|
|
61
|
+
export function normalizeOrigin(origin: HttpsUrl): HttpsUrl {
|
|
62
|
+
if (!origin.endsWith("/")) return origin;
|
|
63
|
+
const trimmed = origin.slice(0, -1);
|
|
64
|
+
// Survives unless the whole value was `https://`, which is a scheme rather
|
|
65
|
+
// than an origin. Re-checking beats a cast asserting what was not tested.
|
|
66
|
+
if (!isHttpsUrl(trimmed)) {
|
|
67
|
+
throw new Error(`"${origin}" is a scheme, not an origin.`);
|
|
68
|
+
}
|
|
69
|
+
return trimmed;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* An origin and a path, with exactly one slash between them.
|
|
74
|
+
*
|
|
75
|
+
* Every absolute URL lib emits is a concatenation, and both ends can carry a
|
|
76
|
+
* slash. Doing it by hand in a dozen places is how one of them ends up with
|
|
77
|
+
* `//` — which is a different URL, served or redirected inconsistently, and
|
|
78
|
+
* duplicate content if a crawler reaches both.
|
|
79
|
+
*/
|
|
80
|
+
export function joinUrl(origin: HttpsUrl, path: UrlPath): HttpsUrl {
|
|
81
|
+
return `${normalizeOrigin(origin)}${path}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface TrailingSlashNotAllowed<U extends string> {
|
|
85
|
+
readonly __URL_HAS_TRAILING_SLASH__: `url "${U}" must not end with a slash: every path lib builds already starts with one`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface MustBeHttps<U extends string> {
|
|
89
|
+
readonly __URL_NOT_HTTPS__: `url "${U}" must start with https://: an http link is a downgrade, and browsers increasingly refuse it outright`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* An absolute `https://` URL. A path, query and trailing slash are all fine.
|
|
94
|
+
*
|
|
95
|
+
* For a URL that points *somewhere else* — the target of a redirect, say — where
|
|
96
|
+
* only the scheme is this library's business.
|
|
97
|
+
*/
|
|
98
|
+
export type ValidHttpsUrl<U extends string> = U extends `https://${string}`
|
|
99
|
+
? U
|
|
100
|
+
: MustBeHttps<U>;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Rejects an origin that is not a bare `https://` prefix without a trailing
|
|
104
|
+
* slash. A path is allowed, since deploying under a subdirectory is legitimate.
|
|
105
|
+
*
|
|
106
|
+
* Catches both at the point the URL is written, since a project states it as a
|
|
107
|
+
* literal. `createSite` still checks at build time, for the case where the URL
|
|
108
|
+
* comes from somewhere typed as plain `string` — an env var, say — and these
|
|
109
|
+
* checks pass vacuously.
|
|
110
|
+
*/
|
|
111
|
+
export type ValidSiteUrl<U extends string> = U extends `${string}/`
|
|
112
|
+
? TrailingSlashNotAllowed<U>
|
|
113
|
+
: ValidHttpsUrl<U>;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* An asset's `src` as an absolute URL, whatever form it arrived in.
|
|
117
|
+
*
|
|
118
|
+
* Takes `string` because that is what a bundler declares `src` as, and turns it
|
|
119
|
+
* into one of the only two things it can legitimately be — an `https://` URL
|
|
120
|
+
* already, or a path on this origin.
|
|
121
|
+
*
|
|
122
|
+
* Neither rejection is pedantry. An `http://` asset is mixed content, dropped
|
|
123
|
+
* by anything that scrapes it. A relative `logo.png` would be joined as written
|
|
124
|
+
* and produce `https://example.comlogo.png`, which reads as a typo in the
|
|
125
|
+
* output long before anyone suspects the join.
|
|
126
|
+
*
|
|
127
|
+
* Throws rather than skipping, because the callers build *sets* — three crops
|
|
128
|
+
* of a photograph, one per aspect ratio — and quietly returning two of them is
|
|
129
|
+
* a set that looks complete and is not.
|
|
130
|
+
*/
|
|
131
|
+
export function absoluteUrl(
|
|
132
|
+
origin: HttpsUrl,
|
|
133
|
+
src: string,
|
|
134
|
+
at: string
|
|
135
|
+
): HttpsUrl {
|
|
136
|
+
if (isHttpsUrl(src)) return src;
|
|
137
|
+
|
|
138
|
+
if (!isUrlPath(src)) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`${at}: "${src}" is neither an https:// URL nor a path from the root.`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return joinUrl(origin, src);
|
|
144
|
+
}
|
package/src/warn.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place lib writes to a console.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is a *warning* by definition: a fact about a file or a string
|
|
5
|
+
* that no type can see, worth saying out loud but never worth failing a build
|
|
6
|
+
* over. Errors throw instead, and say so at the point they are wrong.
|
|
7
|
+
*
|
|
8
|
+
* Shared so the colouring and the `[lib]` prefix are decided once — a warning
|
|
9
|
+
* that looks different depending on which module raised it reads like two
|
|
10
|
+
* different tools.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* What every line lib writes is labelled with.
|
|
15
|
+
*
|
|
16
|
+
* A constant rather than a literal at each call site: grep for it to find every
|
|
17
|
+
* warning the library can raise, and change it once if this folder is ever
|
|
18
|
+
* published under its real name.
|
|
19
|
+
*/
|
|
20
|
+
const PREFIX = "lib";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The escape character, named rather than embedded. 27 is its code point.
|
|
24
|
+
*
|
|
25
|
+
* The three constants below used to hold the raw control byte inside their
|
|
26
|
+
* string literals — invisible in every editor, and sitting in a folder whose
|
|
27
|
+
* whole purpose is to be copied between repositories, where a tool that
|
|
28
|
+
* normalises or re-encodes on the way through would take the colour with it
|
|
29
|
+
* silently and only in the copy.
|
|
30
|
+
*
|
|
31
|
+
* Built from the code point rather than from a unicode escape, which takes the
|
|
32
|
+
* same reasoning one step further: the source stays plain ASCII with nothing to
|
|
33
|
+
* mis-transcribe, and the constant says what the byte *is* rather than leaving
|
|
34
|
+
* a reader to decode it. Nothing in this file needs a character an editor
|
|
35
|
+
* cannot show.
|
|
36
|
+
*/
|
|
37
|
+
const ESC = String.fromCharCode(27);
|
|
38
|
+
|
|
39
|
+
const YELLOW = `${ESC}[33m`;
|
|
40
|
+
const DIM = `${ESC}[2m`;
|
|
41
|
+
const RESET = `${ESC}[0m`;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Whether to emit ANSI colour.
|
|
45
|
+
*
|
|
46
|
+
* `NO_COLOR` is the convention every CLI honours, and the check reaches through
|
|
47
|
+
* `globalThis` rather than naming `process`: this file is part of the core,
|
|
48
|
+
* which is type-checked with `"types": []` and cannot see Node. A browser has no
|
|
49
|
+
* `process` and simply gets colour, which is what a browser console renders
|
|
50
|
+
* anyway.
|
|
51
|
+
*/
|
|
52
|
+
const colour =
|
|
53
|
+
(
|
|
54
|
+
globalThis as {
|
|
55
|
+
process?: { env?: Readonly<Record<string, string | undefined>> };
|
|
56
|
+
}
|
|
57
|
+
).process?.env?.NO_COLOR === undefined;
|
|
58
|
+
|
|
59
|
+
const paint = (code: string, text: string): string =>
|
|
60
|
+
colour ? `${code}${text}${RESET}` : text;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Warns about one page, or one file.
|
|
64
|
+
*
|
|
65
|
+
* `at` is whatever identifies the thing — a canonical URL, an asset path. It is
|
|
66
|
+
* always printed, because a warning nobody can locate is one nobody acts on.
|
|
67
|
+
*/
|
|
68
|
+
export function warn(at: string, message: string): void {
|
|
69
|
+
console.warn(
|
|
70
|
+
`${paint(YELLOW, `[${PREFIX}] [WARN]`)} ${paint(DIM, shorten(at))}\n ${message}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* An absolute URL down to its path.
|
|
76
|
+
*
|
|
77
|
+
* Every warning in one build shares an origin, so printing it on each is the
|
|
78
|
+
* same forty characters repeated — and the path is what matches the page list
|
|
79
|
+
* the build is already printing beside it. Anything that is not a URL, such as
|
|
80
|
+
* a filename, is left as it is.
|
|
81
|
+
*/
|
|
82
|
+
function shorten(at: string): string {
|
|
83
|
+
try {
|
|
84
|
+
return new URL(at).pathname;
|
|
85
|
+
} catch {
|
|
86
|
+
return at;
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/xml.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal XML element builder.
|
|
3
|
+
*
|
|
4
|
+
* Generating XML by concatenating template literals means hand-managing
|
|
5
|
+
* indentation, stray newlines and — worse — remembering to escape at every
|
|
6
|
+
* interpolation. Building a tree instead makes the shape of the document read
|
|
7
|
+
* like the document, and escaping happens once, in the renderer.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const XML_ESCAPES: Readonly<Record<string, string>> = {
|
|
11
|
+
"&": "&",
|
|
12
|
+
"<": "<",
|
|
13
|
+
">": ">",
|
|
14
|
+
'"': """,
|
|
15
|
+
"'": "'",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function escapeXml(value: string): string {
|
|
19
|
+
return value.replace(/[&<>"']/g, (char) => XML_ESCAPES[char] ?? char);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type XmlAttributes = Readonly<Record<string, string | undefined>>;
|
|
23
|
+
|
|
24
|
+
export interface XmlElement {
|
|
25
|
+
readonly tag: string;
|
|
26
|
+
readonly attrs?: XmlAttributes;
|
|
27
|
+
readonly text?: string;
|
|
28
|
+
readonly children?: readonly XmlChild[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `false` and `undefined` are allowed so conditional children read inline:
|
|
33
|
+
* `cond && el("priority", undefined, "1.0")`.
|
|
34
|
+
*/
|
|
35
|
+
export type XmlChild = XmlElement | false | undefined;
|
|
36
|
+
|
|
37
|
+
export type XmlChildren = string | readonly XmlChild[];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Builds an element. Attributes are optional and may be skipped entirely:
|
|
41
|
+
*
|
|
42
|
+
* ```ts
|
|
43
|
+
* el("loc", "https://example.com") // text
|
|
44
|
+
* el("url", [el("loc", "…")]) // children
|
|
45
|
+
* el("xhtml:link", { rel: "alternate", href: "…" }) // attributes only
|
|
46
|
+
* el("urlset", { xmlns: "…" }, urls) // both
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function el(tag: string, children?: XmlChildren): XmlElement;
|
|
50
|
+
export function el(
|
|
51
|
+
tag: string,
|
|
52
|
+
attrs: XmlAttributes,
|
|
53
|
+
children?: XmlChildren
|
|
54
|
+
): XmlElement;
|
|
55
|
+
export function el(
|
|
56
|
+
tag: string,
|
|
57
|
+
attrsOrChildren?: XmlAttributes | XmlChildren,
|
|
58
|
+
maybeChildren?: XmlChildren
|
|
59
|
+
): XmlElement {
|
|
60
|
+
const isChildren =
|
|
61
|
+
typeof attrsOrChildren === "string" || Array.isArray(attrsOrChildren);
|
|
62
|
+
const attrs = isChildren ? undefined : (attrsOrChildren as XmlAttributes);
|
|
63
|
+
const children = isChildren
|
|
64
|
+
? (attrsOrChildren as XmlChildren)
|
|
65
|
+
: maybeChildren;
|
|
66
|
+
|
|
67
|
+
return typeof children === "string"
|
|
68
|
+
? { tag, attrs, text: children }
|
|
69
|
+
: { tag, attrs, children };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderAttrs(attrs: XmlAttributes | undefined): string {
|
|
73
|
+
if (attrs === undefined) return "";
|
|
74
|
+
return Object.entries(attrs)
|
|
75
|
+
.filter(([, value]) => value !== undefined)
|
|
76
|
+
.map(([name, value]) => ` ${name}="${escapeXml(value as string)}"`)
|
|
77
|
+
.join("");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Renders one element, indenting nested levels by two spaces. */
|
|
81
|
+
export function renderXml(node: XmlElement, depth = 0): string {
|
|
82
|
+
const pad = " ".repeat(depth);
|
|
83
|
+
const open = `${pad}<${node.tag}${renderAttrs(node.attrs)}`;
|
|
84
|
+
|
|
85
|
+
if (node.text !== undefined) {
|
|
86
|
+
return `${open}>${escapeXml(node.text)}</${node.tag}>`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const children = (node.children ?? []).filter(
|
|
90
|
+
(child): child is XmlElement => child !== false && child !== undefined
|
|
91
|
+
);
|
|
92
|
+
if (children.length === 0) return `${open} />`;
|
|
93
|
+
|
|
94
|
+
const inner = children
|
|
95
|
+
.map((child) => renderXml(child, depth + 1))
|
|
96
|
+
.join("\n");
|
|
97
|
+
return `${open}>\n${inner}\n${pad}</${node.tag}>`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Renders a whole document, declaration included, with a trailing newline. */
|
|
101
|
+
export function renderXmlDocument(root: XmlElement): string {
|
|
102
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n${renderXml(root)}\n`;
|
|
103
|
+
}
|