@escape-game-over/atlas 0.1.82 → 0.1.84
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 +51 -43
- package/docs/NOT-BUILT.md +30 -30
- package/docs/checks.md +6 -6
- package/docs/client-scripts.md +46 -28
- package/docs/rich-text.md +15 -8
- package/docs/share-images.md +1 -1
- package/docs/toolchain.md +2 -2
- package/package.json +7 -7
- package/src/jsonld/organization.ts +17 -0
- package/src/llms.ts +15 -6
- package/src/site/create.ts +12 -0
package/README.md
CHANGED
|
@@ -61,33 +61,37 @@ Linking to a page the active deployment switched off is a compile error, not a
|
|
|
61
61
|
One file per deployment, saying only what differs:
|
|
62
62
|
|
|
63
63
|
```ts
|
|
64
|
-
export const { project, redirects } = atlas.project(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
64
|
+
export const { project, redirects } = atlas.project(
|
|
65
|
+
defaultMessages,
|
|
66
|
+
defaultRoutes,
|
|
67
|
+
{
|
|
68
|
+
url: "https://acme.example",
|
|
69
|
+
siteName: "Acme Rome",
|
|
70
|
+
icon,
|
|
71
|
+
themeColor: "#1d51e0",
|
|
72
|
+
|
|
73
|
+
enabledLocales: ["el-GR", "en-US"],
|
|
74
|
+
overrideRouting: { defaultLocale: "el-GR", prefixDefaultLocale: true },
|
|
75
|
+
|
|
76
|
+
overrideMessages: {
|
|
77
|
+
// Greek only — the English copy still falls through to the default catalog.
|
|
78
|
+
"home.hero.body": { "el-GR": "Η {company} κρατά το απόθεμα…" },
|
|
79
|
+
},
|
|
80
|
+
overrideRoutes: {
|
|
81
|
+
careers: { enabled: true }, // opt into a page
|
|
82
|
+
contact: { slugByLocale: { "el-GR": "epikoinoniste-mazi-mas" } }, // retranslate a URL
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
);
|
|
82
86
|
```
|
|
83
87
|
|
|
84
|
-
Every field is named for what it
|
|
88
|
+
Every field is named for what it _does_ to the defaults, so a project file can
|
|
85
89
|
never be mistaken for the whole truth: `enabledLocales` picks from what the site
|
|
86
90
|
declares, and every `override*` is a patch over it.
|
|
87
91
|
|
|
88
92
|
## The four steps
|
|
89
93
|
|
|
90
|
-
Every step infers its types from the
|
|
94
|
+
Every step infers its types from the _values_ of the last, so a consuming project
|
|
91
95
|
never writes a type argument:
|
|
92
96
|
|
|
93
97
|
```ts
|
|
@@ -111,29 +115,33 @@ Step 4 returns `t()`, `rich()`, `plain()`, `pathFor()`, `urlFor()`, `fileUrl()`,
|
|
|
111
115
|
wired.
|
|
112
116
|
|
|
113
117
|
Step 3's `redirects` is the one way to write a redirect: it types one
|
|
114
|
-
deployment's old URLs against
|
|
118
|
+
deployment's old URLs against _that_ deployment's pages and languages,
|
|
115
119
|
whichever project the site is currently built from. `siteRoutes()` takes what
|
|
116
120
|
it returns and nothing else, resolves it, and writes `_redirects`:
|
|
117
121
|
|
|
118
122
|
```ts
|
|
119
123
|
// projects/rome/redirects.ts
|
|
120
124
|
export default redirects([
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
{ from: "/jobs", to: { route: "careers" }, kind: "permanent" }, // ✗ if Rome has no careers page
|
|
126
|
+
{
|
|
127
|
+
from: "/en/*",
|
|
128
|
+
to: { route: "home", locale: "en-US", splat: true },
|
|
129
|
+
kind: "permanent",
|
|
130
|
+
}, // /en/faq → /en-US/faq
|
|
131
|
+
]);
|
|
124
132
|
```
|
|
125
133
|
|
|
126
134
|
`t()` is bound to a locale and knows each message's `{placeholders}` from its
|
|
127
135
|
text:
|
|
128
136
|
|
|
129
137
|
```ts
|
|
130
|
-
const t = site.translate(locale)
|
|
138
|
+
const t = site.translate(locale);
|
|
131
139
|
|
|
132
|
-
t("contact.intro")
|
|
133
|
-
t("footer.copyright", { company: "Acme Rome", year: "2026" })
|
|
134
|
-
t("footer.copyright")
|
|
135
|
-
t("footer.copyright", { company: "Acme Rome" })
|
|
136
|
-
t("footer.copyright", { company: "A", year: "1", x: "" }) // ✗ {x} is not a placeholder
|
|
140
|
+
t("contact.intro"); // no placeholders, no second argument
|
|
141
|
+
t("footer.copyright", { company: "Acme Rome", year: "2026" });
|
|
142
|
+
t("footer.copyright"); // ✗ params are not optional
|
|
143
|
+
t("footer.copyright", { company: "Acme Rome" }); // ✗ {year} missing
|
|
144
|
+
t("footer.copyright", { company: "A", year: "1", x: "" }); // ✗ {x} is not a placeholder
|
|
137
145
|
```
|
|
138
146
|
|
|
139
147
|
Nothing is auto-injected. Site-wide values are ordinary placeholders, passed
|
|
@@ -154,8 +162,8 @@ typed the English.
|
|
|
154
162
|
```
|
|
155
163
|
|
|
156
164
|
```ts
|
|
157
|
-
const rich = site.rich(locale)
|
|
158
|
-
rich("about.intro", { venue: "contact" })
|
|
165
|
+
const rich = site.rich(locale);
|
|
166
|
+
rich("about.intro", { venue: "contact" }); // the destination, stated once
|
|
159
167
|
```
|
|
160
168
|
|
|
161
169
|
| Mark | Short for | Is |
|
|
@@ -235,7 +243,7 @@ regression-tested by an `@ts-expect-error` suite.
|
|
|
235
243
|
| Two routes resolving to the same URL | build throws, naming both |
|
|
236
244
|
| A site icon that is not square, a multiple of 48px, and PNG | build throws |
|
|
237
245
|
|
|
238
|
-
The runtime throws are the ones types cannot see: facts about a
|
|
246
|
+
The runtime throws are the ones types cannot see: facts about a _file_ (an
|
|
239
247
|
icon's real dimensions), facts only known after merging (URL collisions), or a
|
|
240
248
|
value typed as plain `string` because it came from an env var. See
|
|
241
249
|
[`docs/checks.md`](docs/checks.md) for the full split and how each error is
|
|
@@ -243,8 +251,8 @@ worded.
|
|
|
243
251
|
|
|
244
252
|
## Site-level versus per-page
|
|
245
253
|
|
|
246
|
-
The same question answers it every time:
|
|
247
|
-
the same site
|
|
254
|
+
The same question answers it every time: _would this differ between two pages of
|
|
255
|
+
the same site?_
|
|
248
256
|
|
|
249
257
|
| Per page — passed to `metaFor()` | Site-level — declared in `atlas.project()` |
|
|
250
258
|
| -------------------------------- | ------------------------------------------ |
|
|
@@ -262,7 +270,7 @@ actually renders, and why `twitter:card` is pinned.
|
|
|
262
270
|
|
|
263
271
|
## Config that tracks which pages exist
|
|
264
272
|
|
|
265
|
-
A project switches routes off. Anything defined
|
|
273
|
+
A project switches routes off. Anything defined _alongside_ a route then has two
|
|
266
274
|
ways to rot: the page is built and its config is missing, or the page is gone and
|
|
267
275
|
its config lingers. `PerRoute<typeof site, T>` rejects both.
|
|
268
276
|
|
|
@@ -274,7 +282,7 @@ against a known target gets it.
|
|
|
274
282
|
const heroes = {
|
|
275
283
|
home: { variant: "wide" },
|
|
276
284
|
about: { variant: "tall" },
|
|
277
|
-
} satisfies PerRoute<typeof site, Hero>;
|
|
285
|
+
} satisfies PerRoute<typeof site, Hero>; // ✗ if `about` is off, ✗ if `contact` is on
|
|
278
286
|
```
|
|
279
287
|
|
|
280
288
|
## The rules this package keeps
|
|
@@ -290,9 +298,9 @@ const heroes = {
|
|
|
290
298
|
runs with `"types": []` to keep it honest, and excludes `src/astro/` because
|
|
291
299
|
that is the one place allowed to import the framework. Put something there
|
|
292
300
|
only when it genuinely cannot work without Astro, and prefer moving the
|
|
293
|
-
framework-dependent
|
|
301
|
+
framework-dependent _edge_ there over moving the logic.
|
|
294
302
|
4. **Infer from values, never from type arguments.** When a helper needs to be
|
|
295
|
-
typed against project data, take that data as a
|
|
303
|
+
typed against project data, take that data as a _parameter_ and let TypeScript
|
|
296
304
|
infer it. This is what keeps rules 1 and 2 satisfiable without making callers
|
|
297
305
|
write `<Locale, typeof baseMessages>`.
|
|
298
306
|
|
|
@@ -314,7 +322,7 @@ Whatever those entry points do not export is internal by construction rather tha
|
|
|
314
322
|
by convention, which is what makes the file layout under `src/` a private detail:
|
|
315
323
|
modules can be split, renamed or flattened without touching a consumer.
|
|
316
324
|
|
|
317
|
-
The split
|
|
325
|
+
The split _within_ `astro/` is a further constraint, not tidiness. `./astro` is
|
|
318
326
|
imported by an Astro config, which is evaluated before the build exists — so it
|
|
319
327
|
may use `node:` built-ins but not `astro:assets`. `./astro/images` is the
|
|
320
328
|
opposite: it runs inside the build, from a page, and is unusable from a config.
|
|
@@ -427,18 +435,18 @@ Three things the `.astro` setup works around rather than fixes. Each is open
|
|
|
427
435
|
upstream, and each explains a rule that would otherwise read as an oversight.
|
|
428
436
|
|
|
429
437
|
- [biomejs/biome#10321](https://github.com/biomejs/biome/issues/10321) —
|
|
430
|
-
|
|
438
|
+
_Astro: not formatting inside `{( … )}`_ (`S-Bug-confirmed`). Biome formats
|
|
431
439
|
`.astro` markup but not the interior of an expression, so the 31 `.map()`
|
|
432
440
|
blocks across the templates keep their own indentation while the markup around
|
|
433
441
|
them follows `indentWidth`. Hand-aligned until this lands.
|
|
434
442
|
- [biomejs/biome#9944](https://github.com/biomejs/biome/issues/9944) —
|
|
435
|
-
|
|
443
|
+
_Astro: Parser error inside expression_. Biome parses the inside of `{ }` as
|
|
436
444
|
strict JSX, which rejects things Astro itself accepts. This is why every
|
|
437
445
|
comment in a template is `{/* … */}` and never `<!-- … -->`: an HTML comment
|
|
438
446
|
anywhere inside an expression is a parse error, and the JSX form is also
|
|
439
447
|
stripped from the built HTML rather than shipped to readers.
|
|
440
448
|
- [biomejs/biome#11275](https://github.com/biomejs/biome/issues/11275) —
|
|
441
|
-
|
|
449
|
+
_LSP formatting deletes Astro frontmatter/HTML in multi-project workspaces_
|
|
442
450
|
(`S-Bug-confirmed`). It names `html.experimentalFullSupportEnabled`, which this
|
|
443
451
|
repository sets, and a workspace of several projects, which this repository is.
|
|
444
452
|
The gate above is CLI-only and unaffected; editor format-on-save is the thing
|
package/docs/NOT-BUILT.md
CHANGED
|
@@ -4,7 +4,7 @@ Questions that come up, with the answer and the reasoning behind it. Every one
|
|
|
4
4
|
of these looks like an obvious addition until you check — so the check is
|
|
5
5
|
written down once, rather than redone by whoever thinks of it next.
|
|
6
6
|
|
|
7
|
-
Each answer says what is
|
|
7
|
+
Each answer says what is _not_ built and, where it matters, what is still fine
|
|
8
8
|
to build. The two are easy to run together: "no star-rating markup" is not "no
|
|
9
9
|
reviews", and reading it as the second would lose something worth having.
|
|
10
10
|
|
|
@@ -32,17 +32,17 @@ below still stands: the node renders nowhere, a model does not read it out of a
|
|
|
32
32
|
`<script>` block, and it duplicates the page's own text at several KB per load.
|
|
33
33
|
What is left in its favour is Bing, which has said its LLMs read schema.
|
|
34
34
|
|
|
35
|
-
Which makes this a call a
|
|
35
|
+
Which makes this a call a _site_ makes per page, not one lib makes for every
|
|
36
36
|
repo it syncs to — hence a builder that has to be called and passed into a
|
|
37
37
|
graph, rather than anything wired into `metaFor()`. A project that has not
|
|
38
38
|
weighed the payload against a Copilot payoff it can actually measure should keep
|
|
39
39
|
not calling it, which is the same answer this entry always gave.
|
|
40
40
|
|
|
41
|
-
The two arguments that
|
|
41
|
+
The two arguments that _sound_ decisive and are wrong are recorded below, since
|
|
42
42
|
they will be made again either way.
|
|
43
43
|
|
|
44
|
-
Google restricted FAQ rich results to
|
|
45
|
-
health websites"
|
|
44
|
+
Google restricted FAQ rich results to _"well-known, authoritative government and
|
|
45
|
+
health websites"_ in 2023, then [removed them from Search entirely](https://developers.google.com/search/docs/appearance/structured-data/faqpage)
|
|
46
46
|
on 7 May 2026 and archived the documentation. A commercial site marking up its
|
|
47
47
|
FAQ gets nothing rendered for the effort.
|
|
48
48
|
|
|
@@ -52,17 +52,17 @@ the HTML-to-text pipelines that feed a model routinely drop `<script>`, often th
|
|
|
52
52
|
whole `<head>` with it. Where a block does survive, what arrives is
|
|
53
53
|
`{"@type":"Question","name":…}` as tokens: the punctuation of a graph rather than
|
|
54
54
|
a parsed one. Independent testing through 2025–26 has repeatedly found that
|
|
55
|
-
content present
|
|
55
|
+
content present _only_ in structured data goes unextracted, while the same
|
|
56
56
|
content in visible headings and paragraphs is read reliably.
|
|
57
57
|
|
|
58
58
|
That is not an argument against the nodes lib does build. Schema is parsed at
|
|
59
|
-
|
|
59
|
+
_indexing_ time, so it reaches the AI surfaces built on top of a search index —
|
|
60
60
|
AI Overviews, Copilot — even though a chatbot fetching the page directly sees
|
|
61
61
|
only the visible text. It is an argument against reaching for this one expecting
|
|
62
62
|
a model to pick the answers up, which is the reason it is given every time.
|
|
63
63
|
|
|
64
64
|
The one cost that counts is the payload. Every other node lib builds is metadata
|
|
65
|
-
|
|
65
|
+
_about_ the page — an address, a price, a date. This one is a verbatim _copy of_
|
|
66
66
|
it: every question and every full answer, a second time, in a `<script>` block on
|
|
67
67
|
the page already showing them. A ten-question FAQ is several KB duplicated on
|
|
68
68
|
every load, and page weight is paid per visitor, forever.
|
|
@@ -116,12 +116,12 @@ answer for each is a fact about Google rather than a preference.
|
|
|
116
116
|
clothes. Where there is a real number, `Offer` already carries it.
|
|
117
117
|
- **`ImageObject`.** Both sibling implementations are `{"@type": "ImageObject",
|
|
118
118
|
url}` and nothing else, which is a wrapper around the plain URL string Google
|
|
119
|
-
already accepts. Article documents the field as
|
|
120
|
-
`URL`"
|
|
119
|
+
already accepts. Article documents the field as _"Repeated `ImageObject` or
|
|
120
|
+
`URL`"_ — neither form preferred — and **width and height appear nowhere in
|
|
121
121
|
either the Article guidance or the image-licence guidance**. So having the
|
|
122
122
|
dimensions is not a reason to emit the node; lib does have them for the share
|
|
123
123
|
image, the icon and a video thumbnail, and Google has no use for them.
|
|
124
|
-
What the node
|
|
124
|
+
What the node _does_ unlock is the **Licensable badge** in Google Images, and
|
|
125
125
|
that needs `contentUrl` plus at least one of `license`, `creator`,
|
|
126
126
|
`creditText` or `copyrightNotice` — rights data no project here holds. Build
|
|
127
127
|
it the day someone wants that badge, and build it around the licence fields
|
|
@@ -137,8 +137,8 @@ does and does not buy.
|
|
|
137
137
|
|
|
138
138
|
A section is the part of a publication a piece lives in: Sports, Business,
|
|
139
139
|
Opinion. Open Graph's `article:section` takes one, and schema.org's
|
|
140
|
-
`articleSection` is the same idea. It is
|
|
141
|
-
which are
|
|
140
|
+
`articleSection` is the same idea. It is _where a piece sits_, as against tags,
|
|
141
|
+
which are _what it is about_ — one home versus many labels.
|
|
142
142
|
|
|
143
143
|
Everything here sits in the same place. A `section` field would say `"News"` on
|
|
144
144
|
every post for as long as `/news` is the only feed, which is the URL repeated
|
|
@@ -172,7 +172,7 @@ Reviews a business collects about itself never qualify, so venue-level stars are
|
|
|
172
172
|
unreachable however good the data is — they come from Google Business Profile
|
|
173
173
|
instead, which is outside this repo.
|
|
174
174
|
|
|
175
|
-
Room-level ratings on the `Product`/`Game` node
|
|
175
|
+
Room-level ratings on the `Product`/`Game` node _are_ eligible. They need reviews
|
|
176
176
|
collected on the site itself, shown in full on the page, and averaged over
|
|
177
177
|
exactly the set displayed — never a number typed in beside a shorter list.
|
|
178
178
|
|
|
@@ -191,14 +191,14 @@ language registry](https://www.iana.org/assignments/language-subtag-registry) is
|
|
|
191
191
|
not — which is the case `LanguageTag` in `config.ts` declines and says why.
|
|
192
192
|
|
|
193
193
|
What moved is not the risk, it is the reason to hold the list at all. A country
|
|
194
|
-
menu needs the codes
|
|
194
|
+
menu needs the codes _as data_ — `countriesFor` renders and sorts them per
|
|
195
195
|
locale — and once the list exists, deriving the type from it is free and the
|
|
196
196
|
shape is strictly worse: it admitted 676 strings for 250 countries, including
|
|
197
197
|
the withdrawn codes (`SU`, `YU`, `AN`) that look plausible enough to be typed by
|
|
198
198
|
accident. So `CountryCode` is now `(typeof COUNTRY_CODES)[number]`, defined in
|
|
199
199
|
`countries.ts` and re-exported from `contact.ts` beside `PostalAddress`.
|
|
200
200
|
|
|
201
|
-
`CurrencyCode` stays a shape, because nothing needs a currency
|
|
201
|
+
`CurrencyCode` stays a shape, because nothing needs a currency _list_: a
|
|
202
202
|
currency is written once per project, beside the prices it applies to, and a
|
|
203
203
|
wrong one is visible the first time a page renders. Enumerate it the day
|
|
204
204
|
something renders a currency picker, and not before.
|
|
@@ -221,7 +221,7 @@ without the rejection, but it means carrying a curated list for editor
|
|
|
221
221
|
convenience alone, and a list that earns nothing else is a list that drifts
|
|
222
222
|
unnoticed.
|
|
223
223
|
|
|
224
|
-
The version worth building, if this ever bites: validate the
|
|
224
|
+
The version worth building, if this ever bites: validate the _emitted_ JSON-LD
|
|
225
225
|
against a vocabulary dump, in a test. That sees all 800 types, and sees what no
|
|
226
226
|
type could — a property that is not valid for the type it sits on.
|
|
227
227
|
|
|
@@ -245,12 +245,12 @@ subtitle: [
|
|
|
245
245
|
{ type: "bold", content: "hero.subtitle_text_2" },
|
|
246
246
|
{ type: "text", content: "hero.subtitle_text_3" },
|
|
247
247
|
// …through _7
|
|
248
|
-
]
|
|
248
|
+
];
|
|
249
249
|
```
|
|
250
250
|
|
|
251
251
|
**It fixes two things copy is not allowed to fix.** Word order, which differs
|
|
252
252
|
per language — the array says the bold clause comes second, in every locale,
|
|
253
|
-
forever. And the
|
|
253
|
+
forever. And the _number of runs_, which differs more: a clause English spends
|
|
254
254
|
four fragments on is one word in Greek, and there is no way to write that in an
|
|
255
255
|
array whose length was decided by whoever typed the English. The extra keys get
|
|
256
256
|
padded with empty strings or the spare ones go untranslated, and either way the
|
|
@@ -269,7 +269,7 @@ compared across locales by the machinery that already compares `{placeholders}`
|
|
|
269
269
|
— see `Tokens` in `i18n/placeholders.ts` — so a translation that drops the link
|
|
270
270
|
fails the build rather than the page. That check is the reason marks are safe to
|
|
271
271
|
put in copy at all, and it is what the array shape genuinely did buy: with the
|
|
272
|
-
structure in config, no locale
|
|
272
|
+
structure in config, no locale _could_ lose a link. Marks had to earn that back.
|
|
273
273
|
|
|
274
274
|
**A mark names a slot, never a destination**, and that distinction is the second
|
|
275
275
|
thing the array shape got right and the first version of this got wrong. `venue`
|
|
@@ -294,7 +294,7 @@ whose whole claim is that a translation cannot silently go wrong. The migration
|
|
|
294
294
|
is happening once; this is the moment it costs least to fix, and every store
|
|
295
295
|
collapsed to marks is a store whose copy a translator can finally read.
|
|
296
296
|
|
|
297
|
-
### Should lib carry the consent
|
|
297
|
+
### Should lib carry the consent _banner_ as well as the tags?
|
|
298
298
|
|
|
299
299
|
**No — and the line between the two halves is the point.**
|
|
300
300
|
|
|
@@ -315,12 +315,12 @@ somebody's compliance.
|
|
|
315
315
|
**One part of it is lib's, and is built as of August 2026: whether there should
|
|
316
316
|
be a banner at all.** `consentRequired(analytics)`, with the reasoning in
|
|
317
317
|
`src/analytics/index.ts`. It sits here rather than in each project because it is
|
|
318
|
-
a question about
|
|
318
|
+
a question about _tags_ — which lib owns — wearing the costume of a question
|
|
319
319
|
about banners, and because the obvious check is wrong in both directions.
|
|
320
320
|
Everything above about the banner itself still stands.
|
|
321
321
|
|
|
322
322
|
The seam between the halves is a global — `CONSENT_UPDATE_GLOBAL` — because
|
|
323
|
-
that is the only place they
|
|
323
|
+
that is the only place they _can_ meet: the head script runs before any module
|
|
324
324
|
exists. lib emits an updater covering exactly the signals it defaulted, so a
|
|
325
325
|
banner never names them and the two lists cannot drift. `src/astro/consent.ts`
|
|
326
326
|
carries the browser-side parts that are the same everywhere — reading an answer,
|
|
@@ -351,7 +351,7 @@ Two traps live with this, both spelled out where the code is. A preconnect is
|
|
|
351
351
|
reused only by a request whose CORS mode matches it, so one marked
|
|
352
352
|
`crossorigin` for a classic `<script src>` warms nothing while looking entirely
|
|
353
353
|
correct — fonts are the opposite case, and the two must not be copied between.
|
|
354
|
-
And Tag Manager's loader URL is written
|
|
354
|
+
And Tag Manager's loader URL is written _by_ the inline block, so nothing in the
|
|
355
355
|
markup names that origin until the script has run, which is what makes the
|
|
356
356
|
connection worth stating rather than leaving to the preload scanner.
|
|
357
357
|
|
|
@@ -384,7 +384,7 @@ repo, which is why this entry exists — reading that file is what prompts the
|
|
|
384
384
|
question.
|
|
385
385
|
|
|
386
386
|
Nothing is served by Apache any more. Worth knowing the seam is already there if
|
|
387
|
-
that ever changes: the site resolves rules to plain
|
|
387
|
+
that ever changes: the site resolves rules to plain _data_, and
|
|
388
388
|
`buildCloudflareRedirects` is one renderer over them rather than the only
|
|
389
389
|
possible one, so another host is a new function and no redesign. The two
|
|
390
390
|
non-redirect halves of that `.htaccess` — extension resolution and slash
|
|
@@ -419,13 +419,13 @@ staleness, and `generate()` rebuilding per request does not cure it — it deriv
|
|
|
419
419
|
the same answer from the same frozen inputs.
|
|
420
420
|
|
|
421
421
|
Routes look like the fix, and they genuinely are one. Measured on the b2b
|
|
422
|
-
example: an injected endpoint that reaches the site through the
|
|
422
|
+
example: an injected endpoint that reaches the site through the _page_ graph
|
|
423
423
|
served the edited string immediately, side by side with the integration's
|
|
424
424
|
middleware still serving the old one.
|
|
425
425
|
|
|
426
426
|
**And `_redirects` can be one of those routes** — worth recording, because the
|
|
427
427
|
old comment in `site-routes.ts` said it could not be, and that was wrong. Astro's
|
|
428
|
-
page walker skips a
|
|
428
|
+
page walker skips a _filename_ beginning with `_`; an injected route names its
|
|
429
429
|
pattern and its entrypoint separately, so the underscore lands only on the URL.
|
|
430
430
|
It was built, served in dev, and written to `dist/_redirects` by the route alone.
|
|
431
431
|
|
|
@@ -474,7 +474,7 @@ replaced.
|
|
|
474
474
|
**No, and a post's trail should point at the list, not at the page it is listed
|
|
475
475
|
on.**
|
|
476
476
|
|
|
477
|
-
A `BreadcrumbList` states
|
|
477
|
+
A `BreadcrumbList` states _hierarchical_ position, and pagination is lateral
|
|
478
478
|
movement between siblings at one level: `Home › News` is the trail on `/news`
|
|
479
479
|
and on `/news/page/7` alike. Google renders breadcrumbs in results, so a
|
|
480
480
|
trailing `News (2)` buys noise in the SERP for a listing page that is already a
|
|
@@ -505,7 +505,7 @@ looking at" better than any link can.
|
|
|
505
505
|
With `prefixDefaultLocale: false` the default locale is served at `/about-us`
|
|
506
506
|
and nothing is built under `/en-US/`. Claiming the pages under it one rule at a
|
|
507
507
|
time does not work: slugs are translated per locale, so the URL a reader lands
|
|
508
|
-
on by editing `/el-GR/sxetika-me-emas` down to English keeps the
|
|
508
|
+
on by editing `/el-GR/sxetika-me-emas` down to English keeps the _Greek_ slug —
|
|
509
509
|
`/en-US/sxetika-me-emas`. Covering that means the cross product of the prefix
|
|
510
510
|
with every locale's spelling of every slug, a `_redirects` of hundreds against
|
|
511
511
|
the 2000 a host reads.
|
|
@@ -521,7 +521,7 @@ left out if anything is built under the prefix.
|
|
|
521
521
|
|
|
522
522
|
## Adding to this
|
|
523
523
|
|
|
524
|
-
One question, one answer, one reason. Say what is
|
|
524
|
+
One question, one answer, one reason. Say what is _not_ built and what still is
|
|
525
525
|
— the scope is the part that gets lost. If the answer rests on someone else's
|
|
526
526
|
decision, put it in the first group and name the date, so a reader can tell a
|
|
527
527
|
settled fact from one worth re-checking.
|
package/docs/checks.md
CHANGED
|
@@ -18,12 +18,12 @@ type-checking would pass all of it.
|
|
|
18
18
|
|
|
19
19
|
## Compile errors, and where they are stated
|
|
20
20
|
|
|
21
|
-
Most guarantees are compile errors, stated in a
|
|
21
|
+
Most guarantees are compile errors, stated in a _constraint_ rather than in the
|
|
22
22
|
parameter type — in parameter position a single unrelated mistake widens the
|
|
23
23
|
inferred type and every key then reports as malformed, burying the real error in
|
|
24
24
|
a cascade.
|
|
25
25
|
|
|
26
|
-
That rule inverts when the validator maps the
|
|
26
|
+
That rule inverts when the validator maps the _whole_ object rather than one of
|
|
27
27
|
its properties, as the slug-depth checks do: in the constraint, any unrelated
|
|
28
28
|
malformation makes the argument itself unassignable and TypeScript reports at the
|
|
29
29
|
call site instead of the offending line. Those two live in parameter position for
|
|
@@ -50,7 +50,7 @@ sees.
|
|
|
50
50
|
| a `lastmod` that is not a `YYYY-MM-DD` calendar date | |
|
|
51
51
|
| config for a route this project does not build, or missing for one it does (`PerRoute`) | |
|
|
52
52
|
|
|
53
|
-
The runtime checks are the ones types cannot see: facts about a
|
|
53
|
+
The runtime checks are the ones types cannot see: facts about a _file_ (an icon's
|
|
54
54
|
real dimensions), facts only known after merging (URL collisions), or a value
|
|
55
55
|
typed as plain `string` because it came from an env var, where the literal checks
|
|
56
56
|
pass vacuously.
|
|
@@ -87,7 +87,7 @@ consuming project's own `type-tests/`, which is also the only place the
|
|
|
87
87
|
augmentation is real. Both examples carry one:
|
|
88
88
|
[`b2c`](../examples/b2c/type-tests/wiring.ts) asserts against a `public/` with
|
|
89
89
|
two files in it, and [`b2b`](../examples/b2b/type-tests/wiring.ts) against one
|
|
90
|
-
with none, where the union is empty and
|
|
90
|
+
with none, where the union is empty and _every_ path is rejected.
|
|
91
91
|
|
|
92
92
|
Declared analytics events are the other augmentation, and the opposite case:
|
|
93
93
|
nothing is inferred, so a declaration means the same thing here as in a project.
|
|
@@ -152,10 +152,10 @@ does not carry; see [`client-scripts.md`](client-scripts.md).
|
|
|
152
152
|
|
|
153
153
|
## What a consumer checks
|
|
154
154
|
|
|
155
|
-
A consuming project keeps its own thin suite for
|
|
155
|
+
A consuming project keeps its own thin suite for _wiring_ — that its catalog,
|
|
156
156
|
routes and config actually satisfy these contracts, and that its own types have
|
|
157
157
|
not been loosened by a stray `any` or a dropped `satisfies`. It does not restate
|
|
158
158
|
the machinery's behaviour; that is what the two suites above are for.
|
|
159
159
|
|
|
160
160
|
Both examples' `type-tests/wiring.ts` are deliberately short, and every assertion
|
|
161
|
-
in them names something that must stay
|
|
161
|
+
in them names something that must stay _rejected_.
|
package/docs/client-scripts.md
CHANGED
|
@@ -22,11 +22,11 @@ than a view of something else, and a `hidden` on a clear button, a no-results
|
|
|
22
22
|
message or a section with nothing left in it is binary and derived — there is no
|
|
23
23
|
version of "the clear button while the field is empty" that is a matter of taste.
|
|
24
24
|
It sets no class, no style and no `aria`, which is where the choices live. It
|
|
25
|
-
also never
|
|
25
|
+
also never _finds_ an element: it is handed the ones a project looked up, so no
|
|
26
26
|
attribute name in this package has to be matched by any consumer.
|
|
27
27
|
|
|
28
28
|
`youtube` is the other exception, and the one that cannot be avoided: the player
|
|
29
|
-
|
|
29
|
+
_is_ the behaviour, so something has to put an iframe in the page. What it sets
|
|
30
30
|
on it has no taste in it — the address, the title, the permissions, full screen,
|
|
31
31
|
the referrer policy — and everything that does, classes above all, comes from the
|
|
32
32
|
caller through a callback. It too is handed the button and the box rather than
|
|
@@ -78,10 +78,10 @@ the options, and when one drifts nothing reports it — a listener added without
|
|
|
78
78
|
its twin simply survives teardown.
|
|
79
79
|
|
|
80
80
|
```ts
|
|
81
|
-
const detach = slider.attach(viewport);
|
|
82
|
-
const detach = list.attach();
|
|
83
|
-
const detach = loop.attach(video);
|
|
84
|
-
const detach = trailer.attach(button, box);
|
|
81
|
+
const detach = slider.attach(viewport); // carousel: one element
|
|
82
|
+
const detach = list.attach(); // filters: no element, only popstate
|
|
83
|
+
const detach = loop.attach(video); // background-video: the <video>
|
|
84
|
+
const detach = trailer.attach(button, box); // youtube: what is pressed, what it replaces
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
`element` says the same thing in the shape a custom element needs: its function
|
|
@@ -90,8 +90,10 @@ element leaves, and what it returns is the undo for everything else.
|
|
|
90
90
|
|
|
91
91
|
```ts
|
|
92
92
|
export const thing = element("go-thing", ({ root, signal }) => {
|
|
93
|
-
ref(root, "[data-thing-button]").addEventListener("click", open, {
|
|
94
|
-
|
|
93
|
+
ref(root, "[data-thing-button]").addEventListener("click", open, {
|
|
94
|
+
signal,
|
|
95
|
+
}); // ends with the visit
|
|
96
|
+
return loop.attach(root); // and so does this
|
|
95
97
|
});
|
|
96
98
|
```
|
|
97
99
|
|
|
@@ -129,7 +131,7 @@ const detach = list.attach();
|
|
|
129
131
|
|
|
130
132
|
A field is its kind, and its URL parameter is its own name — to rename the
|
|
131
133
|
parameter, rename the field. `{ kind: "text", url: false }` keeps a field out of
|
|
132
|
-
the URL, for a list whose page owns the URL itself. `values` is typed
|
|
134
|
+
the URL, for a list whose page owns the URL itself. `values` is typed _from_
|
|
133
135
|
`fields`, so a `flag` demands a boolean and a mismatch is a compile error rather
|
|
134
136
|
than an item that silently never matches. It is read once per item, at
|
|
135
137
|
construction. `matched` is a set of the caller's own items, so there is no key
|
|
@@ -149,7 +151,7 @@ and then answers to any of them:
|
|
|
149
151
|
values: (room) => ({ category: data(room, "categories").split(" ") }),
|
|
150
152
|
```
|
|
151
153
|
|
|
152
|
-
An escape room is adventure
|
|
154
|
+
An escape room is adventure _and_ sci-fi; a film is a comedy _and_ a drama. This
|
|
153
155
|
is a widening of `choice` rather than a fourth kind, because a kind for it would
|
|
154
156
|
carry the same state, the same parameter, the same `set` semantics and the same
|
|
155
157
|
control, and differ by one operator — two kinds doing one job, which is what the
|
|
@@ -158,7 +160,7 @@ bare string and is untouched.
|
|
|
158
160
|
|
|
159
161
|
**`text` folds both sides** — `NFD`, drop the combining marks, lowercase, trim.
|
|
160
162
|
This is the piece that is missing nearly everywhere, and it is not cosmetic: a
|
|
161
|
-
reader who types `malaga` is otherwise told there is no
|
|
163
|
+
reader who types `malaga` is otherwise told there is no _Málaga_, and every
|
|
162
164
|
Romanian entry spelled with `ă`, `ș` or `ț` hides from anyone whose keyboard does
|
|
163
165
|
not carry them. Item text is folded once at construction; only the query is
|
|
164
166
|
folded per render.
|
|
@@ -170,8 +172,8 @@ whole query as one substring makes that accident load-bearing, so a two-word
|
|
|
170
172
|
query fails whenever the words are not adjacent.
|
|
171
173
|
|
|
172
174
|
**`flag` is a narrowing, not a two-way filter.** Off keeps everything rather than
|
|
173
|
-
keeping the items that are
|
|
174
|
-
"out of stock". On, off and
|
|
175
|
+
keeping the items that are _not_ flagged — "In stock", unchecked, does not mean
|
|
176
|
+
"out of stock". On, off and _either_ is a `choice` with two values; folding that
|
|
175
177
|
into a toggle produces a control with a third position nothing can reach.
|
|
176
178
|
|
|
177
179
|
Flags are not exclusive. A second toggle is another **field**, not another value
|
|
@@ -183,7 +185,7 @@ parameters or comma-joined) that nothing has needed yet.
|
|
|
183
185
|
### Facet counts: `matchedWithout`
|
|
184
186
|
|
|
185
187
|
A dropdown that says how many results each option would give needs a different
|
|
186
|
-
set from `matched`: every field
|
|
188
|
+
set from `matched`: every field _except its own_. Counted against `matched`, a
|
|
187
189
|
facet whose value is already picked can only ever offer that value — every other
|
|
188
190
|
option reads zero, because the items holding them were just filtered out by the
|
|
189
191
|
very field being asked about.
|
|
@@ -220,7 +222,7 @@ construction.
|
|
|
220
222
|
1. **Replace while typing, push on a choice.** An input emits an event per
|
|
221
223
|
keystroke, and history should not fill with a word being spelled; picking a
|
|
222
224
|
category is one deliberate act the back button should undo. The default comes
|
|
223
|
-
from the kind, but it is really a fact about the
|
|
225
|
+
from the kind, but it is really a fact about the _input_ — a `<select>` or a
|
|
224
226
|
preset link bound to a `text` field wants `list.set("q", v, { history: "push" })`.
|
|
225
227
|
2. **Empty parameters are dropped**, and an empty state is the bare path rather
|
|
226
228
|
than a trailing `?`. Both spellings are the same page; one of them is worth
|
|
@@ -232,7 +234,7 @@ construction.
|
|
|
232
234
|
because nobody deleted it. Fields write in declaration order, so one state
|
|
233
235
|
always produces one URL.
|
|
234
236
|
|
|
235
|
-
`attach` reads the URL
|
|
237
|
+
`attach` reads the URL _before_ the first `onChange`. That is what lets a list
|
|
236
238
|
work before this script arrives: a `<form method="get">` submits, the server
|
|
237
239
|
renders the filtered page, and the first announcement continues from that state
|
|
238
240
|
instead of flashing the whole list back. Nothing writes to the URL before
|
|
@@ -263,7 +265,8 @@ byte.
|
|
|
263
265
|
|
|
264
266
|
```ts
|
|
265
267
|
const list = filters({
|
|
266
|
-
fields,
|
|
268
|
+
fields,
|
|
269
|
+
items,
|
|
267
270
|
onChange({ matched }) {
|
|
268
271
|
// …everything this list draws for itself
|
|
269
272
|
},
|
|
@@ -317,13 +320,13 @@ onChange({ matched }) {
|
|
|
317
320
|
just written it and that is the shortest true description of what survived — and
|
|
318
321
|
because reading it is what lets the helper nest. A country is empty when every
|
|
319
322
|
city in it is hidden; a region is empty when every country in it is hidden,
|
|
320
|
-
|
|
323
|
+
_including the ones the previous call just hid_.
|
|
321
324
|
|
|
322
325
|
**Which makes the order load-bearing: innermost first.**
|
|
323
326
|
|
|
324
327
|
```ts
|
|
325
328
|
hideEmpty(countries, (c) => citiesIn.get(c) ?? []);
|
|
326
|
-
hideEmpty(regions,
|
|
329
|
+
hideEmpty(regions, (r) => countriesIn.get(r) ?? []);
|
|
327
330
|
```
|
|
328
331
|
|
|
329
332
|
Run the other way round, the regions are judged against countries nothing has
|
|
@@ -422,9 +425,20 @@ import AtlasElement from "@escape-game-over/atlas/astro/element";
|
|
|
422
425
|
|
|
423
426
|
<script src="./faq.ts"></script>
|
|
424
427
|
|
|
425
|
-
<AtlasElement
|
|
426
|
-
|
|
427
|
-
|
|
428
|
+
<AtlasElement
|
|
429
|
+
of={faq}
|
|
430
|
+
class="…"
|
|
431
|
+
>
|
|
432
|
+
<input
|
|
433
|
+
data-faq-search
|
|
434
|
+
type="search"
|
|
435
|
+
>
|
|
436
|
+
<details
|
|
437
|
+
data-faq-row
|
|
438
|
+
data-key={key}
|
|
439
|
+
>
|
|
440
|
+
…
|
|
441
|
+
</details>
|
|
428
442
|
</AtlasElement>
|
|
429
443
|
```
|
|
430
444
|
|
|
@@ -456,11 +470,15 @@ attribute, and the script writes only that run:
|
|
|
456
470
|
```
|
|
457
471
|
|
|
458
472
|
```astro
|
|
459
|
-
<RichText
|
|
473
|
+
<RichText
|
|
474
|
+
spans={rich("faq.matched", { count: "" })}
|
|
475
|
+
attrs={{ count: { "data-faq-count": "" } }}
|
|
476
|
+
/>
|
|
460
477
|
```
|
|
461
478
|
|
|
462
479
|
```ts
|
|
463
|
-
for (const each of refs(root, "[data-faq-count]"))
|
|
480
|
+
for (const each of refs(root, "[data-faq-count]"))
|
|
481
|
+
each.textContent = String(hits.size);
|
|
464
482
|
```
|
|
465
483
|
|
|
466
484
|
Marks are parsed before placeholders are filled, so a run filled with `""`
|
|
@@ -557,7 +575,7 @@ What it owns is what every hero loop needs and most get quietly wrong:
|
|
|
557
575
|
- **The right cut after a rotation.** A browser chooses among `<source media>`
|
|
558
576
|
once, when the element loads, and never again, so a phone turned sideways
|
|
559
577
|
keeps the portrait cut. This listens to every source's query and calls
|
|
560
|
-
`load()` when the answer changes —
|
|
578
|
+
`load()` when the answer changes — _the answer_, not any crossing: a later
|
|
561
579
|
source's query flipping while an earlier one still matches chooses the same
|
|
562
580
|
file, and a reload would restart the loop from its first frame for nothing.
|
|
563
581
|
- **The reader's pause surviving that reload.** A reload resets the element to
|
|
@@ -633,7 +651,7 @@ carries a name only, so its entries are all `undefined`. Snap and Axon take no
|
|
|
633
651
|
declarations because they accept no names but their own.
|
|
634
652
|
|
|
635
653
|
**The file must be a module** — the `export {}` above. Without an import or an
|
|
636
|
-
export, `declare module`
|
|
654
|
+
export, `declare module` _replaces_ the package's types instead of adding to
|
|
637
655
|
them, and every import from it breaks.
|
|
638
656
|
|
|
639
657
|
**Declared data is sent as written, so it names what the visitor chose, never
|
|
@@ -651,7 +669,7 @@ the same reason — that sends:
|
|
|
651
669
|
|
|
652
670
|
- with `tagIds`: `gtag("event", name, data)`, which `gtag.js` reads;
|
|
653
671
|
- with `containerIds`: `dataLayer.push({ event: name, ...data })`, which is what
|
|
654
|
-
a Tag Manager
|
|
672
|
+
a Tag Manager _Custom Event_ trigger listens for — the `dataLayer.push` a
|
|
655
673
|
marketing agency's brief asks for;
|
|
656
674
|
- with both, both. A container that also runs a GA4 tag on the same event then
|
|
657
675
|
counts it twice; that is set up in Tag Manager, where lib cannot see it.
|
|
@@ -673,7 +691,7 @@ right properties is a faithful stand-in.
|
|
|
673
691
|
listener, covering the matching, `matchedWithout`, the four URL decisions and
|
|
674
692
|
attach/detach.
|
|
675
693
|
- `tests/filters-view.test.ts` fakes plain objects with a `hidden` property,
|
|
676
|
-
pins the write-back guard by counting
|
|
694
|
+
pins the write-back guard by counting _writes_ — so deleting the guard fails
|
|
677
695
|
rather than passing on an identical value — and pins the nesting order in
|
|
678
696
|
both directions.
|
|
679
697
|
- `tests/filtering.test.ts` stubs `document` with a `startViewTransition` whose
|
package/docs/rich-text.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Rich text: the marks a message may carry
|
|
2
2
|
|
|
3
|
-
`t()` prints a message. `rich()` reads the marks
|
|
3
|
+
`t()` prints a message. `rich()` reads the marks _in_ one and hands back runs —
|
|
4
4
|
so a sentence with emphasis, a link and a line break in it stays **one message**,
|
|
5
5
|
which is the only shape a translator can reorder.
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ which is the only shape a translator can reorder.
|
|
|
13
13
|
|
|
14
14
|
```ts
|
|
15
15
|
const rich = site.rich(locale);
|
|
16
|
-
rich("about.intro", { venue: "contact" })
|
|
16
|
+
rich("about.intro", { venue: "contact" }); // the destination, stated once
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Greek moves the link to the front and spends fewer runs on the clause. Neither is
|
|
@@ -49,7 +49,7 @@ sentence the way their tags do, and `[x]` would not.
|
|
|
49
49
|
|
|
50
50
|
`[b]` is the one presentational-looking mark lib names outright, and it is not
|
|
51
51
|
presentational — it is `<strong>`, it survives into plain text as emphasis a
|
|
52
|
-
screen reader announces. Everything that is genuinely a
|
|
52
|
+
screen reader announces. Everything that is genuinely a _style_ is `[v:]`.
|
|
53
53
|
|
|
54
54
|
## An argument is a role or a slot, never a value
|
|
55
55
|
|
|
@@ -108,7 +108,7 @@ and fills none.
|
|
|
108
108
|
## Marks do not nest
|
|
109
109
|
|
|
110
110
|
One mark is open at a time. `[b]bold [v:accent]and red[/v][/b]` is refused rather
|
|
111
|
-
than flattened, because a run is bold
|
|
111
|
+
than flattened, because a run is bold _or_ it carries a variant and there is
|
|
112
112
|
nowhere for the second one to go.
|
|
113
113
|
|
|
114
114
|
If the combination is wanted, it is a role: give one variant both properties and
|
|
@@ -119,7 +119,7 @@ name it once. (The shape this replaces answered the same limit by inventing a
|
|
|
119
119
|
without closing the mark:
|
|
120
120
|
|
|
121
121
|
```ts
|
|
122
|
-
parseMarks("[b]One[br]Two[/b]")
|
|
122
|
+
parseMarks("[b]One[br]Two[/b]");
|
|
123
123
|
// [ bold "One", break, bold "Two" ]
|
|
124
124
|
```
|
|
125
125
|
|
|
@@ -212,7 +212,7 @@ every link in every description in order to throw them all away.
|
|
|
212
212
|
|
|
213
213
|
```ts
|
|
214
214
|
const plainText = site.plain(locale);
|
|
215
|
-
plainText("about.intro", { company })
|
|
215
|
+
plainText("about.intro", { company }); // no `venue`, no `ask`
|
|
216
216
|
```
|
|
217
217
|
|
|
218
218
|
Reach for `t()` when the copy is structurally plain and should stay that way — a
|
|
@@ -229,7 +229,10 @@ and escapes all the copy. A `[v:]` role and an in-page anchor come out as their
|
|
|
229
229
|
words.
|
|
230
230
|
|
|
231
231
|
```ts
|
|
232
|
-
faqPage(
|
|
232
|
+
faqPage(
|
|
233
|
+
[{ question, answer: html(rich("faq.city.a", { network: "network" })) }],
|
|
234
|
+
at
|
|
235
|
+
);
|
|
233
236
|
```
|
|
234
237
|
|
|
235
238
|
## The renderer half
|
|
@@ -241,7 +244,11 @@ per `[v:name]` and one for links — usually in a small wrapper
|
|
|
241
244
|
|
|
242
245
|
```astro
|
|
243
246
|
<p class="…">
|
|
244
|
-
<RichText
|
|
247
|
+
<RichText
|
|
248
|
+
spans={rich("about.intro", { venue: "contact" })}
|
|
249
|
+
variants={{ accent: "…" }}
|
|
250
|
+
link="…"
|
|
251
|
+
/>
|
|
245
252
|
</p>
|
|
246
253
|
```
|
|
247
254
|
|
package/docs/share-images.md
CHANGED
|
@@ -54,4 +54,4 @@ padded — and the failure was invisible, because a card that renders is a card
|
|
|
54
54
|
that looks like it worked. One value, no threshold to sit near, nothing to
|
|
55
55
|
override. `tests/meta.test.ts` pins it at three sizes including a square one.
|
|
56
56
|
|
|
57
|
-
What Atlas does
|
|
57
|
+
What Atlas does _not_ model is a second image for X alone: one page, one picture.
|
package/docs/toolchain.md
CHANGED
|
@@ -28,7 +28,7 @@ it matters. Track
|
|
|
28
28
|
One without the other is the trap. `experimentalFullSupportEnabled` moves
|
|
29
29
|
`.astro` onto the HTML-family path, where the formatter is off unless it is
|
|
30
30
|
asked for — so setting only the first drops every `.astro` file from
|
|
31
|
-
`biome format` entirely. It reports them as
|
|
31
|
+
`biome format` entirely. It reports them as _ignored_ rather than as errors, so
|
|
32
32
|
`check-fmt` stays green while nothing is being formatted at all.
|
|
33
33
|
|
|
34
34
|
With both set, `noUnusedVariables` and `noUnusedImports` resolve template usage
|
|
@@ -56,7 +56,7 @@ single catch-all page in each example, from the route registry.
|
|
|
56
56
|
## Formatting
|
|
57
57
|
|
|
58
58
|
4-space indent, double quotes, `es5` trailing commas, LF. Biome formats `.astro`
|
|
59
|
-
frontmatter
|
|
59
|
+
frontmatter _and_ markup, but not the inside of a `{ }` expression — so the
|
|
60
60
|
`.map()` blocks in the examples stay hand-aligned while the markup around them
|
|
61
61
|
follows `indentWidth`. See the open Biome issues in the README.
|
|
62
62
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escape-game-over/atlas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.84",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
|
|
6
6
|
"private": false,
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"check-fmt": "biome check . --error-on-warnings"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@types/google.maps": "3.66.
|
|
52
|
+
"@types/google.maps": "3.66.9"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@types/node": ">=22",
|
|
@@ -58,11 +58,11 @@
|
|
|
58
58
|
"vite": ">= 8"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@biomejs/biome": "2.
|
|
62
|
-
"@types/node": "26.6.
|
|
63
|
-
"@vitest/coverage-istanbul": "5.0
|
|
64
|
-
"astro": "7.4.
|
|
61
|
+
"@biomejs/biome": "2.6.4",
|
|
62
|
+
"@types/node": "26.6.6",
|
|
63
|
+
"@vitest/coverage-istanbul": "5.1.0",
|
|
64
|
+
"astro": "7.4.5",
|
|
65
65
|
"typescript": "6.0.3",
|
|
66
|
-
"vitest": "5.0
|
|
66
|
+
"vitest": "5.1.0"
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type PostalAddress,
|
|
6
6
|
} from "../contact.ts";
|
|
7
7
|
import type { ImageAsset } from "../image.ts";
|
|
8
|
+
import type { IsoDate } from "../types.ts";
|
|
8
9
|
import { absoluteUrl, type HttpsUrl, joinUrl, type UrlPath } from "../url.ts";
|
|
9
10
|
import { organizationId } from "./ids.ts";
|
|
10
11
|
import { alternateName, type JsonLdNode } from "./node.ts";
|
|
@@ -15,6 +16,16 @@ export interface OrganizationInput {
|
|
|
15
16
|
readonly name: string;
|
|
16
17
|
/** The brand's own home. Also its identity, see `@id` below. */
|
|
17
18
|
readonly url: HttpsUrl;
|
|
19
|
+
/**
|
|
20
|
+
* What the brand is, in a sentence. Google lists it as a recommended
|
|
21
|
+
* property of an organization.
|
|
22
|
+
*/
|
|
23
|
+
readonly description?: string;
|
|
24
|
+
/**
|
|
25
|
+
* When the brand was founded: a full date, or just the year where that is
|
|
26
|
+
* all anyone can honestly state. Both are valid ISO 8601.
|
|
27
|
+
*/
|
|
28
|
+
readonly foundingDate?: IsoDate | `${number}`;
|
|
18
29
|
/**
|
|
19
30
|
* Another name the brand actually goes by — an acronym, a short form.
|
|
20
31
|
*
|
|
@@ -162,6 +173,12 @@ export function organization(brand: OrganizationInput): JsonLdNode {
|
|
|
162
173
|
name: brand.name,
|
|
163
174
|
url: brand.url,
|
|
164
175
|
...alternateName(brand.name, brand.alternateName),
|
|
176
|
+
...(brand.description === undefined
|
|
177
|
+
? {}
|
|
178
|
+
: { description: brand.description }),
|
|
179
|
+
...(brand.foundingDate === undefined
|
|
180
|
+
? {}
|
|
181
|
+
: { foundingDate: brand.foundingDate }),
|
|
165
182
|
...(brand.logo === undefined
|
|
166
183
|
? {}
|
|
167
184
|
: { logo: joinUrl(brand.url, brand.logo) }),
|
package/src/llms.ts
CHANGED
|
@@ -68,6 +68,11 @@ export interface LlmsInput {
|
|
|
68
68
|
readonly tag: string;
|
|
69
69
|
readonly url: HttpsUrl;
|
|
70
70
|
}[];
|
|
71
|
+
/**
|
|
72
|
+
* Whether any page's slug differs between languages. Decides what the file
|
|
73
|
+
* tells a reader about reaching the same page in another one.
|
|
74
|
+
*/
|
|
75
|
+
readonly translatedSlugs?: boolean;
|
|
71
76
|
readonly sections: readonly LlmsSection[];
|
|
72
77
|
}
|
|
73
78
|
|
|
@@ -104,14 +109,18 @@ export function buildLlms(input: LlmsInput): GeneratedFile {
|
|
|
104
109
|
// Which language the links are in, and how to get to the others — stated
|
|
105
110
|
// only when there *are* others.
|
|
106
111
|
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
+
// Whether "swap the prefix" works depends on the site, so it is stated only
|
|
113
|
+
// where it holds. Where a slug is translated, the Greek `/about-us` is
|
|
114
|
+
// `/el-GR/sxetika-me-emas` and a reader told to substitute the prefix would
|
|
115
|
+
// construct a URL that 404s; the sitemap is where that mapping lives, one
|
|
116
|
+
// `xhtml:link` per page per language.
|
|
112
117
|
if (input.otherLanguages !== undefined && input.otherLanguages.length > 0) {
|
|
113
118
|
lines.push(
|
|
114
|
-
`Every URL below is the ${input.language.tag} version.
|
|
119
|
+
`Every URL below is the ${input.language.tag} version. ${
|
|
120
|
+
input.translatedSlugs === true
|
|
121
|
+
? "Other languages might translate their slugs, so the same page is not always this URL with a different prefix —"
|
|
122
|
+
: "Other languages use the same path under their own address above —"
|
|
123
|
+
} the sitemap lists each page in every language.`,
|
|
115
124
|
""
|
|
116
125
|
);
|
|
117
126
|
}
|
package/src/site/create.ts
CHANGED
|
@@ -761,6 +761,18 @@ export function createSite<
|
|
|
761
761
|
tag: localeMeta[other].htmlLang,
|
|
762
762
|
url: joinUrl(siteUrl, buildPath("", other, pathContext)),
|
|
763
763
|
})),
|
|
764
|
+
// Read off the routes, so the file cannot claim something the URLs
|
|
765
|
+
// do not do.
|
|
766
|
+
translatedSlugs: enabledRouteIds.some((id) => {
|
|
767
|
+
const route = resolved[id];
|
|
768
|
+
return (
|
|
769
|
+
route !== undefined &&
|
|
770
|
+
others.some(
|
|
771
|
+
(other) =>
|
|
772
|
+
slugFor(route, other) !== slugFor(route, locale)
|
|
773
|
+
)
|
|
774
|
+
);
|
|
775
|
+
}),
|
|
764
776
|
sections,
|
|
765
777
|
});
|
|
766
778
|
}
|