@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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# What we don't build, and why
|
|
2
|
+
|
|
3
|
+
Questions that come up, with the answer and the reasoning behind it. Every one
|
|
4
|
+
of these looks like an obvious addition until you check — so the check is
|
|
5
|
+
written down once, rather than redone by whoever thinks of it next.
|
|
6
|
+
|
|
7
|
+
Each answer says what is *not* built and, where it matters, what is still fine
|
|
8
|
+
to build. The two are easy to run together: "no FAQ structured data" is not "no
|
|
9
|
+
FAQ page", and reading it as the second would lose something worth having.
|
|
10
|
+
|
|
11
|
+
Kept beside the code rather than in the backlog, because a backlog is a list of
|
|
12
|
+
things still to do and every one of these is a thing not to do.
|
|
13
|
+
|
|
14
|
+
**Two groups.** The first rests on decisions the search engines made and could
|
|
15
|
+
reverse — re-read it occasionally. The second turns on how our data and our
|
|
16
|
+
sites are shaped, and will not change on its own.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Answers that could change
|
|
21
|
+
|
|
22
|
+
### Should we mark up our FAQ with `FAQPage`?
|
|
23
|
+
|
|
24
|
+
**Not now — and this one genuinely depends. The page is a fine idea either way.**
|
|
25
|
+
|
|
26
|
+
The most conditional answer in this file, so it is worth reading before
|
|
27
|
+
assuming. Three things hold it shut today and the first two could move. Two
|
|
28
|
+
further arguments that *sound* decisive are wrong, and are recorded below so
|
|
29
|
+
they stop being made.
|
|
30
|
+
|
|
31
|
+
Google restricted FAQ rich results to *"well-known, authoritative government and
|
|
32
|
+
health websites"* in 2023, then [removed them from Search entirely](https://developers.google.com/search/docs/appearance/structured-data/faqpage)
|
|
33
|
+
on 7 May 2026 and archived the documentation. A commercial site marking up its
|
|
34
|
+
FAQ gets nothing rendered for the effort.
|
|
35
|
+
|
|
36
|
+
**And no, a model does not read it either** — the follow-up worth recording,
|
|
37
|
+
because it is the one that reopens this. JSON-LD sits in a `<script>` block, and
|
|
38
|
+
the HTML-to-text pipelines that feed a model routinely drop `<script>`, often the
|
|
39
|
+
whole `<head>` with it. Where a block does survive, what arrives is
|
|
40
|
+
`{"@type":"Question","name":…}` as tokens: the punctuation of a graph rather than
|
|
41
|
+
a parsed one. Independent testing through 2025–26 has repeatedly found that
|
|
42
|
+
content present *only* in structured data goes unextracted, while the same
|
|
43
|
+
content in visible headings and paragraphs is read reliably.
|
|
44
|
+
|
|
45
|
+
That is not an argument against the nodes lib does build. Schema is parsed at
|
|
46
|
+
*indexing* time, so it reaches the AI surfaces built on top of a search index —
|
|
47
|
+
AI Overviews, Copilot — even though a chatbot fetching the page directly sees
|
|
48
|
+
only the visible text. It is an argument against adding one that no longer
|
|
49
|
+
renders anywhere and is not read directly either.
|
|
50
|
+
|
|
51
|
+
The one cost that counts is the payload. Every other node lib builds is metadata
|
|
52
|
+
*about* the page — an address, a price, a date. This one is a verbatim *copy of*
|
|
53
|
+
it: every question and every full answer, a second time, in a `<script>` block on
|
|
54
|
+
the page already showing them. A ten-question FAQ is several KB duplicated on
|
|
55
|
+
every load, and page weight is paid per visitor, forever.
|
|
56
|
+
|
|
57
|
+
**What is not a reason, and was argued twice before landing here.** That it
|
|
58
|
+
would be hand-maintained: false — the FAQ lives in config as question-and-answer
|
|
59
|
+
pairs the page renders, so the node derives from that same array exactly as
|
|
60
|
+
`Product` derives from the price the page shows. Nothing to keep in step,
|
|
61
|
+
nothing to drift. And that it is code to write in lib: lib exists to absorb that
|
|
62
|
+
cost once for every repo it syncs to, so "it is 150 lines" is an argument
|
|
63
|
+
against lib itself, and applied evenly it forbids everything already in here.
|
|
64
|
+
Neither survives — which is why what stands above is about what consumes the
|
|
65
|
+
markup and what it costs to send, and not about how much work it is to write.
|
|
66
|
+
|
|
67
|
+
**What would flip it.** Google reinstating FAQ rich results is the obvious one.
|
|
68
|
+
The likelier one is Bing: it has said outright that its LLMs read schema, which
|
|
69
|
+
is the single live third-party payoff here, so if Copilot visibility ever
|
|
70
|
+
becomes something these venues actually measure rather than speculate about,
|
|
71
|
+
build it. The data is already in config, so it is a small build on the day the
|
|
72
|
+
trigger fires — which is the reason to leave it until then rather than guess now.
|
|
73
|
+
|
|
74
|
+
Writing the page is a separate question and the answer there is yes: readers
|
|
75
|
+
want one, and `llms.txt` carries question-and-answer copy unusually well, since
|
|
76
|
+
that is close to the form a model is asked things in. It is content, needs
|
|
77
|
+
nothing from lib, and — today — no structured data.
|
|
78
|
+
|
|
79
|
+
### The other node types the sibling projects emit — which of them do we want?
|
|
80
|
+
|
|
81
|
+
**These six: no. `Service` and `OfferCatalog` were the ones worth taking, and
|
|
82
|
+
they are built.**
|
|
83
|
+
|
|
84
|
+
A sweep of the four Next.js sites this package was extracted from turned up
|
|
85
|
+
every schema type they emit between them. Most of it lib already had. What was
|
|
86
|
+
left is listed here so the sweep does not get run again from scratch, since the
|
|
87
|
+
answer for each is a fact about Google rather than a preference.
|
|
88
|
+
|
|
89
|
+
- **`ItemList` / `CollectionPage`.** Google renders `ItemList` as a carousel in
|
|
90
|
+
a handful of verticals — recipe, course, movie, restaurant — and as nothing at
|
|
91
|
+
all outside them. A list page for challenges or news is outside them.
|
|
92
|
+
- **`WebPage` / `WebPageElement` / `ContactPage`.** Google's guidance is that
|
|
93
|
+
generic page-level markup is not needed; the page already says it is a page.
|
|
94
|
+
Nothing reads these that does not already have the HTML.
|
|
95
|
+
- **`LocationFeatureSpecification`.** Amenity features are used for lodging.
|
|
96
|
+
A venue's facilities are not that, and nothing renders them here.
|
|
97
|
+
- **`ContactPoint` / `ServiceChannel`.** Ruled out with the retired Corporate
|
|
98
|
+
Contact feature — see `organization()` for the full reasoning. `ServiceChannel`
|
|
99
|
+
is the same idea one node over and falls with it.
|
|
100
|
+
- **`PriceSpecification`.** As the siblings use it, it carries a description and
|
|
101
|
+
a currency and no amount — a "contact us" placeholder wearing a price's
|
|
102
|
+
clothes. Where there is a real number, `Offer` already carries it.
|
|
103
|
+
- **`ImageObject`.** Both sibling implementations are `{"@type": "ImageObject",
|
|
104
|
+
url}` and nothing else, which is a wrapper around the plain URL string Google
|
|
105
|
+
already accepts. Article documents the field as *"Repeated `ImageObject` or
|
|
106
|
+
`URL`"* — neither form preferred — and **width and height appear nowhere in
|
|
107
|
+
either the Article guidance or the image-licence guidance**. So having the
|
|
108
|
+
dimensions is not a reason to emit the node; lib does have them for the share
|
|
109
|
+
image, the icon and a video thumbnail, and Google has no use for them.
|
|
110
|
+
What the node *does* unlock is the **Licensable badge** in Google Images, and
|
|
111
|
+
that needs `contentUrl` plus at least one of `license`, `creator`,
|
|
112
|
+
`creditText` or `copyrightNotice` — rights data no project here holds. Build
|
|
113
|
+
it the day someone wants that badge, and build it around the licence fields
|
|
114
|
+
rather than the sizes. Checked August 2026.
|
|
115
|
+
|
|
116
|
+
`FAQPage` is above, and has its own entry because it is the one that keeps
|
|
117
|
+
getting asked.
|
|
118
|
+
|
|
119
|
+
### Should a post say which `articleSection` it is in?
|
|
120
|
+
|
|
121
|
+
**No — not while there is one feed.**
|
|
122
|
+
|
|
123
|
+
A section is the part of a publication a piece lives in: Sports, Business,
|
|
124
|
+
Opinion. Open Graph's `article:section` takes one, and schema.org's
|
|
125
|
+
`articleSection` is the same idea. It is *where a piece sits*, as against tags,
|
|
126
|
+
which are *what it is about* — one home versus many labels.
|
|
127
|
+
|
|
128
|
+
Everything here sits in the same place. A `section` field would say `"News"` on
|
|
129
|
+
every post for as long as `/news` is the only feed, which is the URL repeated
|
|
130
|
+
into the markup. Tags are the half that varies, so tags are what posts carry.
|
|
131
|
+
|
|
132
|
+
It earns its place the day the feed splits — News, Events, Offers — because then
|
|
133
|
+
it distinguishes something. Add it with that split, not before it.
|
|
134
|
+
|
|
135
|
+
### Should we add `WebSite` + `SearchAction` so a search box shows in results?
|
|
136
|
+
|
|
137
|
+
**No — the schema. A search page is a separate question.**
|
|
138
|
+
|
|
139
|
+
Google [retired the sitelinks search box](https://developers.google.com/search/blog/2024/10/sitelinks-search-box)
|
|
140
|
+
globally on 21 November 2024, citing lack of use. The markup renders nothing
|
|
141
|
+
now. That none of our sites has a search page to point it at is the second
|
|
142
|
+
reason, not the first — build one if it is useful to readers, just do not expect
|
|
143
|
+
this markup to advertise it.
|
|
144
|
+
|
|
145
|
+
`WebSite` itself is still read for site names, which is the only thread left to
|
|
146
|
+
pull here.
|
|
147
|
+
|
|
148
|
+
### Should we show star ratings for a venue?
|
|
149
|
+
|
|
150
|
+
**Not for the venue. For a room, the door is open.**
|
|
151
|
+
|
|
152
|
+
> If the entity that's being reviewed controls the reviews about itself, their
|
|
153
|
+
> pages that use LocalBusiness or any other type of Organization structured data
|
|
154
|
+
> are ineligible for star review feature.
|
|
155
|
+
|
|
156
|
+
Reviews a business collects about itself never qualify, so venue-level stars are
|
|
157
|
+
unreachable however good the data is — they come from Google Business Profile
|
|
158
|
+
instead, which is outside this repo.
|
|
159
|
+
|
|
160
|
+
Room-level ratings on the `Product`/`Game` node *are* eligible. They need reviews
|
|
161
|
+
collected on the site itself, shown in full on the page, and averaged over
|
|
162
|
+
exactly the set displayed — never a number typed in beside a shorter list.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### Should `CurrencyCode` and `CountryCode` spell out the ISO lists?
|
|
167
|
+
|
|
168
|
+
**Not now.**
|
|
169
|
+
|
|
170
|
+
Both are shapes rather than vocabularies — `` `${Letter}${Letter}${Letter}` ``
|
|
171
|
+
and `` `${Letter}${Letter}` `` — so `"XYZ"` and `"ZZ"` type-check. ISO 4217 has
|
|
172
|
+
around 180 active codes and ISO 3166-1 alpha-2 has 249; both are small, closed
|
|
173
|
+
and effectively frozen, so enumerating them is possible in a way the [IANA
|
|
174
|
+
language registry](https://www.iana.org/assignments/language-subtag-registry) is
|
|
175
|
+
not — which is the case `LanguageTag` in `config.ts` declines and says why.
|
|
176
|
+
|
|
177
|
+
Left as shapes because the mistake is not one anyone has made: a currency is
|
|
178
|
+
written once per project, beside the prices it applies to, and a wrong one is
|
|
179
|
+
visible the first time a page renders. That is a different risk from a typo in a
|
|
180
|
+
directive nobody reads. Worth revisiting if a project ever computes a code
|
|
181
|
+
rather than writing it.
|
|
182
|
+
|
|
183
|
+
### Should `@type` be checked against schema.org's vocabulary?
|
|
184
|
+
|
|
185
|
+
**Not in the type system. Possibly against the built output, one day.**
|
|
186
|
+
|
|
187
|
+
`LocalBusinessInput.type` and `ProductInput.alsoA` take bare strings, so
|
|
188
|
+
`"EntertainmentBussiness"` compiles, gets emitted, and is ignored by every
|
|
189
|
+
reader — the silent failure that `RobotsDirective` and `OpenGraphType` were
|
|
190
|
+
tightened to prevent.
|
|
191
|
+
|
|
192
|
+
They are not the same case. Those two vocabularies are closed; schema.org has
|
|
193
|
+
around 800 types and is still growing. A partial enumeration of an open
|
|
194
|
+
vocabulary rejects types that are perfectly valid, and the response to that is
|
|
195
|
+
to cast past it — at which point the check is theatre. A literal union that
|
|
196
|
+
still admits any string (`Known | (string & {})`) would give autocomplete
|
|
197
|
+
without the rejection, but it means carrying a curated list for editor
|
|
198
|
+
convenience alone, and a list that earns nothing else is a list that drifts
|
|
199
|
+
unnoticed.
|
|
200
|
+
|
|
201
|
+
The version worth building, if this ever bites: validate the *emitted* JSON-LD
|
|
202
|
+
against a vocabulary dump, in a test. That sees all 800 types, and sees what no
|
|
203
|
+
type could — a property that is not valid for the type it sits on.
|
|
204
|
+
|
|
205
|
+
## Answers that will not change on their own
|
|
206
|
+
|
|
207
|
+
### Should lib carry the consent *banner* as well as the tags?
|
|
208
|
+
|
|
209
|
+
**No — and the line between the two halves is the point.**
|
|
210
|
+
|
|
211
|
+
lib carries Umami, Google Analytics and Tag Manager, in `analytics/`. It also
|
|
212
|
+
carries the half of consent that is a sequencing problem: `gtag('consent',
|
|
213
|
+
'default', …)` has to be queued before any tag reads it, and behind it Google
|
|
214
|
+
applies its implicit default, which is everything granted. Nothing errors, the
|
|
215
|
+
numbers arrive, and slightly more of them than should have. That is exactly the
|
|
216
|
+
kind of failure lib exists to make unwritable, so `googleScripts` emits the
|
|
217
|
+
defaults first and denies by default.
|
|
218
|
+
|
|
219
|
+
What lib will not carry is the banner: markup, wording, whether the reject
|
|
220
|
+
button is as easy to find as accept, where the answer is stored, and how a
|
|
221
|
+
visitor changes their mind. Those are product and legal decisions that differ
|
|
222
|
+
per deployment, and a template that guessed at them would be guessing at
|
|
223
|
+
somebody's compliance.
|
|
224
|
+
|
|
225
|
+
The seam between the halves is a global — `CONSENT_UPDATE_GLOBAL` — because
|
|
226
|
+
that is the only place they *can* meet: the head script runs before any module
|
|
227
|
+
exists. lib emits an updater covering exactly the signals it defaulted, so a
|
|
228
|
+
banner never names them and the two lists cannot drift. `src/astro/consent.ts`
|
|
229
|
+
carries the browser-side parts that are the same everywhere — reading an answer,
|
|
230
|
+
expiring it, applying it — and `examples/b2c/src/components/ConsentBanner.astro`
|
|
231
|
+
is a worked
|
|
232
|
+
example of the rest, marked as a demonstration rather than a product.
|
|
233
|
+
|
|
234
|
+
Note the shape sibling projects use, and do not copy it: they group
|
|
235
|
+
`siteVerification` with `analytics` and `tagManager` under one `integrations`
|
|
236
|
+
key. Verification is a `<meta>` proving ownership; analytics is a script. One
|
|
237
|
+
config block, two different kinds of thing.
|
|
238
|
+
|
|
239
|
+
### Should a room be an `Event`?
|
|
240
|
+
|
|
241
|
+
**No.**
|
|
242
|
+
|
|
243
|
+
`Event` wants a name, a start date, a location and offers. A room bookable at
|
|
244
|
+
any opening hour is not an event, and lib should not invent a start time to
|
|
245
|
+
satisfy a schema. Reconsider only if a venue runs dated one-off sessions — a
|
|
246
|
+
Halloween night, a tournament — which would be a real event and would earn the
|
|
247
|
+
markup honestly.
|
|
248
|
+
|
|
249
|
+
### Should lib generate share images — logo over a field, title burnt in?
|
|
250
|
+
|
|
251
|
+
**No. It crops, it does not compose.**
|
|
252
|
+
|
|
253
|
+
Composing a card needs a renderer rather than a resizer, and every project would
|
|
254
|
+
end up rewriting the layout anyway. `shareImage` crops a source it was given and
|
|
255
|
+
refuses to enlarge it; `astro-og-canvas` or satori is the tool for the other
|
|
256
|
+
thing. This was built once, with satori, and removed.
|
|
257
|
+
|
|
258
|
+
### Should lib render `.htaccess` as well as `_redirects`?
|
|
259
|
+
|
|
260
|
+
**No — everything deploys to Cloudflare now.**
|
|
261
|
+
|
|
262
|
+
One of those sites hand-rolls an Apache `.htaccess`: trailing-slash removal,
|
|
263
|
+
`.html` extension resolution, then the redirects. It is real code in a sibling
|
|
264
|
+
repo, which is why this entry exists — reading that file is what prompts the
|
|
265
|
+
question.
|
|
266
|
+
|
|
267
|
+
Nothing is served by Apache any more. Worth knowing the seam is already there if
|
|
268
|
+
that ever changes: `site.redirects()` returns resolved rules as *data*, and
|
|
269
|
+
`buildCloudflareRedirects` is one renderer over them rather than the only
|
|
270
|
+
possible one, so another host is a new function and no redesign. The two
|
|
271
|
+
non-redirect halves of that `.htaccess` — extension resolution and slash
|
|
272
|
+
handling — are `build.format` and `trailingSlash` here, which `siteRoutes`
|
|
273
|
+
already sets and the next entry explains.
|
|
274
|
+
|
|
275
|
+
### Should `trailingSlash`, `output` and `build.format` be options?
|
|
276
|
+
|
|
277
|
+
**No. `siteRoutes()` sets them.**
|
|
278
|
+
|
|
279
|
+
They are not preferences. Each is the other half of a contract lib already
|
|
280
|
+
wrote — the files it emits at build time, and the slash-free URLs it publishes
|
|
281
|
+
as canonical — so a project that changed one would silently break the other. The
|
|
282
|
+
integration sets all three and logs that it did, so nothing is hidden.
|
|
283
|
+
|
|
284
|
+
### Should `/` serve a meta-refresh stub when every locale is prefixed?
|
|
285
|
+
|
|
286
|
+
**No. A real 301 in `_redirects`.**
|
|
287
|
+
|
|
288
|
+
A meta refresh is a soft redirect: followed slowly, weighted less, and it
|
|
289
|
+
renders a page before sending the reader on. The two cannot coexist either — a
|
|
290
|
+
static host serves the file and the rule never fires — so this was built, then
|
|
291
|
+
replaced.
|
|
292
|
+
|
|
293
|
+
### Should the breadcrumb say which page of a list you are on?
|
|
294
|
+
|
|
295
|
+
**No, and a post's trail should point at the list, not at the page it is listed
|
|
296
|
+
on.**
|
|
297
|
+
|
|
298
|
+
A `BreadcrumbList` states *hierarchical* position, and pagination is lateral
|
|
299
|
+
movement between siblings at one level: `Home › News` is the trail on `/news`
|
|
300
|
+
and on `/news/page/7` alike. Google renders breadcrumbs in results, so a
|
|
301
|
+
trailing `News (2)` buys noise in the SERP for a listing page that is already a
|
|
302
|
+
weak search target — and the trail would change every time something was
|
|
303
|
+
published above the item. The pager already says which page this is, which is
|
|
304
|
+
where that state belongs.
|
|
305
|
+
|
|
306
|
+
Built and removed, along with the per-crumb link callback on `breadcrumbFor`
|
|
307
|
+
that existed only to serve it.
|
|
308
|
+
|
|
309
|
+
### Should a post link back to the page of the list it appeared on?
|
|
310
|
+
|
|
311
|
+
**No. To the list.**
|
|
312
|
+
|
|
313
|
+
This was built, and it was static — derived from the post's position rather than
|
|
314
|
+
from a referrer — so it prerendered fine. The problem was the reader: someone
|
|
315
|
+
arriving from a search result was sent to page four of an archive they had never
|
|
316
|
+
seen, and the link moved whenever something was published above the post.
|
|
317
|
+
|
|
318
|
+
Reading order is the relationship a reader actually has with an article, so a
|
|
319
|
+
post carries newer/older links instead. Browser back covers "the list I was
|
|
320
|
+
looking at" better than any link can.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Adding to this
|
|
325
|
+
|
|
326
|
+
One question, one answer, one reason. Say what is *not* built and what still is
|
|
327
|
+
— the scope is the part that gets lost. If the answer rests on someone else's
|
|
328
|
+
decision, put it in the first group and name the date, so a reader can tell a
|
|
329
|
+
settled fact from one worth re-checking.
|
package/docs/checks.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# What is checked, and where
|
|
2
|
+
|
|
3
|
+
Three layers, and each catches what the one before it cannot see. `npm test`
|
|
4
|
+
runs all three in this order and stops at the first failure.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm run test:types # the package compiles, and the type assertions still bite
|
|
8
|
+
npm run test:unit # runtime behaviour
|
|
9
|
+
npm run test:examples # every example, every deployment: astro check + astro build
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The last one **builds** rather than only checking, and that is not thoroughness
|
|
13
|
+
for its own sake. `astro check` sees one deployment — whichever `config/project`
|
|
14
|
+
holds — so checking once leaves every other venue unlooked-at. And the
|
|
15
|
+
right-hand column of the table below can only throw at build time, because each
|
|
16
|
+
entry depends on merged data or on a real file. A gate that stopped at
|
|
17
|
+
type-checking would pass all of it.
|
|
18
|
+
|
|
19
|
+
## Compile errors, and where they are stated
|
|
20
|
+
|
|
21
|
+
Most guarantees are compile errors, stated in a *constraint* rather than in the
|
|
22
|
+
parameter type — in parameter position a single unrelated mistake widens the
|
|
23
|
+
inferred type and every key then reports as malformed, burying the real error in
|
|
24
|
+
a cascade.
|
|
25
|
+
|
|
26
|
+
That rule inverts when the validator maps the *whole* object rather than one of
|
|
27
|
+
its properties, as the slug-depth checks do: in the constraint, any unrelated
|
|
28
|
+
malformation makes the argument itself unassignable and TypeScript reports at the
|
|
29
|
+
call site instead of the offending line. Those two live in parameter position for
|
|
30
|
+
that reason.
|
|
31
|
+
|
|
32
|
+
An error is carried by an interface with one impossible property, whose name and
|
|
33
|
+
message say what is wrong — `SlugDepthMismatch`, `LocaleNotEnabled`,
|
|
34
|
+
`MalformedLanguageTag`. **Name that property for the specific failure, never a
|
|
35
|
+
generic `__ERROR__`:** TypeScript prints the alias name when the marker is a
|
|
36
|
+
conditional's result, but goes structural — `Property '__X__' is missing` — when
|
|
37
|
+
it is an annotation, and there the property name is the only part the reader
|
|
38
|
+
sees.
|
|
39
|
+
|
|
40
|
+
| Compile error | Build-time throw |
|
|
41
|
+
| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
42
|
+
| unknown message key, or an override dropping a `{placeholder}` | two routes resolving to the same URL |
|
|
43
|
+
| a message the project declared mandatory and never wrote (`CatalogCovers`) | |
|
|
44
|
+
| copy or a locale label written for a locale outside `enabledLocales` | |
|
|
45
|
+
| a slug whose translation sits at a different depth, in the registry or across the overlay | the same, when a slug is not a literal (computed, or from an env var) |
|
|
46
|
+
| unknown route id, or a locale the site does not ship | a locale published twice |
|
|
47
|
+
| a malformed language tag (`en_US`, `en-us`, `EN-US`, bare `en`) | an icon that is not square, a multiple of 48, and PNG |
|
|
48
|
+
| `defaultLocale` outside `locales`; unknown top-level field | a `url` that is not `https://` or ends in `/`, when it is not a literal |
|
|
49
|
+
| a `url` literal that is not `https://` or ends in `/` | |
|
|
50
|
+
| a `lastmod` that is not a `YYYY-MM-DD` calendar date | |
|
|
51
|
+
| config for a route this project does not build, or missing for one it does (`PerRoute`) | |
|
|
52
|
+
|
|
53
|
+
The runtime checks are the ones types cannot see: facts about a *file* (an icon's
|
|
54
|
+
real dimensions), facts only known after merging (URL collisions), or a value
|
|
55
|
+
typed as plain `string` because it came from an env var, where the literal checks
|
|
56
|
+
pass vacuously.
|
|
57
|
+
|
|
58
|
+
## Isolation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm run test:types # tsc --noEmit, with no consumer paths and no ambient types
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
That command is the contract. The root `tsconfig.json` runs with `"types": []`
|
|
65
|
+
and excludes `src/astro/`, so the core provably imports nothing from `astro`,
|
|
66
|
+
`node:` or a DOM — and there is no path alias for a consumer to reach through. If
|
|
67
|
+
it passes, the package is genuinely independent of anything consuming it.
|
|
68
|
+
|
|
69
|
+
## Type tests
|
|
70
|
+
|
|
71
|
+
`type-tests/` asserts the compile-time guarantees with `@ts-expect-error`: if a
|
|
72
|
+
mistake ever becomes legal again, the unused directive fails the build. They live
|
|
73
|
+
with the package, not in a consumer, because they test **the package's**
|
|
74
|
+
behaviour and must travel with it.
|
|
75
|
+
|
|
76
|
+
They run against `type-tests/fixtures.ts` — the single place in the package where
|
|
77
|
+
concrete values are allowed. The fixture locale is `"xx-XX"`, which exists in no
|
|
78
|
+
real project, so a test can never pass by accidentally leaning on a consumer's
|
|
79
|
+
data. Route fixtures are named `blue-room` / `red-room` / `green-room` for the
|
|
80
|
+
same reason.
|
|
81
|
+
|
|
82
|
+
**One thing cannot be tested here: anything typed `PublicFilePath`.** The
|
|
83
|
+
registry it reads is empty in this program, so it widens to any path — and inside
|
|
84
|
+
a project that has run `publicFiles()` it is that project's union instead. An
|
|
85
|
+
assertion written here would mean the opposite thing there. Those belong in the
|
|
86
|
+
consuming project's own `type-tests/`, which is also the only place the
|
|
87
|
+
augmentation is real. Both examples carry one:
|
|
88
|
+
[`b2c`](../examples/b2c/type-tests/wiring.ts) asserts against a `public/` with
|
|
89
|
+
two files in it, and [`b2b`](../examples/b2b/type-tests/wiring.ts) against one
|
|
90
|
+
with none, where the union is empty and *every* path is rejected.
|
|
91
|
+
|
|
92
|
+
## Runtime tests
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm run test:unit
|
|
96
|
+
npm run test:watch
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`tests/` covers what a type test cannot reach: what a merge resolves to, what a
|
|
100
|
+
builder rejects, what a file ends up containing. They go through the public
|
|
101
|
+
API — `createSite(...).metaFor(...)` rather than the private helper it calls — so
|
|
102
|
+
a test pins a decision rather than an implementation.
|
|
103
|
+
|
|
104
|
+
They are **excluded from the type check**, because they import `vitest` and that
|
|
105
|
+
command's promise is that the package type-checks with no ambient types and no
|
|
106
|
+
dependencies at all. Vitest type-checks them itself when it runs them.
|
|
107
|
+
|
|
108
|
+
| File | What it pins |
|
|
109
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
110
|
+
| `meta.test.ts` | the `robots` tag a policy builds, the pinned `twitter:card`, share-image warnings, length warnings, `fileUrl` |
|
|
111
|
+
| `redirects.test.ts` | rule resolution for all three target kinds, the five rejections, the Cloudflare 2000-rule cap |
|
|
112
|
+
| `sitemap.test.ts` | splitting into an index plus numbered parts, and that the entry keeps its name either way |
|
|
113
|
+
| `merge.test.ts` | catalog and route overlay precedence — per locale, never per key |
|
|
114
|
+
| `llms.test.ts` | section grouping, owners, heading and link precedence, and the undescribed fallback |
|
|
115
|
+
| `public-files.test.ts` | walking `public/`, what is skipped, and why the declaration must be a module |
|
|
116
|
+
| `breadcrumbs.test.ts` | the trail, a dropped ancestor, `orphanSegments`, and how a gap is reported |
|
|
117
|
+
| `jsonld/*.test.ts` | that a `</script>` in any value cannot close the block, each node's shape and `@id`, and how a price table becomes offers |
|
|
118
|
+
| `analytics.test.ts` | Umami's attributes, its three-state booleans, and the `domains` list that would record nothing |
|
|
119
|
+
| `google-analytics.test.ts` | that consent is denied first and precedes every tag, one loader for many ids, and dead `UA-` properties |
|
|
120
|
+
| `contact.test.ts` | the E.164 a `tel:` needs — trunk zero dropped, spacing stripped — and the displayed form kept |
|
|
121
|
+
| `hours.test.ts` | collapsing a week into runs, week start changing the answer, and every impossible week that throws |
|
|
122
|
+
| `money.test.ts` | a bare count widened to a band, the span of a table, and the gaps and overlaps that throw |
|
|
123
|
+
| `url.test.ts` | joining an origin to a path exactly once, and normalising the origin an `@id` is built from |
|
|
124
|
+
| `warn.test.ts` | the shared prefix, and reducing a URL to its path |
|
|
125
|
+
|
|
126
|
+
The fixture carries two nested families for that last one, differing in the only
|
|
127
|
+
way that changes behaviour: `guides` has a route owning its bare slug and `rooms`
|
|
128
|
+
does not, so both the linked and the unlinked section are reachable from one
|
|
129
|
+
site.
|
|
130
|
+
|
|
131
|
+
## What a consumer checks
|
|
132
|
+
|
|
133
|
+
A consuming project keeps its own thin suite for *wiring* — that its catalog,
|
|
134
|
+
routes and config actually satisfy these contracts, and that its own types have
|
|
135
|
+
not been loosened by a stray `any` or a dropped `satisfies`. It does not restate
|
|
136
|
+
the machinery's behaviour; that is what the two suites above are for.
|
|
137
|
+
|
|
138
|
+
Both examples' `type-tests/wiring.ts` are deliberately short, and every assertion
|
|
139
|
+
in them names something that must stay *rejected*.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Share images: what each surface actually shows
|
|
2
|
+
|
|
3
|
+
One `og:image` has to serve every surface, and they do not agree on a shape. The
|
|
4
|
+
asymmetry is what decides it: a square surface takes the **centre** of a wide
|
|
5
|
+
image, while a wide surface takes a band out of a square one and cuts the top and
|
|
6
|
+
bottom off. Wide degrades; square destroys.
|
|
7
|
+
|
|
8
|
+
| Surface | Reads | Renders | Notes |
|
|
9
|
+
| ------------------------- | -------------------------------- | ---------------- | ----------------------------------------- |
|
|
10
|
+
| Facebook | `og:image` | 1.91:1 large | recommends 1200×630, min 200×200 |
|
|
11
|
+
| LinkedIn | `og:image` | 1.91:1 | 1200×627 |
|
|
12
|
+
| iMessage | `og:image` | wide large | |
|
|
13
|
+
| Slack / Discord | `og:image` | wide | |
|
|
14
|
+
| Telegram | `og:image` | wide | |
|
|
15
|
+
| WhatsApp | `og:image` | **small square** | centre-crops; preview needs a small file |
|
|
16
|
+
| X — `summary_large_image` | `twitter:image`, else `og:image` | 2:1 wide | min 300×157 — the card Atlas always emits |
|
|
17
|
+
| X — `summary` | `twitter:image`, else `og:image` | **1:1 square** | min 144×144 — never asked for; see below |
|
|
18
|
+
|
|
19
|
+
So **1200×630 for everything**, and let the square surfaces crop.
|
|
20
|
+
|
|
21
|
+
## Why `fit` matters more than it looks
|
|
22
|
+
|
|
23
|
+
That crop is the whole reason `shareImage` takes a fit at all:
|
|
24
|
+
|
|
25
|
+
| The page's art | `shareImage` call | Why |
|
|
26
|
+
| --------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
27
|
+
| A photograph | `shareImage(src)` — `cover` | edges are scenery; a square crop takes the middle, which is the subject |
|
|
28
|
+
| A logo, square source | `shareImage(src, { fit: "contain", background })` | scaled to 630×630 and centred, so a square crop lands **exactly on the logo** |
|
|
29
|
+
| A logo under ~630px | neither — compose one or get a bigger file | `contain` would enlarge it, and `shareImage` refuses rather than ship a blurred card |
|
|
30
|
+
|
|
31
|
+
Both examples use the second row for their catalogue pages: a room's own mark on
|
|
32
|
+
the brand field, so a chat thumbnail shows which room it is rather than a
|
|
33
|
+
prettier picture that could be any of them.
|
|
34
|
+
|
|
35
|
+
## `twitter:card` is pinned
|
|
36
|
+
|
|
37
|
+
It is **always `summary_large_image`**, and there is no option to change it.
|
|
38
|
+
|
|
39
|
+
X is the one surface that renders whatever the tag claims rather than reading the
|
|
40
|
+
picture and choosing, so the tag is a decision — and the asymmetry at the top of
|
|
41
|
+
this page decides it the same way it decides the shape: a wide image survives a
|
|
42
|
+
square crop, a square one does not survive a wide. `summary` renders a square
|
|
43
|
+
thumbnail, and there is no image worth serving that wants one.
|
|
44
|
+
|
|
45
|
+
An earlier version derived the card from the image's dimensions and took a
|
|
46
|
+
`twitterCard` override. Both are gone. Deriving it meant a page could ship the
|
|
47
|
+
square card by accident — an image slightly under a threshold, a logo someone
|
|
48
|
+
padded — and the failure was invisible, because a card that renders is a card
|
|
49
|
+
that looks like it worked. One value, no threshold to sit near, nothing to
|
|
50
|
+
override. `tests/meta.test.ts` pins it at three sizes including a square one.
|
|
51
|
+
|
|
52
|
+
What Atlas does *not* model is a second image for X alone: one page, one picture.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Notes on the toolchain
|
|
2
|
+
|
|
3
|
+
Decisions that look like oversights until you know why, and the traps that cost
|
|
4
|
+
an afternoon each.
|
|
5
|
+
|
|
6
|
+
## TypeScript is pinned to 6.x
|
|
7
|
+
|
|
8
|
+
`astro check` (via `@astrojs/language-server`) needs TypeScript's programmatic
|
|
9
|
+
API, which the native TS 7 compiler does not expose yet. On TS 7 `astro check`
|
|
10
|
+
refuses to run, and `.astro` frontmatter — where most `t()` calls live — would go
|
|
11
|
+
unchecked.
|
|
12
|
+
|
|
13
|
+
That is not a small loss: the frontmatter is where a view builds its `metaFor`
|
|
14
|
+
call, its JSON-LD node and every templated `t()`, so an unchecked frontmatter
|
|
15
|
+
means most of what this package guarantees stops being verified in the one place
|
|
16
|
+
it matters. Track
|
|
17
|
+
[withastro/roadmap#1321](https://github.com/withastro/roadmap/discussions/1321).
|
|
18
|
+
|
|
19
|
+
## `noUnusedVariables` / `noUnusedImports` are off for `.astro`
|
|
20
|
+
|
|
21
|
+
Biome's Astro support has two modes and neither does both jobs:
|
|
22
|
+
|
|
23
|
+
- **Default mode** formats and lints frontmatter, but cannot see the template —
|
|
24
|
+
so every const the markup uses is reported unused.
|
|
25
|
+
- **`html.experimentalFullSupportEnabled`** lints accurately, but no longer
|
|
26
|
+
formats frontmatter.
|
|
27
|
+
|
|
28
|
+
This repo takes the default mode plus Biome's documented rule exclusions, and
|
|
29
|
+
nothing is lost: `astro check` reports unused frontmatter variables and imports
|
|
30
|
+
itself (`ts(6133)`) while correctly resolving template usage. The two tools cover
|
|
31
|
+
each other.
|
|
32
|
+
|
|
33
|
+
## `biome.json` must not contain `//` comments
|
|
34
|
+
|
|
35
|
+
Biome silently falls back to its defaults and reformats the whole repo to tabs
|
|
36
|
+
and double quotes. No error, no warning — just a very large diff. This is the
|
|
37
|
+
one config file in the repo that cannot explain itself inline.
|
|
38
|
+
|
|
39
|
+
## Astro's built-in i18n routing is deliberately unused
|
|
40
|
+
|
|
41
|
+
It cannot express per-locale slugs. A route id resolving to `/about-us` in one
|
|
42
|
+
language and `/el-GR/sxetika-me-emas` in another is the whole first idea of this
|
|
43
|
+
package, and the built-in strategy has no way to say it. Routing is done by the
|
|
44
|
+
single catch-all page in each example, from the route registry.
|
|
45
|
+
|
|
46
|
+
## Formatting
|
|
47
|
+
|
|
48
|
+
4-space indent, double quotes, `es5` trailing commas, LF. Biome formats `.astro`
|
|
49
|
+
frontmatter but leaves the markup below it alone, which is why the templates in
|
|
50
|
+
the examples are hand-aligned.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm run lint # check
|
|
54
|
+
npm run lint:fix # check and write
|
|
55
|
+
npm run format # format only
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## The build output is file-style
|
|
59
|
+
|
|
60
|
+
`build.format: "file"` and `trailingSlash: "ignore"` are set by `siteRoutes()`
|
|
61
|
+
rather than asked for in each consumer's config, because each is the other half
|
|
62
|
+
of a contract the integration already wrote: the files it emits, and the
|
|
63
|
+
slash-free URLs it publishes as canonical.
|
|
64
|
+
|
|
65
|
+
Every canonical, `hreflang`, sitemap and `llms.txt` URL is written without a
|
|
66
|
+
trailing slash. `directory` format would leave every one of them redirecting to a
|
|
67
|
+
different URL, with nothing in the build to say so. `src/astro/site-routes.ts`
|
|
68
|
+
explains it where it sets it, and logs what it set.
|
|
69
|
+
|
|
70
|
+
The practical consequence: `/about-us` is `about-us.html`, not
|
|
71
|
+
`about-us/index.html`. `dist/` is plain files — any static host works, and no
|
|
72
|
+
server rules are required.
|
|
73
|
+
|
|
74
|
+
## Claiming `/` when every locale is prefixed
|
|
75
|
+
|
|
76
|
+
When a project sets `prefixDefaultLocale: true`, nothing owns `/` and no page is
|
|
77
|
+
generated for it. `site.redirects()` claims it with a real **301** in
|
|
78
|
+
`_redirects` instead, pointing at the default locale's root — a redirect rather
|
|
79
|
+
than a rendered stub, so a crawler is told the destination is canonical instead
|
|
80
|
+
of following a refresh.
|
|
81
|
+
|
|
82
|
+
The target is whichever route has an empty slug, so nothing has to name a "home"
|
|
83
|
+
page. See [`NOT-BUILT.md`](NOT-BUILT.md) for why no meta-refresh stub is emitted.
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@escape-game-over/atlas",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
|
|
6
|
+
"private": false,
|
|
7
|
+
"license": "UNLICENSED",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"workspaces": [
|
|
12
|
+
"examples/*"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./src/index.ts",
|
|
16
|
+
"./astro": "./src/astro/index.ts",
|
|
17
|
+
"./astro/images": "./src/astro/images.ts",
|
|
18
|
+
"./astro/consent": "./src/astro/consent.ts",
|
|
19
|
+
"./astro/meta-tags": "./src/astro/MetaTags.astro"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"atlas": "bin/use-project.mjs"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"bin",
|
|
27
|
+
"docs",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "npm run test:types && npm run test:unit && npm run test:examples",
|
|
32
|
+
"test:types": "tsc --noEmit",
|
|
33
|
+
"test:unit": "vitest run",
|
|
34
|
+
"test:watch": "vitest",
|
|
35
|
+
"test:examples": "npm run test --workspaces",
|
|
36
|
+
"lint": "biome check .",
|
|
37
|
+
"lint:fix": "biome check --write .",
|
|
38
|
+
"format": "biome format --write .",
|
|
39
|
+
"check-fmt": "biome check . --error-on-warnings"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"astro": ">=7"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@biomejs/biome": "2.5.8",
|
|
46
|
+
"@types/node": "26.2.0",
|
|
47
|
+
"astro": "7.2.2",
|
|
48
|
+
"typescript": "6.0.3",
|
|
49
|
+
"vitest": "4.1.10"
|
|
50
|
+
}
|
|
51
|
+
}
|