Skip to content

feat: prices on pages, a currency switch, a cart page and variants in the admin - #6

Merged
JasonYv merged 10 commits into
mainfrom
feature/phase1b-prices-on-pages
Oct 8, 2026
Merged

JasonYv merged 10 commits into
mainfrom
feature/phase1b-prices-on-pages

Conversation

@JasonYv

@JasonYv JasonYv commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Stacked on #5, which moves to mallok@0.1.0-rc.11. Review that one first; this one's diff is against its branch. Merge it after #5, by fast-forward, so that these commits keep their ids.

What this is

Phase 1B of the design (docs/superpowers/specs/2026-09-30-nundar-on-mallok-design.md, §12 and the new §17): the part of the catalogue that waited for Mallok's plugin API 2. Four things are built on it, with nothing worked around.

What a visitor or a seller gets
Prices on pages Each size on a product page with its price, minimum order, availability and lead time; what each product starts at in the finder and the catalogue; the same offers in the page's Product structured data
A currency switch US dollars on English pages, euros on the others, and a switch to any currency the page has prices in
A cart A form beside each size, and a cart page in the site's own design and language: set a quantity, remove a line, choose a currency. No script anywhere in it
Variants in the admin A form under each product's editor for variants, prices and stock, with a ledger of every stock change

How it works, in short

  • The shop plugin declares "pluginApi": 2 and a renderData hook. While Mallok renders a page, one batch of two statements reads the variants and prices of the products shown; the theme prints them, and the page is cached with them. A cold product page is three D1 round trips.
  • A page carries a cache tag for each product it prices. A save in the admin, and a repricing run, purge the tags of the products they changed.
  • A page says whether a size can be had, never how many are left.
  • GET /_mallok/p/shop/cart is the cart page, drawn by the theme's shop/cart layout. POST …/cart/update is where every form posts; it only ever redirects. A change the shop refuses is sent to the cart page, which says what was refused.
  • A public page is the same for every visitor: nothing about a cart is in it, and the cart's cookie goes only to the shop's own routes.

The commits

Commit What it does
feat: show each size's price… The renderData hook, the page data, the theme's rows, the structured data, the cache tags
feat: add a currency switch… A third declared script; the sample gains euro and sterling prices
feat: add a cart page… The two cart routes, the cart layout, the add-to-cart forms, the header's link
feat: edit variants, prices and stock in the admin The records panel and its handlers, the delete hook, purging on change
docs: record what phase 1B built…, docs: bring the security notes up to date… Design §17 and SECURITY.md
three fix: commits and a docs: What an independent review found (below)

An independent review, and what it found

The finished work was given to a second reviewer with the properties it must hold and no account of how it holds them. It found eleven defects and seven smaller things. Each was checked before it was acted on, and each fix has a test that fails without it. The three that mattered most:

  • An open redirect. The path a form may send the buyer back to was judged by the text sent; /.//host is one slash as typed and another site once resolved.
  • A derived price left over from the wrong base price when two saves of one variant crossed.
  • An expired cart coming back with the next thing added, and a cart id a visitor could choose.

The rest are listed in the design's §17, with the one suggestion that was looked at and not taken.

How it was verified

Check Result
npm run lint, npm run typecheck, npm run build Pass
npm run test:project 52 of 52
npm run test:shop 633 of 633 inside workerd, 184 of them new
npm run smoke, npm run smoke:shop, npm run preview -- --check Pass on a real local Worker: a product page's form posted as it stands, a refused quantity followed to the page that explains it, the cart read in English and German, a variant added and deleted the way the admin's form does
Red and green 209 deliberate breakages: 202 turned a test red, five change nothing a caller can observe, two showed checks that could never fire, which were removed
In a browser, by hand A buyer's path clicked through; the admin's variant form opened and saved; every page of the sample, and the cart pages with three lines in them, at thirteen widths from 320 to 1920 px
Each fix: commit Passes lint, the type check and every test on its own

The measuring found one regression before it was committed: with the cart's link in it, the header's one line ran past the page's edge at 1280 px in three languages. The gap between its links is tighter where that line begins.

Not verified

  • Nothing was deployed. No CPU figure on a real account, and whether a purge actually evicts a page there, are both unknown. Of phase 1's exit criteria, those two are not met.
  • No automated browser test exists. What the currency switch does to a page, and the layouts, were checked by hand in one engine.
  • Nothing was tried with a screen reader. The new German, French and Spanish strings were not read by a native speaker.

Not built

  • The cart leads nowhere yet: it cannot be sent as one inquiry, and there is no checkout. Whose the inquiry cart is to build is a decision this pull request does not take.
  • Prices beside the products a collection page lists. Mallok tells a plugin what a list shows and not what a content page lists through a reference. A test here holds that state and fails the day it changes.

For the owner to decide

  • A cart line may hold at most 10,000, a limit carried over from the previous implementation. A catalogue of small parts may want more.
  • A derived price is rounded up to the next .99, which turns $1.60, $1.85 and $2.05 alike into €1.99. The sample uses hand-entered prices instead.

JasonYv added 10 commits October 8, 2026 17:56
The shop plugin declares plugin API 2 and a renderData hook. While Mallok
renders a product page, a list or the home page, the hook reads the
variants and prices of the products shown, in one batch, and the theme
prints them: on a product page a row for each size with its price in the
page's currency, its minimum order, its state and its lead time; in the
finder and the catalogue, what each product starts at.

