Repository navigation
refactor: rebuild Nundar as a shop plugin and a commerce theme on Mallok - #1
Merged
Merged
Conversation
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.
This was referenced Oct 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Replaces the standalone Next.js application with a Mallok site that carries a shop plugin, a commerce theme and sample content.
mallok creategenerates, pinned tomallok@0.1.0-rc.9. Next.js, OpenNext, Drizzle, KV and the Durable Objects are gone. The previous implementation stays at the tagnextjs-final.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-formPOSTroute.src/theme/). Home, product, application, collection, list and page layouts in English, German, French and Spanish, with no client JavaScript.content/,seed/). One product, one application note, one collection and a contact page, each in four languages.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 lintnpm run typechecknpm test— 10 project checks, 130 tests inside workerdnpm run smoke:shop(when the change touches the plugin, the theme or the content) — andnpm run smoke,npm run buildmallok@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.headin 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
2026-09-30-nundar-on-mallok-design.mdis new and is now the source of truth for the architecture.2026-09-03-nundar-design.mdis marked as partly superseded: its commerce decisions hold, its stack does not.