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
37 changes: 28 additions & 9 deletions CLAUDE.md

Large diffs are not rendered by default.

17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![CI](https://github.com/fobstack/Nundar/actions/workflows/ci.yml/badge.svg)](https://github.com/fobstack/Nundar/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue)](LICENSE)

> **Status: in development.** Nundar is being rebuilt on Mallok. The catalogue side works today. Prices on pages, the cart page and checkout are not there yet — see [What works today](#what-works-today). It is not ready to run a real shop.
> **Status: in development.** Nundar is being rebuilt on Mallok. The catalogue side works today. Prices and a cart are on the pages; checkout is not there yet — see [What works today](#what-works-today). It is not ready to run a real shop.

---

Expand Down Expand Up @@ -56,19 +56,22 @@ Nundar builds on `mallok@0.1.0-rc.11`, the first release with the plugin API the

| | 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) |
| **Pricing** | USD base price; EUR and GBP derived from ECB rates with a buffer, rounding to a price point and a drift threshold; manual prices never overwritten | |
| **Cart** | Add, set and remove through a plain form POST, with MOQ and stock enforced server-side | The cart page; submitting a cart as one inquiry |
| **Pages** | A home page with a specification finder; product pages with their sizes and SKUs, and for each size its price, minimum order, availability and lead time, read from the shop while the page is rendered and cached with it; the same offers in the page's `Product` structured data; a starting price under each product in the finder and the catalogue; 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; prices in US dollars on English pages and in euros on the others, with a switch to any currency the shop prices in; no client JavaScript except three small scripts, each added to a page that is complete without it: filters for the finder, the currency switch, and calculators on the engineering reference page | Prices beside the products a collection page lists (Mallok does not yet tell a plugin which products a content page shows) |
| **Catalogue data** | Variants, prices as integer minor units, stock, MOQ, lead time, a made-to-order policy — edited in the admin, in a form under each product's editor; every change of stock goes into a ledger; a deleted product takes its variants with it | |
| **Pricing** | USD base price; EUR and GBP derived from ECB rates with a buffer, rounding to a price point and a drift threshold; manual prices never overwritten; the pages of a product are purged from the cache when one of its prices moves | |
| **Cart** | A form beside each size on a product page, 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. MOQ and stock are enforced by the server, which says what it refused on the cart page | Submitting a cart as one inquiry; checkout |
| **Orders and payment** | The logic, tested and not yet reachable: orders with line snapshots, a payment that takes stock exactly once however often Stripe reports it, oversold orders, refunds that return stock, Stripe signature checks, order emails in four languages | The checkout and order pages, the webhook route, and order handling in the admin |

The reasoning and the plan are in [`docs/superpowers/specs/2026-09-30-nundar-on-mallok-design.md`](docs/superpowers/specs/2026-09-30-nundar-on-mallok-design.md).

### Commerce rules that are easy to get wrong, and are tested

- Money is always integer minor units. Never a float, anywhere.
- A page says whether a size can be had, never how many are left: a count would be wrong after the next sale, a state rarely is.
- The structured data offers exactly the prices the page prints. A test compares the two digit for digit.
- The cart stores variants and quantities only — never a price.
- MOQ is enforced by the form *and* by the server, because a form can be bypassed.
- A public page is the same for every visitor and stays in the cache: nothing about a cart is in it, and the cart's cookie is sent only to the shop's own routes.
- Stock carries a database constraint, so a payment's decrement cannot go negative: a test proves the whole D1 batch rolls back.
- Stock comes off when a payment is confirmed, never before, and once: tests deliver the same payment twice at the same moment.
- A payment is believed only with Stripe's signature on the exact bytes received, and only for five minutes.
Expand Down Expand Up @@ -124,7 +127,7 @@ The sample is a supplier of titanium fasteners that does not exist. Its products

- **Content** is in `content/`, one directory per page, with a Markdown file per language. Replace it with your own and publish.
- **The site's own copy** — the name, the navigation, the home page's headline and sections, the footer — is in `site.json`. Each language has its own values there; none of it is fixed in the theme.
- **Variants and prices** are in `seed/shop-sample.sql`, one variant for each SKU a product page lists.
- **Variants and prices** are in `seed/shop-sample.sql`, one variant for each SKU a product page lists, each with a US dollar base price and a euro and a sterling price entered by hand. Leave a currency out and the shop derives its price from the base price and the day's exchange rate.
- **Fonts** are served from the site itself (`src/theme/assets/fonts/`), under the SIL Open Font License; the licence texts and the source of each file are beside them. The theme loads nothing from a third party.

## Commands
Expand All @@ -142,7 +145,7 @@ The sample is a supplier of titanium fasteners that does not exist. Its products

## Deploying

Not yet. A deployed shop today would show a catalogue with no prices and no cart page. When Nundar is ready, deployment is Mallok's own: `npx mallok create . --slug <slug>`, which creates the Worker, the D1 database and the R2 bucket on your own Cloudflare account. That path has not been run for this repository.
Not yet. A deployed shop today would show a catalogue with prices and a cart that leads nowhere: there is no checkout, and a cart cannot be sent as an inquiry. Nothing here has been measured on a real Cloudflare account either. When Nundar is ready, deployment is Mallok's own: `npx mallok create . --slug <slug>`, which creates the Worker, the D1 database and the R2 bucket on your own Cloudflare account. That path has not been run for this repository.

## Languages and currencies

Expand Down
17 changes: 10 additions & 7 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

**[Mallok](https://github.com/fobstack/mallok) 的商城插件和商城主题。** 为想靠买家真实搜索词获得流量的跨境卖家而做,不去和所有人争抢同一个大词。

> **状态:开发中。** Nundar 正在 Mallok 上重建。商品目录这一侧已经可用;页面上的价格、购物车页和结账还没有,见[现在能用什么](#现在能用什么)。目前还不能用来经营真实店铺。
> **状态:开发中。** Nundar 正在 Mallok 上重建。页面上已有价格和购物车;结账还没有,见[现在能用什么](#现在能用什么)。目前还不能用来经营真实店铺。

---

Expand Down Expand Up @@ -55,19 +55,22 @@ Nundar 基于 `mallok@0.1.0-rc.11`,这是第一个带有商城其余部分所

| | 现在可用 | 尚未实现 |
|---|---|---|
| **页面** | 带规格查找表的首页;列出尺寸和 SKU 的商品页;聚合页、行业页、案例页、问答页、工程资料页和联系页——全部有英、德、法、西四种语言,带 `hreflang`、canonical、sitemap 和 FAQ 结构化数据;字体由站点自己提供;除了两个小脚本之外没有客户端 JavaScript,而且页面没有它们也是完整的:一个给规格查找表加筛选,一个在工程资料页上提供计算器 | 页面上的价格、规格和库存状态;`Offer` 结构化数据 |
| **目录数据** | 规格、以整数最小单位存储的价格、库存、起订量、交期、按单生产策略 | 在后台编辑这些数据(目前只读,示例数据由 SQL 载入) |
| **定价** | 美元基准价;欧元和英镑按 ECB 汇率换算,带缓冲、价位取整和漂移阈值;手动价格永不被覆盖 | |
| **购物车** | 通过普通表单提交加入、修改、移除,服务端校验起订量和库存 | 购物车页;把购物车作为一次询盘提交 |
| **页面** | 带规格查找表的首页;列出尺寸和 SKU 的商品页,每个尺寸带价格、起订量、库存状态和交期(渲染页面时从商城读出,随页面一起缓存);同样的报价写进页面的 `Product` 结构化数据;规格查找表和目录里每个商品名下显示起价;聚合页、行业页、案例页、问答页、工程资料页和联系页——全部有英、德、法、西四种语言,带 `hreflang`、canonical、sitemap 和 FAQ 结构化数据;字体由站点自己提供;英文页面以美元标价,其余语言以欧元标价,并可切换到商城支持的任一币种;除了三个小脚本之外没有客户端 JavaScript,而且页面没有它们也是完整的:给规格查找表加筛选、币种切换、工程资料页上的计算器 | 聚合页所列商品旁的价格(Mallok 目前不会告诉插件一个内容页列出了哪些商品) |
| **目录数据** | 规格、以整数最小单位存储的价格、库存、起订量、交期、按单生产策略——在后台每个商品编辑器下方的表单里编辑;库存的每次变动都记入流水;删除商品时一并清理它的规格 | |
| **定价** | 美元基准价;欧元和英镑按 ECB 汇率换算,带缓冲、价位取整和漂移阈值;手动价格永不被覆盖;某个商品的价格变动时,清除该商品页面的缓存 | |
| **购物车** | 商品页每个尺寸旁有一个表单;购物车页沿用站点自己的设计和语言,可改数量、移除、选币种,全程不需要脚本。起订量和库存由服务端校验,被拒绝的原因直接显示在购物车页上 | 把购物车作为一次询盘提交;结账 |
| **订单与付款** | 逻辑已写好并有测试,但还没有入口可以用到:带行快照的订单、无论 Stripe 通知多少次都只扣一次库存的付款、超卖订单、退还库存的退款、Stripe 签名校验、四种语言的订单邮件 | 结账页和订单页、webhook 路由、后台的订单处理 |

原因和计划见 [`docs/superpowers/specs/2026-09-30-nundar-on-mallok-design.md`](docs/superpowers/specs/2026-09-30-nundar-on-mallok-design.md)。

### 容易做错、并且有测试保证的商业规则

- 金额一律是整数最小单位,任何地方都不用浮点数。
- 页面只说一个尺寸能不能买到,不说还剩多少:数量在下一笔成交后就错了,状态很少变。
- 结构化数据里的报价与页面上印出的价格完全一致,有测试逐位比对。
- 购物车只存规格和数量,绝不存价格。
- 起订量由表单校验,**并且**由服务端再校验一次,因为表单可以被绕过。
- 公开页面对每个访客都一样,可以一直留在缓存里:页面上没有任何与购物车有关的内容,购物车的 cookie 只发给商城自己的路由。
- 库存带数据库约束,付款时的扣减不可能变成负数:有测试证明整个 D1 batch 会回滚。
- 库存只在付款确认后扣减,绝不提前,而且只扣一次:有测试让同一笔付款在同一时刻到达两次。
- 一笔付款只有带着 Stripe 对原始字节的签名才会被采信,而且只在五分钟内有效。
Expand Down Expand Up @@ -123,7 +126,7 @@ npm run seed:local

- **内容**在 `content/` 里,每个页面一个目录,每种语言一个 Markdown 文件。换成你自己的再发布即可。
- **站点自己的文案**——名称、导航、首页的标题和各个区块、页脚——在 `site.json` 里。每种语言在那里有各自的值;这些文案没有一句写死在主题里。
- **规格和价格**在 `seed/shop-sample.sql` 里,商品页上列出的每个 SKU 对应一个规格。
- **规格和价格**在 `seed/shop-sample.sql` 里,商品页上列出的每个 SKU 对应一个规格,各有一个美元基准价和手工录入的欧元、英镑价格。某个币种不填,商城就按基准价和当天汇率换算。
- **字体**由站点自己提供(`src/theme/assets/fonts/`),采用 SIL Open Font License;许可文本和每个文件的来源放在同一目录。主题不从任何第三方加载资源。

## 常用命令
Expand All @@ -141,7 +144,7 @@ npm run seed:local

## 部署

暂时不要部署。现在部署出来的商城只有目录,没有价格,也没有购物车页。等 Nundar 就绪后,部署走 Mallok 自己的流程:`npx mallok create . --slug <slug>`,它会在你自己的 Cloudflare 账号上创建 Worker、D1 数据库和 R2 存储桶。这条路径还没有在本仓库上跑过。
暂时不要部署。现在部署出来的商城有带价格的目录和购物车,但购物车没有下一步:还不能结账,也不能把购物车作为询盘提交;并且这里的一切都还没有在真实的 Cloudflare 账号上实测过。等 Nundar 就绪后,部署走 Mallok 自己的流程:`npx mallok create . --slug <slug>`,它会在你自己的 Cloudflare 账号上创建 Worker、D1 数据库和 R2 存储桶。这条路径还没有在本仓库上跑过。

## 语言和币种

Expand Down
Loading
Loading