@blamejs/blamejs-shop 0.0.48 → 0.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +45 -4
- package/lib/storefront.js +46 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.0.x
|
|
10
10
|
|
|
11
|
+
- v0.0.50 (2026-05-22) — **OpenGraph + Twitter Card meta tags — shared links unfurl with proper previews.** Pages rendered without `og:*` or `twitter:*` tags. Sharing the live URL in Slack / iMessage / Twitter / Discord landed a bare URL with no preview, no image, no description. This release wires OpenGraph + Twitter Card meta tags into the LAYOUT head with sensible site-level defaults (brand logo + shop-level lede). The PDP overrides them with product-specific values — `og:title` is the product title, `og:description` is the catalog `description`, `og:image` is the first attached media URL — so a product share renders with the actual hero image + product copy. **Added:** *OpenGraph + Twitter Card tags in `LAYOUT`* — `<meta property="og:type">`, `og:site_name`, `og:title`, `og:description`, `og:image`, `og:url` + the Twitter Card analogues (`twitter:card=summary_large_image`, `twitter:title`, `twitter:description`, `twitter:image`). All templated through `{{og_*}}` placeholders so the `_render` HTML-escape protects against operator-supplied content breaking out of the attribute boundary. `<meta name="description">` also lands so non-OG crawlers see the same description. · *Default OG values on `_wrap(opts)`* — Every page renders with sensible defaults: `og:type=website`, `og:title=<page title> — <shop name>`, `og:description=Open-source ecommerce framework built on blamejs. Server-rendered HTML, post-quantum crypto, zero npm runtime dependencies.`, `og:image=/assets/brand/logo.png`. Per-renderer overrides via `opts.og_*` work surgically — only the field passed in overrides; the rest stay on defaults. · *Product-specific OG on the PDP* — `renderProduct` overrides four OG fields: `og_type=product`, `og_title=<product.title> — <shop_name>`, `og_description=<product.description>` (or a generated fallback when the description is empty), `og_image=<assetPrefix><heroMedia.r2_key>` (the first attached media row). A PDP share now renders the SVG hero + the product copy, not the brand logo + a generic shop description.
|
|
12
|
+
|
|
13
|
+
- v0.0.49 (2026-05-22) — **README refresh — scoped install command, live-demo URL, refreshed primitive table + migration list.** The README hadn't been touched since v0.0.43; the install snippet still pointed at the unscoped `blamejs-shop` name and the primitives table listed only the original commerce surface. Operators landing on the GitHub repo now see the correct `npm install @blamejs/blamejs-shop`, the live demo URL, badges for npm + license + SLSA L3, and a primitives table that includes `customers`, `subscriptions`, `newsletter`, and `theme` alongside the original commerce stack. **Changed:** *README — install snippet uses `@blamejs/blamejs-shop`* — Replaces the unscoped clone-and-smoke recipe with `npm install @blamejs/blamejs-shop` + the `require()` shape that resolves the framework + adapters. The local clone path stays as the secondary recipe for operators who want to develop against the repo. Three badges land in the header — npm version, Apache-2.0 license, SLSA Level 3. · *README — primitive table extended* — Adds rows for `customers` (passkey-only, email hash-only), `subscriptions` (Stripe-backed recurring), `newsletter` (signup primitive shipped in v0.0.41), and `theme` (file-backed templates). The `storefront` row gains the full surface description — utility bar, sticky header, dark hero with code-preview card, marquee, featured-product callout, collections grid, framework feature band, designed catalog grid, newsletter band, and the four-column footer. `admin` row mentions the subscription-plan routes. · *README — migration list updated* — Lists migrations 0001-0010 (catalog, cart, order, shop_config, webhooks, customers, inventory_thresholds, subscriptions, newsletter_signups) so operators don't have to grep the directory to know what schema lands. Adds the demo-seed recipe (`scripts/seed-sample-products.sql` + `scripts/seed-sample-product-media.sql`) for one-shot population of the live demo's four products + their SVG hero images. · *Live-demo URL pinned* — `https://blamejs-shop.coocoo.workers.dev/` lands in the README header so operators evaluating the framework can see a real deployed instance rendering the storefront design system end-to-end before committing to wiring their own Cloudflare account.
|
|
14
|
+
|
|
11
15
|
- v0.0.48 (2026-05-22) — **Home page — featured-product callout between the primitives marquee and the collections grid.** The home page rhythm went marquee → 6-tile collections grid → framework band, so a single product never got more than the four-tile catalog card's surface area to introduce itself. This release inserts a split callout (square hero image on the left, copy column with `Featured` eyebrow + title + description + accent-coloured price + `View product →` CTA on the right) between the marquee and the collections grid. The render picks the first product with attached media; operators wrapping `renderHome` can override the selection via `opts.featured`. **Added:** *`.featured-product` section* — Sits between the primitives marquee and the collections grid. Two-column grid at desktop (square hero + copy column), stacks to image-above-copy at <64rem with a 16/10 image aspect ratio. Title is a `clamp(1.75rem, 3vw, 2.5rem)` headline; price renders in the display font at `--text-2xl` with the accent colour + tabular-nums. Image is wrapped in an anchor to the PDP and has a hover-scale of `1.03` driven through the existing `--duration-slow` ease curve. The card itself has a hairline border on `var(--paper)` so it reads as a content tile, not a hero band — keeps the dark hero above and the framework band below as the page's main rhythm changes. · *`storefront.renderHome({ ..., featured? })` selection override* — When `opts.featured` is supplied the renderer uses it verbatim (shape: `{ title, description, price, slug, image_url, image_alt }`). When absent, the renderer scans the active-products list for the first row with `image_url` and uses that as the featured pick. Products without media are skipped; when no product has media the callout renders as an empty string and the page rhythm falls back to marquee → collections directly. **Changed:** *Product data shape — `description` carried through* — The renderHome `products.map(...)` projection now includes `description` (from the catalog products row) so the featured callout's lede can pull product-specific copy. Existing callers that didn't pass a description continue to render — the callout falls back to a stock 'Server-rendered, PQC-secured, shipped from origin' line.
|
|
12
16
|
|
|
13
17
|
- v0.0.47 (2026-05-22) — **Account dashboard — 4-stat row + recent-orders table with per-order product thumbnails.** The dashboard surfaced a bare three-column orders table (order id, status, total). This release adds a 4-stat row above the body (orders / lifetime spend / member since / passkeys) and a new 'Items' column on the orders table that renders the first four product thumbnails per order with a `+N` overflow chip when there are more. Visitors land on a richer surface that summarises their relationship with the shop instead of just listing opaque order ids. **Added:** *Stats row — orders, lifetime spend, member since, passkeys* — Four-up grid above the orders table. `Orders` is the count of returned orders (capped at 10 by the listForCustomer page; future patch could surface the true count via a `count()` shape on the order primitive). `Lifetime spend` sums `grand_total_minor` across orders in the dominant currency (falls back to '—' if multiple currencies appear, since silently mis-summing them is worse than declining to compute). `Member since` reads `customer.created_at` when present, otherwise the earliest order's `created_at` — both as an ISO date. `Passkeys` calls `deps.customers.listPasskeys(customer.id)` (drop-silent on `TypeError` if the primitive build doesn't expose the listing surface yet). · *`Items` column — per-order product thumbnails* — Per-order, the route walks `order.lines`, collapses to unique variant ids, and resolves each through cached `catalog.variants.get` → `catalog.products.get` + `catalog.media.listForProduct`. The variant cache is held across orders so a customer with multiple orders containing the same SKU only hits the catalog once. The table cell surfaces the first four thumbs (`.account-order__thumb`, 2.5rem rounded tiles); orders with more than four show a `+N` overflow chip. Status renders as the same `.pdp__badge`-style pill the order page uses — green-bordered `--ok` variant for `completed` / `shipped` / `delivered`. **Changed:** *`storefront.renderAccount({ customer, orders, order_product_lookup?, passkey_count?, asset_prefix? })`* — New optional opts. `order_product_lookup` is `{ order_id: [{ product, hero_media }, ...] }` — the route bundles it in. `passkey_count` is the integer count of enrolled passkeys (0 when the customers primitive can't enumerate them on this build). `asset_prefix` defaults to `/assets/` for image URL composition. All three are optional — calling with just `customer` + `orders` continues to render cleanly, the stat row just shows `0` / `—` for the missing fields.
|
package/README.md
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
# blamejs.shop
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@blamejs/blamejs-shop)
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
[](https://slsa.dev)
|
|
8
|
+
|
|
9
|
+
Open-source ecommerce framework built on [blamejs](https://github.com/blamejs/blamejs). Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.
|
|
10
|
+
|
|
11
|
+
Live demo: **https://blamejs-shop.coocoo.workers.dev/**
|
|
4
12
|
|
|
5
13
|
## Requirements
|
|
6
14
|
|
|
7
15
|
- Node.js LTS (>= 24.14.1)
|
|
16
|
+
- For a deployable shop: a Cloudflare account (Workers, Containers, D1, R2, KV, Durable Objects). Local development works without it via `node:sqlite`.
|
|
8
17
|
|
|
9
18
|
## Install
|
|
10
19
|
|
|
20
|
+
```bash
|
|
21
|
+
npm install @blamejs/blamejs-shop
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
var bShop = require("@blamejs/blamejs-shop");
|
|
26
|
+
var b = bShop.framework; // the vendored blamejs
|
|
27
|
+
var d1 = bShop.externaldbD1; // Cloudflare D1 adapter
|
|
28
|
+
var cat = bShop.catalog.create({ query }); // catalog primitive
|
|
11
29
|
```
|
|
30
|
+
|
|
31
|
+
Or clone for development:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
12
34
|
git clone https://github.com/blamejs/blamejs.shop.git
|
|
13
35
|
cd blamejs.shop
|
|
14
36
|
bash scripts/vendor-update.sh blamejs latest
|
|
@@ -39,19 +61,38 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
|
|
|
39
61
|
| **`lib/order.js`** | FSM-driven post-checkout record via upstream `b.fsm`. States: pending → paid → fulfilling → shipped → delivered (+ refunded / cancelled). Every transition appends to `order_transitions`. |
|
|
40
62
|
| **`lib/checkout.js`** | Orchestrator. `quote()` returns priced quote; `confirm()` creates PaymentIntent + persists order in pending; `handleStripeEvent()` verifies webhook + fires the FSM transition (idempotent on re-delivery). |
|
|
41
63
|
| **`lib/email.js`** | Transactional templates — order receipt, ship notification, refund confirmation. Strict `{{var}}` renderer with HTML escape + refusal of unknown / unused placeholders. Composed on `b.mail` (DKIM/SPF/DMARC/BIMI upstream). |
|
|
42
|
-
| **`lib/storefront.js`** | Server-rendered HTML —
|
|
43
|
-
| **`lib/
|
|
64
|
+
| **`lib/storefront.js`** | Server-rendered HTML — utility bar + sticky header + dark hero with code-preview card + primitives marquee + featured-product callout + collections grid + framework feature band + designed catalog grid + newsletter band + four-column footer. Designed surfaces also for PDP, cart, checkout, pay, order, account login / register / dashboard, search results, `/admin` API landing, 404. Image-bearing cards on the home + search grids pull from `catalog.media`. The default theme stylesheet is external (R2-served `themes/default/assets/css/main.css`) and CSP-compliant — operators override by uploading a replacement at the same key, by passing `opts.theme_css` to renderers, or by registering a named theme through the `theme` primitive. |
|
|
65
|
+
| **`lib/customers.js`** | Customer accounts — passkey-only (WebAuthn). Email is stored hash-only (`b.crypto.namespaceHash` namespace `customer-email`); the raw address never lands in D1. Passkey credentials carry CBOR-encoded public keys, transport hints, and SHA3-512-fingerprinted attestation. Account routes (`/account/login`, `/account/register`, `/account`) ship as designed cards on the storefront. |
|
|
66
|
+
| **`lib/subscriptions.js`** | Stripe-backed recurring billing — `subscription_plans` (interval / amount / trial) + `subscriptions` (mirrors Stripe's object byte-for-byte). `subscriptions.create` POSTs to Stripe via the payment dep, then persists the returned object locally. `handleStripeEvent` replays `customer.subscription.*` events into the local row so the shop has an authoritative view without round-tripping. |
|
|
67
|
+
| **`lib/newsletter.js`** | Operator-collected email broadcast list — `signup({ email, source })` composes `b.guardEmail` for shape validation, `b.crypto.namespaceHash` for the dedup key, and `INSERT OR IGNORE` for idempotency. Storefront POST `/newsletter` route renders a designed thank-you card with separate copy for the `new` vs `dedup` branches. |
|
|
68
|
+
| **`lib/admin.js`** | Bearer-token-gated CRUD over catalog + orders + refunds + bulk CSV import + subscription plans. Token compared via `b.crypto.timingSafeEqual`. Errors as RFC 9457 problem documents via `b.problemDetails`. Audit emission on every mutation. |
|
|
44
69
|
| **`lib/catalog-import.js`** | Bulk CSV import — `POST /admin/catalog/import` accepts a `text/csv` body, parses via `b.csv`, content-safety-filters every cell through `b.guardCsv` (formula-injection / bidi / control / dangerous-function denylist), validates exact header order, de-dupes rows by `product_slug`, returns per-row errors without aborting. Default 1 MiB / 10000 rows caps. |
|
|
70
|
+
| **`lib/theme.js`** | File-backed templates with fallback chain. Operators register a named theme under `<themesDir>/<name>/*.html` and the storefront dispatches every renderer through it. `assetUrl(path)` resolves to `/assets/themes/<name>/<path>`. The shipped `default` theme is the fallback. |
|
|
45
71
|
|
|
46
72
|
### Migrations applied to D1
|
|
47
73
|
|
|
48
74
|
- `migrations-d1/0001_catalog.sql` — products, variants, prices, inventory, media
|
|
49
75
|
- `migrations-d1/0002_cart.sql` — carts, cart_lines (partial-unique active-cart-per-session)
|
|
50
76
|
- `migrations-d1/0003_order.sql` — orders, order_lines, order_transitions (FSM audit log)
|
|
77
|
+
- `migrations-d1/0004_shop_config.sql` — shop_config (operator-tunable runtime config)
|
|
78
|
+
- `migrations-d1/0005_webhooks.sql` — webhooks subscriptions + deliveries (signed fan-out)
|
|
79
|
+
- `migrations-d1/0006_customers.sql` — customers + passkey_credentials
|
|
80
|
+
- `migrations-d1/0008_inventory_thresholds.sql` — low-stock alert thresholds + alerts
|
|
81
|
+
- `migrations-d1/0009_subscriptions.sql` — subscription_plans + subscriptions (Stripe-mirrored)
|
|
82
|
+
- `migrations-d1/0010_newsletter_signups.sql` — email signups with hash-based dedup
|
|
83
|
+
|
|
84
|
+
### Demo seed
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
wrangler d1 execute blamejs-shop --remote --file=scripts/seed-sample-products.sql
|
|
88
|
+
wrangler d1 execute blamejs-shop --remote --file=scripts/seed-sample-product-media.sql
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Lands four demo products (Operator Tee, Edge Reader v1, Operator License, Starter Bundle) with brand-coloured SVG hero images under `scripts/sample-product-images/`. Idempotent — re-running is a no-op via `INSERT OR IGNORE`.
|
|
51
92
|
|
|
52
93
|
### Tests
|
|
53
94
|
|
|
54
|
-
|
|
95
|
+
19+ layer-1 test suites running against in-memory `node:sqlite` loaded from the live migrations. Layer-2 integration test boots the full HTTP server on a 127.0.0.1 ephemeral port and exercises the complete browse → PDP → add-to-cart → cart-update → cart-remove flow. Schema CHECK / UNIQUE / FK constraints exercised end-to-end.
|
|
55
96
|
|
|
56
97
|
## Operator quick-start
|
|
57
98
|
|
package/lib/storefront.js
CHANGED
|
@@ -60,8 +60,19 @@ var LAYOUT =
|
|
|
60
60
|
" <meta charset=\"utf-8\">\n" +
|
|
61
61
|
" <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n" +
|
|
62
62
|
" <title>{{title}} — {{shop_name}}</title>\n" +
|
|
63
|
+
" <meta name=\"description\" content=\"{{og_description}}\">\n" +
|
|
63
64
|
" <link rel=\"icon\" type=\"image/svg+xml\" href=\"/assets/brand/favicon.svg\">\n" +
|
|
64
65
|
" <link rel=\"stylesheet\" href=\"{{theme_css}}\">\n" +
|
|
66
|
+
" <meta property=\"og:type\" content=\"{{og_type}}\">\n" +
|
|
67
|
+
" <meta property=\"og:site_name\" content=\"{{shop_name}}\">\n" +
|
|
68
|
+
" <meta property=\"og:title\" content=\"{{og_title}}\">\n" +
|
|
69
|
+
" <meta property=\"og:description\" content=\"{{og_description}}\">\n" +
|
|
70
|
+
" <meta property=\"og:image\" content=\"{{og_image}}\">\n" +
|
|
71
|
+
" <meta property=\"og:url\" content=\"{{og_url}}\">\n" +
|
|
72
|
+
" <meta name=\"twitter:card\" content=\"summary_large_image\">\n" +
|
|
73
|
+
" <meta name=\"twitter:title\" content=\"{{og_title}}\">\n" +
|
|
74
|
+
" <meta name=\"twitter:description\" content=\"{{og_description}}\">\n" +
|
|
75
|
+
" <meta name=\"twitter:image\" content=\"{{og_image}}\">\n" +
|
|
65
76
|
"</head>\n" +
|
|
66
77
|
"<body>\n" +
|
|
67
78
|
" <a class=\"skip-link\" href=\"#main\">Skip to content</a>\n" +
|
|
@@ -179,14 +190,28 @@ function _wrap(opts) {
|
|
|
179
190
|
var themeCss = (opts && typeof opts.theme_css === "string" && opts.theme_css.length)
|
|
180
191
|
? opts.theme_css
|
|
181
192
|
: DEFAULT_THEME_CSS_URL;
|
|
193
|
+
// OpenGraph / Twitter Card defaults — every page sets reasonable
|
|
194
|
+
// fallbacks; per-page renderers (PDP, etc.) can override via
|
|
195
|
+
// `opts.og_*` for a product-specific share preview.
|
|
196
|
+
var shopName = opts.shop_name || "blamejs.shop";
|
|
197
|
+
var ogType = opts.og_type || "website";
|
|
198
|
+
var ogTitle = opts.og_title || (opts.title ? opts.title + " — " + shopName : shopName);
|
|
199
|
+
var ogDescription = opts.og_description || "Open-source ecommerce framework built on blamejs. Server-rendered HTML, post-quantum crypto, zero npm runtime dependencies.";
|
|
200
|
+
var ogImage = opts.og_image || "/assets/brand/logo.png";
|
|
201
|
+
var ogUrl = opts.og_url || "";
|
|
182
202
|
return _render(LAYOUT, {
|
|
183
|
-
title:
|
|
184
|
-
shop_name:
|
|
185
|
-
cart_count:
|
|
186
|
-
year:
|
|
187
|
-
search_q:
|
|
188
|
-
theme_css:
|
|
189
|
-
|
|
203
|
+
title: opts.title,
|
|
204
|
+
shop_name: shopName,
|
|
205
|
+
cart_count: opts.cart_count == null ? 0 : opts.cart_count,
|
|
206
|
+
year: String(new Date().getUTCFullYear()),
|
|
207
|
+
search_q: opts.search_q == null ? "" : opts.search_q,
|
|
208
|
+
theme_css: themeCss,
|
|
209
|
+
og_type: ogType,
|
|
210
|
+
og_title: ogTitle,
|
|
211
|
+
og_description: ogDescription,
|
|
212
|
+
og_image: ogImage,
|
|
213
|
+
og_url: ogUrl,
|
|
214
|
+
body: "RAW_BODY_PLACEHOLDER",
|
|
190
215
|
}).replace("RAW_BODY_PLACEHOLDER", opts.body);
|
|
191
216
|
// The body is RAW HTML (already rendered + escaped at the
|
|
192
217
|
// per-fragment level). The placeholder swap is post-render so the
|
|
@@ -740,12 +765,21 @@ function renderProduct(opts) {
|
|
|
740
765
|
})
|
|
741
766
|
.replace("RAW_GALLERY_PLACEHOLDER", galleryHtml)
|
|
742
767
|
.replace("RAW_ROWS_PLACEHOLDER", rows);
|
|
768
|
+
// Product-specific OpenGraph + Twitter Card values so shares
|
|
769
|
+
// unfurl as "Operator Tee — blamejs.shop" with the SVG hero, not
|
|
770
|
+
// the default shop-level description + brand logo.
|
|
771
|
+
var heroMedia = (opts.media && opts.media[0]) || null;
|
|
772
|
+
var ogImage = heroMedia ? ((opts.asset_prefix || "/assets/") + heroMedia.r2_key) : "/assets/brand/logo.png";
|
|
743
773
|
return _wrap({
|
|
744
|
-
title:
|
|
745
|
-
shop_name:
|
|
746
|
-
cart_count:
|
|
747
|
-
theme_css:
|
|
748
|
-
|
|
774
|
+
title: opts.product.title,
|
|
775
|
+
shop_name: shopName,
|
|
776
|
+
cart_count: cartCount,
|
|
777
|
+
theme_css: opts.theme_css,
|
|
778
|
+
og_type: "product",
|
|
779
|
+
og_title: opts.product.title + " — " + shopName,
|
|
780
|
+
og_description: description || ("Browse " + opts.product.title + " on " + shopName + "."),
|
|
781
|
+
og_image: ogImage,
|
|
782
|
+
body: body,
|
|
749
783
|
});
|
|
750
784
|
}
|
|
751
785
|
|
package/package.json
CHANGED