The same offers go into the page's Product structured data, and each page
is tagged with the products it depends on, for a price change to purge.
A page says whether a size can be had, never how many are left.

Mallok tells the hook what a list shows and not what a content page lists
through a reference, so the products on a collection's page have no
prices yet.
A page is rendered in its language's own currency, which is what a
crawler reads and what its structured data states. Each price now also
carries its amount in every other currency the page offers, and a third
declared script puts one in its place when a buyer asks, keeping the
choice in the browser's own storage — not a cookie, which would take
that visitor's pages out of the shared cache. The switch is in the page
hidden and absent when there is one currency or none.

The sample variants gain a euro and a sterling price, entered by hand,
so that the sample shows what the switch is for. An asset was added, so
the theme is 0.7.0.
A product page now offers a form beside every size that can be ordered,
and the cart is a page of the site: the shop plugin says what is in it,
priced now, and the theme's shop/cart layout draws it in the site's own
header, footer and language. Quantities, removing a line and choosing a
currency are plain forms; nothing in the flow needs a script.

The forms post to cart/update, a second route, because Mallok keys a
handler by its path. A change that goes through answers 303 to the cart
page. One that is refused — below the minimum order, more than there
is — answers with the cart page saying what was refused, where the buyer
can put it right. The page and an order are built from one reading of
the cart, readCartFacts.

The header gains a link to the cart, the same for every visitor. With it
the one-line bar ran past the page's edge at 1280px in three languages,
so the gap between its links is tighter where that bar begins and opens
out from there. The theme is 0.9.0.
The variants panel is a form under every product's editor now. Mallok
checks each value against the field the manifest declares and calls the
shop's handlers, which judge what only the shop can — a SKU that is
taken, a lead time that ends before it starts — and write the variant,
its prices and the stock ledger in one batch. A base price derives the
other currencies; a price entered by hand replaces the derived one and
is never recomputed. Stock is set to a figure and the ledger records the
difference.

A variant that has been ordered is never deleted, from the form or with
its product: it is archived. A product deleted in its last language
takes its other variants with it.

Whoever changes what a page shows now purges that product's pages: a
save or a delete here, and a repricing run for the products whose prices
moved. A save does not wait for its purge, which Mallok holds for two
seconds to gather others.
Prices on pages, the currency switch, the cart and variants in the
admin, each against the section of the design it carries out; phase 1's
exit criteria one by one, two of them not met; and what waits for a
decision or for a deployed site.
… pages

A public page carries nothing of a cart; a cross-site form is refused;
a page states availability and never a count. Three scripts, one of
which keeps a currency code in the browser. And two residual risks said
plainly: a cached page can show a price that has since changed, and
stock set in the admin overwrites a sale that lands in between.
…ge-data defects

Found by an independent review of phase 1B.

With no currency every priced variant shares, a page prints no price,
and its offers were still built in the language's own currency. They
are built only in a currency the page shows.

The starting price in each currency was the price of whichever variant
is cheapest in the page's own; a price entered by hand can make another
variant the cheapest in euros. It is the lowest in each currency now.

A variant whose minimum order is more than a cart line may hold was
offered a form that every submission of was refused. It has none.

The lead time in the structured data was in days where the page says
business days. It is stated in business days.

The rule for which currency a page is in moves to one function, for the
cart to share.
…cts in the variant form

Found by an independent review of phase 1B.

Whether to derive a price again was decided from the reading at the
start of a save. Another save that changed the base price in between
left euros derived from a dollar price that had been overwritten. The
question is asked of the table inside the write now.

A saved form wrote back the stock it had been opened with. The field
comes to the form empty, and a save that names no figure writes none.

A purge that was not attempted, or was turned down, was counted as
done: Mallok's purge resolves either way. Its answer is read.

A price entered by hand needs a base price, a price cannot be nothing,
and a minimum order cannot exceed what a cart line may hold. Archiving a
variant takes it out of carts, as its help text said. A variant kept for
an order after its product is deleted gives its SKU back.
Found by an independent review of phase 1B.

The path a form may send the buyer back to was judged by the text that
was sent. /.//host begins with one slash and is //host, another site,
once its dot segment is resolved. It is judged by the resolved path.

A refused change was answered with the cart page, rendered by the POST.
Mallok lists a page under the route that rendered it, so that page's
language links and canonical pointed at an address that only takes a
POST. A refusal is a redirect to the cart page now, with the reason and
the variant in the address; the page reads the figures from the
database, so a made-up link puts no words of its own on it.

A cookie that names no live cart is no longer taken up: an expired
cart's lines came back with the next thing added, and a visitor could
choose the id that protects a cart.

A product published for a later date could be put in a cart by its
variant's id. A product can be bought when a visitor can see it.

A part whose product is out in another language only was called gone
when its buyer changed language, and a part priced only in euros could
be left without a price in a dollar cart. The name falls back to any
language the product is out in, and the cart settles its currency by
the rule a page does.

A line links to its product, and each Update button is named for its
line. The theme is 0.10.0.
…hanged

Eleven defects and seven smaller things, each checked before it was
acted on and each fixed with a test that fails without it; the one
suggestion not taken, and why; and what is still open.
@JasonYv
JasonYv changed the base branch from chore/adopt-mallok-rc11 to main October 8, 2026 13:21
@JasonYv
JasonYv merged commit 88ffe6a into main Oct 8, 2026
1 check passed
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