Skip to content

refactor: rebuild Nundar as a shop plugin and a commerce theme on Mallok - #1

Merged
JasonYv merged 11 commits into
mainfrom
feature/rebuild-on-mallok
Oct 5, 2026
Merged

JasonYv merged 11 commits into
mainfrom
feature/rebuild-on-mallok

Conversation

@JasonYv

@JasonYv JasonYv commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

What this changes

Replaces the standalone Next.js application with a Mallok site that carries a shop plugin, a commerce theme and sample content.

  • Site skeleton. The project mallok create generates, pinned to mallok@0.1.0-rc.9. Next.js, OpenNext, Drizzle, KV and the Durable Objects are gone. The previous implementation stays at the tag nextjs-final.
  • Shop plugin (src/plugins/shop/). Variants, prices as integer minor units in three currencies, stock, minimum order quantities, exchange-rate repricing in the site's cron, and a cart with a plain-form POST route.
  • Commerce theme (src/theme/). Home, product, application, collection, list and page layouts in English, German, French and Spanish, with no client JavaScript.
  • Sample content (content/, seed/). One product, one application note, one collection and a contact page, each in four languages.
  • Tooling. npm instead of pnpm (Mallok's CLI supports npm only), tests inside workerd, two smoke runs against a real local Worker, and a CI workflow rewritten to match.

Why

Nundar is meant to be a shop built on Mallok — a plugin and a theme — not a second application beside it. The standalone version reimplemented pages, languages, the admin, sign-in, media and caching, all of which Mallok already provides. The commerce rules carry over; the architecture does not.

The reasoning, the division of responsibility and the owner's decisions are in docs/superpowers/specs/2026-09-30-nundar-on-mallok-design.md.

What does not work yet. Prices and variants on pages, the cart page, and editing variants in the admin each need an extension point Mallok has not released (design §7). They wait for it rather than being worked around. The README's "What works today" table has the full picture.

How it was verified

  • npm run lint
  • npm run typecheck
  • npm test — 10 project checks, 130 tests inside workerd
  • npm run smoke:shop (when the change touches the plugin, the theme or the content) — and npm run smoke, npm run build
  • Checked in the browser (describe what you clicked) — home, product, application, collection and contact pages in English and German, and the mobile menu, on mallok@0.1.0-rc.7. After the upgrade to rc.9 the pages were checked by the tests and the smoke run, not again by eye.

Guards were checked by breaking them and watching a test fail: the server-side minimum-order check, the same-site redirect check, the protection of manual prices, the stock constraint, page.head in the base layout, and a reference in the sample content.

Not verified. This workflow has not run on GitHub before this pull request. Nothing has been deployed to a Cloudflare account from this code, so CPU time and D1 usage on the Workers Free plan are unmeasured.

Spec impact

  • No design decision changed
  • Design decision changed — spec updated in this PR

2026-09-30-nundar-on-mallok-design.md is new and is now the source of truth for the architecture. 2026-09-03-nundar-design.md is marked as partly superseded: its commerce decisions hold, its stack does not.

JasonYv added 11 commits October 2, 2026 21:55
Nundar is to be a shop plugin and a commerce theme on top of Mallok rather
than a standalone application (docs/superpowers/specs/2026-09-30-nundar-on-mallok-design.md).
This removes the Next.js app, OpenNext, Drizzle and their configuration, and
puts in the project `mallok create --no-deploy` generates at mallok 0.1.0-rc.7:
a thin site that depends on the framework at an exact version.

The previous implementation is kept at the tag nextjs-final.

npm replaces pnpm because Mallok's CLI supports npm only and refuses a project
that carries another lockfile. zod is kept for input validation; vitest and
the Workers pool are kept so plugin logic is tested inside workerd. The
lockfile was produced with npm 11, because npm 10.9.7 fails while adding those
two packages; `npm ci` works with either.

Biome skips docs/, which holds exported design files rather than source.
The commerce logic of the previous implementation, rebuilt as a Mallok plugin:
variants keyed to a product's translation group, prices per currency as
integer minor units, ECB-driven repricing with a buffer, psychological
rounding and a drift threshold, manual prices that are never overwritten, the
order state machine, and a cart whose lines hold a variant and a quantity and
never a price.

What changed on the way over, and why:

- The cart lives in D1 instead of KV. The Workers Free plan allows 1,000 KV
  writes a day, which the old per-view rate-limit counters exhausted.
- Repricing reads one batch and writes one statement per chunk (the changed
  rows travel as a single JSON parameter) and resumes from a cursor across
  cron ticks. The old loop queried and wrote every price separately, which
  passes D1's per-invocation query limit on a real catalogue and does not fit
  the CPU budget plugins share inside Mallok's one cron.
- Stock carries CHECK (stock >= 0). A test proves that a decrement which would
  go negative rolls back the whole D1 batch, and another that a conditional
  UPDATE matching no row does not — which is why the payment write in the next
  phase will rely on the constraint rather than on WHERE stock >= qty.
- A variant has a stock policy: tracked, or made to order, where stock is not
  a limit and the lead time is what the buyer is told.
- Add-to-cart is a plain form POST to /_mallok/p/shop/cart, so it needs no
  client JavaScript. The cart cookie is scoped to the plugin's path, because
  Mallok bypasses its edge cache for any public request carrying a cookie.

Tests run inside workerd against the site's own Worker: the site is brought up
through Mallok's HTTP API and the tables are created by its migrator.
A new Liquid theme for the shop, script-free: specification-first product
pages, application notes that stand as their own landing pages, and collection
pages that gather products by attribute. Interface strings ship in English,
German, French and Spanish, carried over from the previous storefront.

How the long-tail structure maps onto Mallok:

- An application declares a reference to its product. Mallok resolves the
  reverse direction, so a product page lists its application notes without
  either side being kept in step by hand, and each language links to the other
  language's own slug.
- A product declares a reference to its collection, and the collection page
  lists the products that name it. Mallok resolves single references only, so a
  product belongs to one collection for now; many-to-many needs `reference[]`
  to be resolved by Mallok.
- Features stay in the product body, where they strengthen the product page
  for attribute searches instead of multiplying near-identical pages.

hreflang, canonicals and structured data come from Mallok through page.head;
the theme only has to emit it, and a test fails if it does not.

The home page's product and application sections are written to Mallok's
documented `recent.<kind>` contract. mallok 0.1.0-rc.7 fills in articles
only, so those sections stay empty until Mallok supplies the rest.

System fonts only, so nothing is fetched to draw text.

Tests render pages through the real Worker, from content created over the
management API.
The sample catalogue of the previous implementation as Mallok content bundles
in four languages: one product, the application note that stands as its own
landing page, a collection, and a contact page carrying the inquiry form. Each
language has its own slug, and each reference names the slug in its own
language, which is how Mallok resolves it.

The product bundle fixes its translation group in mallok.json so that
seed/shop-sample.sql can attach the sample variants to it in every language.

site.json now describes the shop: four locales, the five kinds, navigation per
language, and a per-language link for the request-a-quote button.

Two checks keep this honest:

- test/content.test.ts fails when a reference names a slug that does not
  exist in that language, or when the seed names a product no bundle declares.
  A broken reference still renders, it just quietly loses its link.
- npm run smoke:shop boots a real Worker on throwaway state, creates the
  administrator, applies the settings, publishes content/ with the Mallok CLI,
  loads the seed, requests the pages in two languages and adds to the cart.
  It is the one place theme, plugin, content and CLI are seen working together.
Published with the Mallok CLI, a bundle without a mallok.json gets a separate
translation group for each language (mallok 0.1.0-rc.7): the CLI posts each
language without a group, and the server assigns a new one every time. The
application note, the collection and the contact page each landed as four
unrelated items, so none of them had hreflang or a working language switcher.
Only the product was right, because it already carried an identity file for
the seed's sake.

Every multilingual bundle now has a mallok.json. Two checks hold it there:
test/content.test.ts refuses a bundle with more than one language and no
identity file, and the end-to-end smoke run asserts that the application
note's four languages share one group and that its page links to another
language's version.

The smoke run keeps applying site.json through the API and publishing
./content. Publishing the repository root with --with-settings is not usable
here: the CLI scans every file under the directory, so it picks up the sample
page inside node_modules/mallok/template, and it decides which kinds exist
before the settings it was asked to apply have taken effect.
Publishing content needs the shop's kinds and languages to exist on the site
first, and there was no working command to put them there. Mallok's own
`mallok publish . --with-settings` is not usable from this repository's root:
it scans every file under the directory, so it picks up the sample page inside
node_modules/mallok/template, and it decides which kinds exist before the
settings it was asked to apply have taken effect.

scripts/apply-settings.mjs sets the name, languages, kinds, navigation and
theme options from site.json. It reads the token from MALLOK_TOKEN rather than
from an argument, which would show up in `ps` and in shell history. The
end-to-end smoke run now uses it, so the command the README gives is the one
that is exercised.

.dev.vars.example gains MALLOK_SETUP_KEY: without it the first-run wizard
cannot be completed on a local site.
The workflow still installed with pnpm and ran the Next.js build. It now
installs with npm, which is the only manager Mallok's CLI supports, and runs
lint, the type check, the tests, the build and then two runs against a real
local Worker: one that it boots, and one that takes the whole shop from
settings to a cart.

The pull request and issue templates name the new commands and the new scope.
The README, the contributing guide, the security policy and CLAUDE.md all
described the standalone Next.js application that no longer exists.

The README now says what Nundar is, and states plainly that it is in
development: what works on mallok 0.1.0-rc.7, and what waits for extension
points Mallok does not have yet. The Deploy to Cloudflare button is gone until
a deployed shop would be more than a catalogue without prices.

The security policy follows the new boundary: accounts, sessions and the admin
are Mallok's, and only what the shop plugin owns is described here.

The design spec is corrected where building it proved it wrong — a collection
cannot list its products through reference[], which Mallok does not resolve —
and records what phase 1A changed and found. The original spec is marked as
partly superseded, and a phase record sits beside the earlier ones.
Seen while looking at the rendered pages: an application note or a collection
usually has no cover image, and its card still reserved a grey 4:3 block above
the title, which read as a broken image. The frame is now rendered only when
there is a picture to put in it.
Done with `mallok upgrade --to 0.1.0-rc.9`, which sets the exact version,
installs it and re-runs this project's type check, tests, build and deploy dry
run against it. All passed.

rc.8 and rc.9 fix three defects this project ran into on rc.7 — a bundle's
languages published into separate translation groups, the home page receiving
recent articles only, and gaps in the site template — and export the helpers a
site's own plugin needs to build HTML and email.
Each was removed with the upstream fix proven here rather than assumed.

- src/text-modules.d.ts: the package now declares the text modules itself, and
  the type check passes on its declarations.
- scripts/apply-settings.mjs: `mallok publish . --with-settings` now works
  from a project root, so the end-to-end smoke run and the README use that one
  command instead.
- The mallok.json files on the application, collection and contact bundles:
  the CLI now keeps a bundle's languages in one translation group. The smoke
  run still asserts that the application note's four languages share one, so
  it now checks Mallok's fix instead of our workaround. The product keeps its
  identity file, because the sample variants attach to its translation group.
- The home page tests assert what the template was written for all along:
  products, application notes and collections, each language's own.

The rule that every multilingual bundle must carry an identity file goes with
them. Where a bundle does have one, the check that it agrees with the bundle
stays.

The design spec and the phase record say which Mallok gaps have closed and
which still shape this code.
@JasonYv
JasonYv merged commit e4b639f into main Oct 5, 2026
1 check passed
@JasonYv
JasonYv deleted the feature/rebuild-on-mallok branch October 6, 2026 04:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant