diff --git a/CLAUDE.md b/CLAUDE.md index 5a85d57..ab2424b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,11 +4,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## What this repository is -Nundar is a **shop plugin and a commerce theme for Mallok**, packaged as a Mallok site. It is not a standalone application: Mallok (the `mallok` npm package, pinned to an exact version, currently `0.1.0-rc.9`) provides routing, rendering, content, languages, hreflang, the sitemap, the admin, sign-in, media, email and the edge cache. Nundar adds only commerce. +Nundar is a **shop plugin and a commerce theme for Mallok**, packaged as a Mallok site. It is not a standalone application: Mallok (the `mallok` npm package, pinned to an exact version, currently `0.1.0-rc.11`) provides routing, rendering, content, languages, hreflang, the sitemap, the admin, sign-in, media, email and the edge cache. Nundar adds only commerce. `docs/superpowers/specs/2026-09-30-nundar-on-mallok-design.md` is the source of truth for the architecture, with the owner's decisions in its §11. The commerce rules in `docs/superpowers/specs/2026-09-03-nundar-design.md` (§4–§7) still hold; its stack and architecture are superseded. The previous standalone Next.js implementation is at the tag `nextjs-final` — read it for reference, never restore it. -Nundar is in development. Several features wait for extension points Mallok does not have yet (design §7). **Do not work around a missing Mallok capability** — no writing into Mallok's core tables, no `onRequest` hook to intercept pages, no client-side fetching of prices. The Mallok-side work is done by the owner in the Mallok repository; do not edit that repository from a session here. +Nundar is in development. `mallok@0.1.0-rc.11` carries plugin API 2 — the extension points of design §7 — and several features are not built on them yet (the list is below). **Do not work around a missing Mallok capability** — no writing into Mallok's core tables, no `onRequest` hook to intercept pages, no client-side fetching of prices. A defect or a gap met in Mallok is written up for the owner, who does the Mallok-side work in the Mallok repository; do not edit that repository from a session here. ## Commands @@ -60,27 +60,33 @@ Mallok decides the contracts on both sides. Its documentation is the reference: ### The theme and content - Templates are restricted Liquid. Output is escaped; only `content.html` and `page.head` are emitted verbatim. `page.head` carries hreflang and structured data from Mallok and must stay in `layouts/base.liquid`. -- Interface strings are in `locales/*.json` (flat maps, the default locale is the fallback). Site-specific copy — the home page's headline and sections, the footer, the links behind the buttons — is a theme option, never a string in a template; per-language option values go under `themeOptions.$locales` in `site.json`, and `test/project.test.ts` fails when a language is left without one. -- **Kinds**: `product`, `collection`, `application` (the sample's industry pages, at `/industries`), `case`, `faq`, `tool`, `article`, `page`. A kind with a `base` needs a `listLayout`: without one Mallok answers its base path with a 500. +- Interface strings are in `locales/*.json` (flat maps, the default locale is the fallback). Site-specific copy — the home page's headline and sections, the footer, the links behind the buttons — is a theme option, never a string in a template; per-language option values go under `themeOptions.$locales` in `site.json`, and `test/project.test.ts` fails when a language is left without one. The tagline is not an option: it is Mallok's own setting, a map of language to text in `site.json`. It follows the site's name in the home page's title, and describes that page unless the `home_description` option gives the language a fuller text. +- **Kinds**: `product`, `collection`, `application` (the sample's industry pages, at `/industries`), `case`, `faq`, `tool`, `article`, `page`. Every kind with a `base` has a `listLayout` here, because each list is a page worth having; a kind without one has no list page — its base path answers 404 — and no entry in `site.kinds`. A template links to a kind's list only through `site.kinds.`, inside `{% if %}`: the breadcrumb (`partials/crumbs.liquid`) and the home page's link to the catalogue do, and `test/theme/unlisted.test.ts` holds both on a site that serves products without a list. - **A product is one page with its sizes on it**, not a page per size. `facets` are the attributes a buyer filters by; `sizes` maps each SKU to what distinguishes it; `specs` is the full table. `partials/spec-table.liquid` (the specification finder, the catalogue, a collection's products, a product's neighbours) takes its columns from the first product that has `facets` and fills every row by attribute name, so every product in a language must use the same names. Below 72rem the same table is laid out as cards, two to a row on a tablet: seven columns need about 1100px in German. - **References resolve by slug within the same language.** An `application` and a `case` name their `product`; the product page lists them through `content.backrefs.application` and `content.backrefs.case`. A `product` names its `collection`; the collection page lists `content.backrefs.product`. Mallok resolves `reference` only, not `reference[]`. - **A link in a Markdown body is plain text to Mallok**: it is not rewritten per language and nothing reports a dead one. Write the path of the page in the same language (`/de/products/`); `test/content.test.ts` checks every one. -- `[[inquiry]]` on a line of its own becomes the inquiry plugin's form when that plugin is enabled. Its labels exist in English and Chinese only (Mallok), so the other languages show English labels. +- `[[inquiry]]` on a line of its own becomes the inquiry plugin's form when that plugin is enabled. The plugin has its own labels in English and Chinese and reads any other language's from the theme's pack: the six `inquiry_*` keys in `locales/de.json`, `fr.json` and `es.json`. They are deliberately not in `en.json` — a pack falls back to the default one, so English keys would replace the plugin's own text in every language this theme has no pack for. - The header puts the site name, the navigation, the language control and one button on a single line from 1280px. The sample's ten links fit in all four languages with little to spare (German is the longest); a longer label in `site.json` overflows that line, and only a look at the page at 1280px shows it. - **A script only adds to a page that is already whole.** There are two. `assets/finder.js` puts filters above the specification table on the home page and in the catalogue. `assets/calculators.js` runs the three fastener calculators on a `tool` page whose front matter says `calculators: fasteners`, above a text that prints the same formulas, constants and tables — and `test/theme-scripts.test.ts` holds every constant in the script, and every figure it computes, to that page. In both cases the form is in the page `hidden`, with its labels from the language pack and its numbers in `value` attributes, so the script reads no language and the page offers nothing it cannot do. A script asks for a field with `querySelector`, never through `form.elements`: that list answers `length` with a count, whatever a field is called. A script is a plain file — no build step, no imports — declared in `theme.json`'s `clientScripts` with its exact size, and loaded only as ``, only by the layout that has what it works on. It hands its pure functions to `module.exports` when a `module` exists, which is how `test/theme-scripts.test.ts` runs them under `node:vm`; what it does to a page is checked in a browser, by hand. - Fonts are files in `assets/fonts/`, declared in `style.css` and preloaded in `layouts/base.liquid`; nothing is loaded from another host. Changing any asset means bumping `version` in `theme.json`: assets are served from a versioned path and cached for good. - Each language of a bundle is its own content item with its own `slug` (`index.md`, `index..md`); Mallok puts them in one `translation_group`. A bundle carries a `mallok.json` only when something outside the content must name it: each product's fixes the `translation_group` that `seed/shop-sample.sql` attaches its variants to. `test/content.test.ts` checks the reference rule, keeps every identity file in step with its bundle, and holds the seed's variants to the SKUs the product pages list. - The default language (English) is unprefixed; others are `//…`. English pages default to USD, the rest to EUR (`lib/currency.ts`), never by IP. -### Known limits of mallok 0.1.0-rc.9 that shape the code +### What is not built on plugin API 2 yet -- No render-time hook with database access, so prices and variants are not on pages yet. -- Plugin routes cannot render through the theme, so there is no cart page yet. -- Plugin admin panels are read-only tables; variants are seeded from `seed/shop-sample.sql`. -- Plugin routes receive a parsed body, so a Stripe signature cannot be checked in one: there is no webhook route, and so no checkout. -- `reference[]` fields are not resolved, so a product names one collection. +`mallok@0.1.0-rc.11` allows each of these; the code here does not use it yet. -Each is a task in Mallok's plan for plugin API 2. When Mallok ships one, upgrade, remove the corresponding limitation here, and prove the new behaviour with a test or the smoke run. +- No `renderData` hook, so prices and variants are not on pages. +- No page route and no `pluginLayouts` in the theme, so there is no cart page. +- The variants panel is a read-only table; variants are seeded from `seed/shop-sample.sql`. +- No raw-body route, so no Stripe webhook, and so no checkout. +- The sample's products name one collection each, though `reference[]` is resolved now. + +When one is built, take it off this list and prove the behaviour with a test or the smoke run. + +### Known limits of mallok 0.1.0-rc.11 that shape the code + +- Switching a plugin on or off, or changing a setting, can leave cached pages as they were — and the admin's "Clear cached pages" can report success without clearing anything. So the plugins are switched on before the first publish (`scripts/lib/local-shop.mjs`, the README), and `test/shop/helpers.ts` deletes the cached home page by hand after changing settings. Keep both until a Mallok release says the purge can be trusted. ## Testing diff --git a/README.md b/README.md index f2bf247..cac4d7e 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,9 @@ Everything runs on Cloudflare Workers with D1 and R2. Local development needs no ## What works today -Nundar builds on `mallok@0.1.0-rc.9`. Some of the shop needs extension points Mallok does not have yet; those parts wait for them rather than being worked around. +Nundar builds on `mallok@0.1.0-rc.11`, the first release with the plugin API the rest of the shop needs. The right-hand column is what has not been built on it yet. -| | Works today | Waits for Mallok's next plugin API | +| | Works today | Not built yet | |---|---|---| | **Pages** | A home page with a specification finder; product pages with their sizes and SKUs; collection, industry, case study, question, engineering reference and contact pages — all in English, German, French and Spanish, with `hreflang`, canonicals, sitemap and FAQ structured data; self-hosted fonts; no client JavaScript except two small scripts, each added to a page that is complete without it: filters for the finder, and calculators on the engineering reference page | Prices, variants and availability on the page; `Offer` structured data | | **Catalogue data** | Variants, prices as integer minor units, stock, MOQ, lead time, a made-to-order policy | Editing them in the admin (read-only for now; the sample data is loaded from SQL) | diff --git a/README.zh-CN.md b/README.zh-CN.md index ca26d2a..5f55f3e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -51,9 +51,9 @@ Nundar 不是 Mallok 旁边的第二个应用。一个商城就是一个加了 ## 现在能用什么 -Nundar 基于 `mallok@0.1.0-rc.9`。商城的一部分功能需要 Mallok 目前还没有的扩展点,这些部分会等扩展点就绪,不做绕路实现。 +Nundar 基于 `mallok@0.1.0-rc.11`,这是第一个带有商城其余部分所需插件 API 的版本。右侧一列是尚未在它之上实现的部分。 -| | 现在可用 | 等 Mallok 的下一版插件接口 | +| | 现在可用 | 尚未实现 | |---|---|---| | **页面** | 带规格查找表的首页;列出尺寸和 SKU 的商品页;聚合页、行业页、案例页、问答页、工程资料页和联系页——全部有英、德、法、西四种语言,带 `hreflang`、canonical、sitemap 和 FAQ 结构化数据;字体由站点自己提供;除了两个小脚本之外没有客户端 JavaScript,而且页面没有它们也是完整的:一个给规格查找表加筛选,一个在工程资料页上提供计算器 | 页面上的价格、规格和库存状态;`Offer` 结构化数据 | | **目录数据** | 规格、以整数最小单位存储的价格、库存、起订量、交期、按单生产策略 | 在后台编辑这些数据(目前只读,示例数据由 SQL 载入) | diff --git a/docs/superpowers/specs/2026-09-30-nundar-on-mallok-design.md b/docs/superpowers/specs/2026-09-30-nundar-on-mallok-design.md index 80fc59f..bb00800 100644 --- a/docs/superpowers/specs/2026-09-30-nundar-on-mallok-design.md +++ b/docs/superpowers/specs/2026-09-30-nundar-on-mallok-design.md @@ -284,7 +284,7 @@ The Next.js application stays in maintenance mode, with security and correctness ## 9. Phases -The phases follow Mallok's own roadmap (`mallok: docs/PRODUCT_VISION.md §9`: 0.2 inquiry cart, 0.3 payment, 1.0 storefront). Nundar was started on the published `mallok@0.1.0-rc.7`, is on `0.1.0-rc.9` now (§12), and moves to the Mallok release that carries the P0 extension points once the owner publishes it (§11 decision 8). +The phases follow Mallok's own roadmap (`mallok: docs/PRODUCT_VISION.md §9`: 0.2 inquiry cart, 0.3 payment, 1.0 storefront). Nundar was started on the published `mallok@0.1.0-rc.7`, moved to `0.1.0-rc.9` (§12), and is on `0.1.0-rc.11` now — the release that carries the extension points of §7 (§16). **Phase 0: decisions, no code** - §11 is answered (2026-10-01). @@ -504,3 +504,46 @@ Nothing here is commerce logic, and none of it needs a Mallok change: all of it **What review found.** An independent review of the finished theme confirmed twenty-three defects, none of them in what a crawler indexes, most of them in what happens off the path the sample walks: a language whose words are longer, a width between a phone and a laptop, an option left empty, a field left out. The plan lists them. Two rules came out of it that now have tests behind them: a template prints a wrapper only when it has something to put in it, and a layout is not finished until it has been measured in the longest language at every width. **Left open.** The sample's own contradictions, inherited from the template and listed in the plan; whether the images may be published under this repository's licences; a browser test for what only a browser shows; a pass with a screen reader; measurements on a deployed site. + +## 16. What the move to `mallok@0.1.0-rc.11` changed or found (2026-10-08) + +Mallok published plugin API 2 in two releases: `0.1.0-rc.10` with what phase 1 needs, and `0.1.0-rc.11` with what phases 2 and 3 need. Nundar pins `0.1.0-rc.11` exactly. It is published under npm's `next` tag only; `latest` is still rc.10, so a range or a tag would not select it. + +**The upgrade itself changed no code.** `npx mallok upgrade --to 0.1.0-rc.11` rewrote the version in `package.json` and the lockfile, and the lint, the type check, both test suites, the build, both smoke runs and `npm run preview -- --check` passed as they were. The shop plugin declares `"pluginApi": 1` and is routed exactly as before. + +**One thing the upgrade notes ask of every site.** `wrangler.jsonc` gains a second rate-limit binding, `RATE_LIMITER_RELAXED`, for routes a visitor repeats in normal use. `mallok upgrade` does not edit that file. Its namespace is the strict tier's number plus one, which is what a new site gets; the two must differ, and `test/project.test.ts` holds them apart. Until a route asks for the relaxed tier, nothing uses it. + +**§7, row by row.** Every row of that table is in the release. What each became, where it differs from what this document asked for: + +| §7 | In `mallok@0.1.0-rc.11` | Differs from the request | +|---|---|---| +| API-1 | The `renderData` hook; templates read `plugins.shop` | One database call per hook, a single `SELECT` checked by keyword, at most two plugins per page. A failing hook costs the page its place in the cache, not the page | +| API-2 | `cacheTags` from `renderData`; `ctx.purgeTags` | A plugin purges only its own tags and `site` | +| API-3 | Routes with `"render": "page"`, drawn by a layout the theme lists in `pluginLayouts` | Layout files are flat (`layouts/shop-cart.liquid`); the layout's *name* keeps the `shop/cart` form. The locale is a segment after the plugin id: `/_mallok/p/shop/de/cart` | +| API-4 | Several segments, `:parameters`, `input.json` | The first segment may not look like a locale code | +| API-5 | `"body": "raw"` | Capped at 256 KiB, or up to 1 MiB with `maxBytes` — Mallok's judgement: Stripe documents no maximum | +| API-6 | `records` panels with `money` and `rows` fields, attached to a content kind; action `params`; `related` child tables | An action handler is `(ids, ctx, params)`. A list of references has no picker in the admin | +| API-7 | `onContentSave` is called; `onContentDelete` exists | The delete hook runs after the row is gone and cannot refuse | +| API-8 | Each plugin's `scheduled` is isolated; `jobs`, `ctx.enqueue`, and `ctx.enqueueStatement` for the plugin's own batch | Five plugin jobs a tick, five attempts. A job runs a minute or more later, so order, payment and stock state never depend on one | +| API-10 | `createMallok({ starters })`, and a starter may carry `records` for a plugin's panel | | +| Structured data | `renderData` returns `structuredData: { offers }`, merged into the core's `Product` node | `offers` only, on `Product` only | +| Script check | Every template is scanned whether or not the theme declares scripts | | +| `reference[]` | Resolved in `content.refs` and `content.backrefs` | At most 24 | +| Rate limits (§5.2) | Two tiers, `strict` and `relaxed`, counted per route | Cloudflare's binding takes a period of 10 or 60 seconds only, so "ten per ten minutes" for checkout is not expressible; a longer window is the plugin's own count | +| Email (§14, item 11) | The Resend key and the sender are site settings; `ctx.sendEmail` uses them for every plugin | The shop declares neither | + +**The five behaviours of §15.** Four are answered in this release. Each stand-in here was removed in its own commit, and each answer was seen in this repository rather than taken from the changelog: + +| §15 found | In `mallok@0.1.0-rc.11` | Here | +|---|---|---| +| The inquiry form's labels are English on a German page | The plugin reads six `inquiry_*` keys from the theme's language pack, one at a time, and keeps its own English and Chinese as the fallback | The keys are in the German, French and Spanish packs, and deliberately not in the English one: a pack falls back to the default pack, so English keys would replace the plugin's own text in every language this theme has no pack for. `test/theme/inquiry.test.ts` holds each label in its place in the form, and holds the default pack empty of them | +| The tagline is one string for all languages | `tagline` is a string or a map of language to text, and Mallok describes each language's home page with it | The `tagline` theme option is gone and `site.json` carries four taglines. `home_description` stays, as an option and no longer as a stand-in: Mallok's description of the home page *is* the tagline, and a site may want a sentence there rather than five words | +| A kind with an address and no list layout answers 500 | 404, with the theme's not-found page in the address's language | Nothing to remove: every kind with an address has a list layout because each list is a page worth having. Checked by taking one kind's list layout away for a single run: `/tools`, `/de/tools` and `/tools/page/2` answered 404, in English and in German | +| A template cannot link to its kind's list page | `site.kinds.`, with `path` and `label`, present exactly when the list page exists | The `catalogue_href` option is gone. One breadcrumb partial serves every layout — home, the list of the kind, a product's collection, the page — and `test/theme/unlisted.test.ts` holds it, and the home page's link to the catalogue, on a site that serves products without a list | +| Toggling a plugin or changing settings leaves cached pages as they were | The first-run half is fixed: nothing a site serves before its administrator exists is stored | **The rest stands**, by Mallok's own check on a deployed site: a plugin switch did not evict a cached page, and neither did the admin's "Clear cached pages", which reported success all the same. So the plugins are still switched on before the first publish, the README still says so, and `test/shop/helpers.ts` still deletes the cached home page by hand | + +The theme is `0.5.0`: two options were removed. The longer breadcrumb was measured the way §15's review asked — all 156 pages of the sample at nine widths from 320 to 1440 px, none scrolling sideways and no breadcrumb running past its box. + +**Two things noticed on the way, both written up for the Mallok side and neither worked around.** A content page's structured data is its own node and nothing else: there is no `BreadcrumbList`, and a theme cannot add one, since no template may carry a JSON-LD block. And the admin has no field for a home page description apart from the tagline, which is why `home_description` remains a theme option. + +**Not measured by anyone yet.** Mallok has no CPU figure for a cold render with `renderData` active, and no plugin has used API 2 on a deployed site. The measurements §9 asks for before each phase ships are still owed, on a real account. diff --git a/package-lock.json b/package-lock.json index f5f90f2..f43b3d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "license": "MIT OR Apache-2.0", "dependencies": { - "mallok": "0.1.0-rc.9", + "mallok": "0.1.0-rc.11", "zod": "4.4.3" }, "devDependencies": { @@ -2267,9 +2267,9 @@ } }, "node_modules/mallok": { - "version": "0.1.0-rc.9", - "resolved": "https://registry.npmjs.org/mallok/-/mallok-0.1.0-rc.9.tgz", - "integrity": "sha512-X+UJKYeMY+h9rlRF6gLkMwU4Ub8AUVqAPJpE7WQ2YJmLEFotZdcFCp7LUf4TYvcowwyrGKtJ3pUuQ2xQ6bBPjA==", + "version": "0.1.0-rc.11", + "resolved": "https://registry.npmjs.org/mallok/-/mallok-0.1.0-rc.11.tgz", + "integrity": "sha512-OFlqNoBCTNrTOWp6eHAi3lylfERM6vLeR3Uhx48Zkp9ntBpQJGa9vBUsGX9k3miOusZJvVcWVySQE6IIgIETTQ==", "license": "Apache-2.0", "dependencies": { "@types/mdast": "4.0.4", diff --git a/package.json b/package.json index 04ae8fb..2a59aee 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "smoke:shop": "node scripts/smoke-shop.mjs" }, "dependencies": { - "mallok": "0.1.0-rc.9", + "mallok": "0.1.0-rc.11", "zod": "4.4.3" }, "devDependencies": { diff --git a/scripts/lib/local-shop.mjs b/scripts/lib/local-shop.mjs index 9419ef2..848896e 100644 --- a/scripts/lib/local-shop.mjs +++ b/scripts/lib/local-shop.mjs @@ -29,6 +29,16 @@ export function expect(condition, message) { } } +/** Text as a template prints it. */ +export function escaped(text) { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + /** An OS-assigned free port, so two runs cannot collide. */ export function freePort() { return new Promise((resolve, reject) => { diff --git a/scripts/preview.mjs b/scripts/preview.mjs index 94a82d8..5c48345 100644 --- a/scripts/preview.mjs +++ b/scripts/preview.mjs @@ -27,6 +27,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { parseArgs } from 'node:util'; import { + escaped, expect, fillShop, freePort, @@ -84,16 +85,6 @@ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) { }); } -/** Text as a template prints it. */ -function escaped(text) { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - /** What `--check` asks of the shop before calling this command sound. */ async function check(login) { const site = JSON.parse(await readFile('site.json', 'utf8')); diff --git a/scripts/smoke-shop.mjs b/scripts/smoke-shop.mjs index 6d4372a..e5adb62 100644 --- a/scripts/smoke-shop.mjs +++ b/scripts/smoke-shop.mjs @@ -23,6 +23,7 @@ import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { + escaped, expect, fillShop, freePort, @@ -256,7 +257,14 @@ try { } // The form is the inquiry plugin's, put where the page wrote `[[inquiry]]`. - for (const path of ['/contact', '/fr/contact']) { + // Its English is the plugin's own; the French comes from the theme's pack. + const french = JSON.parse( + await readFile('src/theme/locales/fr.json', 'utf8'), + ); + for (const [path, submit] of [ + ['/contact', 'Send inquiry'], + ['/fr/contact', french.inquiry_submit], + ]) { const contact = await page(path); expect( contact.status === 200 && @@ -264,6 +272,10 @@ try { !contact.html.includes('[[inquiry]]'), `${path} does not carry the inquiry form`, ); + expect( + contact.html.includes(``), + `${path} does not label the inquiry form in its language`, + ); } // The navigation and the footer are written in `site.json`, the pages they @@ -274,6 +286,25 @@ try { const start = locale === site.defaultLocale ? '/' : `/${locale}/`; const front = await page(start); expect(front.status === 200, `${start} returned ${front.status}`); + // The tagline is one of Mallok's own settings and has a value per + // language in `site.json`; `publish --with-settings` applied them all. + expect( + front.html.includes( + `${escaped(site.name)} — ${escaped(site.tagline[locale])}`, + ), + `${start} is not titled with the ${locale} tagline in site.json`, + ); + // Under the finder, the way into the whole catalogue. Its address is + // Mallok's, from the base `site.json` gives products; no option names it. + const catalogue = `${start}${site.kinds.product.base}`; + expect( + front.html.includes( + `

`, + ), + `${start} does not link to the catalogue at ${catalogue}`, + ); + const listed = await page(catalogue); + expect(listed.status === 200, `${catalogue} returned ${listed.status}`); const chrome = [ /

[\s\S]*?<\/header>/.exec(front.html)?.[0] ?? '', /