Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,9 @@ jobs:
- run: npm run smoke

# The whole shop on a real Worker: settings, content published with the
# Mallok CLI, the seed, pages in two languages, the cart
# Mallok CLI, the seed, every kind of page in four languages, the cart
- run: npm run smoke:shop

# The one command a newcomer runs first still ends with a filled shop
# and an admin login that works
- run: npm run preview -- --check
24 changes: 17 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ Nundar is in development. Several features wait for extension points Mallok does
```bash
npm ci
npm run build # mallok prepare (stages admin + theme assets) then a deploy dry run
npm run dev # wrangler dev with local D1 and R2; needs .dev.vars
npm run preview # a filled local shop on a throwaway database; prints an admin login
npm run dev # wrangler dev with local D1 and R2 that keep their data; starts empty, needs .dev.vars
npm run lint # biome check . (npm run lint:fix to apply)
npm run typecheck
npm test # test:project, then test:shop
Expand All @@ -39,8 +40,8 @@ CI (`.github/workflows/ci.yml`) runs `npm ci` → lint → typecheck → test

- `src/worker/index.ts` is the whole site: `createMallok({ theme: nundarTheme, plugins: [inquiry, shop] })`. Theme and plugins are build-time choices.
- `src/plugins/shop/` — commerce logic and nothing about pages. `plugin.json` is the manifest (validated strictly by `definePlugin`: unknown fields are rejected, every declared hook and route needs an implementation and vice versa). `migrations/` holds SQL, `lib/` the logic, `routes/` the HTTP handlers, `index.ts` wires them.
- `src/theme/` — how pages look and nothing else: `theme.json` (kinds, fields, options), `layouts/`, `partials/`, `locales/<locale>.json`, `assets/`. It has no tables, no routes and no JavaScript.
- `content/` and `seed/` — the sample catalogue. `site.json` — the site's languages, kinds, navigation and theme options.
- `src/theme/` — how pages look and nothing else: `theme.json` (kinds, fields, options), `layouts/`, `partials/`, `locales/<locale>.json`, `assets/` (the stylesheet, four images, the fonts and their licences, and the scripts `theme.json` declares). It has no tables and no routes.
- `content/` and `seed/` — the sample catalogue, a fictional titanium fastener supplier: 31 bundles in four languages, and one variant per SKU. `site.json` — the site's languages, kinds, navigation and theme options, which is where this site's own copy lives.

Mallok decides the contracts on both sides. Its documentation is the reference: `PLUGIN_API.md`, `THEME_FORMAT.md` and `CONTENT_FORMAT.md` in the Mallok repository (`node_modules/mallok/types/worker.d.ts` has the types).

Expand All @@ -59,9 +60,16 @@ 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 is a theme option; per-language option values go under `themeOptions.$locales` in `site.json`.
- **References resolve by slug within the same language.** An `application` names its `product`; the product page lists them through `content.backrefs.application`. A `product` names its `collection`; the collection page lists `content.backrefs.product`. Mallok resolves `reference` only, not `reference[]`.
- Each language of a bundle is its own content item with its own `slug` (`index.md`, `index.<locale>.md`); Mallok puts them in one `translation_group`. A bundle carries a `mallok.json` only when something outside the content must name it: the product's fixes the `translation_group` that `seed/shop-sample.sql` attaches variants to. `test/content.test.ts` checks the reference rule and keeps any identity file in step with its bundle.
- 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.
- **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/<german slug>`); `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.
- 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 `<script src="{{ theme.asset_base }}/<file>" defer></script>`, 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.<locale>.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 `/<locale>/…`. 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
Expand All @@ -81,7 +89,9 @@ Each is a task in Mallok's plan for plugin API 2. When Mallok ships one, upgrade
- A test for a fix must be seen failing without the fix. For new guards, break the guard and confirm the test goes red.
- A race is tested by running the calls with `Promise.all`, and such a test only counts once breaking the guard turns it red: that is the proof the two calls really interleave.
- `countD1Calls` in `test/shop/helpers.ts` counts round trips; use it wherever the number is a design constraint. `interceptBatches` runs a hook around each batch: it is how a test changes the data between a function's reading and its writing, or loses the answer to a write that committed.
- `npm run smoke:shop` is the only place theme, plugin, content and the Mallok CLI run together; run it when touching any of them.
- `test/theme/pages.test.ts` renders every layout from content it creates itself, with everything set; `test/theme/bare.test.ts` does the same for a site that has filled in almost nothing, and fails on any empty element or `href=""`. Neither reads `content/`. A template that prints a wrapper has to check that there is something to put in it — and `content.html` is not a string, so capture it before comparing it with `blank`. The sample is checked by `test/content.test.ts` and `test/project.test.ts` (files only, no Worker) and by the smoke run.
- `npm run smoke:shop` is the only place theme, plugin, content and the Mallok CLI run together; run it when touching any of them. It publishes the real sample, requests every kind of page, and follows every header and footer link in all four languages.
- `scripts/lib/local-shop.mjs` is the one place that brings a local shop up — administrator, plugins, settings, content, variants, in that order and before any page is requested. The smoke run and `npm run preview` both use it; do not grow a second copy. Locally nothing purges the page cache (`s-maxage=3600`, persisted under the state directory), so a page requested before the set-up finished stays as it was.

## Commerce invariants (do not "simplify" them away)

Expand Down
23 changes: 16 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ Thanks for taking the time to contribute.

```bash
npm ci
npm run build # stages the admin and the theme's assets
npm run smoke:shop # the whole shop on a throwaway local Worker
npm run preview # a local shop with the sample catalogue, to look at
npm run smoke:shop # the same shop, checked end to end
```

No Cloudflare account is needed for local development: D1 and R2 are simulated
locally. The README has the steps for a local shop you can browse.
locally. `preview` runs on a throwaway database; the README has the steps for a
local site that keeps its data.

**npm, not pnpm or yarn.** Nundar is a Mallok site, and Mallok's CLI installs
and upgrades with npm only; it refuses a project that carries another
Expand Down Expand Up @@ -71,10 +72,13 @@ same pull request and explain the new reasoning.
| Webhooks | Verified against the bytes as received before anything is parsed. 5xx only for what delivering again could change; everything else is answered 200 and written down. |
| Language | Decided by the URL alone. Never redirect or switch by IP — crawlers would see one language. |
| References | A `reference` field names the target's slug **in the same language**. `test/content.test.ts` checks the sample content. |
| Bundle identity | A bundle needs a `mallok.json` only when something outside the content names it: the sample variants attach to the product by its translation group. Where the file exists, `test/content.test.ts` keeps it in step with the bundle. |
| Links in content | A link in a Markdown body is the path of the page in the same language. Mallok does not rewrite it and does not report a dead one; `test/content.test.ts` does. |
| Product attributes | Every product in a language names the same `facets`, in the same order: they are the columns of the specification finder. A size is a row in `sizes`, not a page of its own. |
| Bundle identity | A bundle needs a `mallok.json` only when something outside the content names it: the sample variants attach to a product by its translation group. Where the file exists, `test/content.test.ts` keeps it in step with the bundle, and holds the seeded variants to the SKUs the page lists. |
| Site copy | Words a visitor reads that belong to this site, not to the theme, are theme options in `site.json`, with a value per language under `$locales`. `test/project.test.ts` fails when a language has none. |
| Database access | Raw SQL through D1, no ORM. Batch reads and writes: a tick of the cron shares one invocation's CPU budget with every other plugin. |
| Migrations | Additive only, idempotent, and a comment has a line to itself — Mallok's migrator drops whole-line comments and then splits on semicolons. |
| Theme | No `<script>`, no inline event handlers. Anything interactive is declared in `theme.json`'s `clientScripts`. |
| Theme | No inline script, no inline event handlers, no script from another host. A script is a file in `src/theme/assets/`, declared in `theme.json`'s `clientScripts` with its exact size, loaded with `defer` by the one layout that needs it, and it only adds to a page that is complete without it. `test/project.test.ts` holds the declared list to the templates and the files. Nothing is loaded from another host: fonts and images are files in `src/theme/assets/`, and a change to any of them comes with a new `version` in `theme.json`. |
| Secrets | Never in the repository. `.dev.vars` locally, Worker secrets when deployed. |
| Dependencies | Ask whether the platform or Mallok already provides it. Every dependency is inherited attack surface. |

Expand Down Expand Up @@ -119,10 +123,15 @@ docs: explain how references resolve per language

## Adding a language

1. Add the locale to `site.json` (`locales`, and a `nav` entry).
1. Add the locale to `site.json`: `locales`, a `nav` entry, and the site's
copy and links under `themeOptions.$locales`.
2. Add `src/theme/locales/<locale>.json` and list the locale in
`src/theme/theme.json`.
3. Translate content: add `index.<locale>.md` to each bundle, give it its own
`slug`. If the bundle has a `mallok.json`, add the language there too.
`slug`, name references by that language's slugs and write links as that
language's paths. If the bundle has a `mallok.json`, add the language
there too.

`npm run test:project` reports whatever is still missing.

No schema change is needed: every language version is its own content item.
Loading
Loading