@blamejs/blamejs-shop 0.0.49 → 0.0.51
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/lib/storefront.js +119 -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.51 (2026-05-22) — **SEO crawl surfaces — `/robots.txt` + `/sitemap.xml` listing every active product.** Crawlers landing on the live shop had no discovery surface — every product page was reachable only by being linked from the home grid, and there was no robots.txt to direct the crawl. This release wires `/robots.txt` (allow-everything-except-session-scoped, pointing at the sitemap) and `/sitemap.xml` (lists the home + /admin + every `status='active'` product with its `updated_at` lastmod, capped at 1000 rows). The XML is hand-rolled — no node:xml dep — with attribute-escaped values so a slug containing `&` or `<` can't break out of the document. **Added:** *`GET /robots.txt`* — `User-agent: *` + `Allow: /` for crawlable surfaces. Disallow paths cover the session-scoped + operator-only surfaces (`/admin`, `/cart`, `/checkout`, `/pay/`, `/orders/`, `/account`) — none of those have crawl value and exposing them risks crawlers tripping rate limits on the cart-bound DO. `Sitemap:` directive resolves from the request `Host` header so the same handler serves the right absolute URL for `localhost:8080` (dev), `blamejs-shop.coocoo.workers.dev` (live), or any custom domain. `Cache-Control: public, max-age=3600`. Operators with stricter requirements override by uploading a `robots.txt` key to R2 — the Worker's static-asset bridge serves R2 keys ahead of the storefront router, so the bucket file wins. · *`GET /sitemap.xml`* — Lists the home page (priority 1.0, daily changefreq), the `/admin` landing (priority 0.3, monthly changefreq), and every `status='active'` product (priority 0.8, weekly changefreq) with the product's `updated_at` or `created_at` as `lastmod` (ISO date). Capped at 1000 rows — the sitemap spec allows 50,000 but a shop at that scale should pre-segment into a sitemap index. The XML is hand-rolled, attribute-escape applies `&`, `<`, `>`, `"`, `'` so an operator-supplied slug can't break the document. `Cache-Control: public, max-age=300` so a catalog update propagates inside five minutes without an operator action.
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
11
15
|
- 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.
|
|
12
16
|
|
|
13
17
|
- 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.
|
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
|
|
|
@@ -2398,6 +2432,79 @@ function mount(router, deps) {
|
|
|
2398
2432
|
});
|
|
2399
2433
|
}
|
|
2400
2434
|
|
|
2435
|
+
// robots.txt — minimal crawl policy. Allow everything except
|
|
2436
|
+
// the admin API + cart + account + checkout / pay / orders (these
|
|
2437
|
+
// are session-scoped or operator-only, no crawl value), and
|
|
2438
|
+
// point at the sitemap. Operators with stricter requirements
|
|
2439
|
+
// replace the file at the same path via R2 (the Worker's
|
|
2440
|
+
// static-asset bridge would serve it ahead of this route if a
|
|
2441
|
+
// `robots.txt` key exists in the bucket).
|
|
2442
|
+
router.get("/robots.txt", function (req, res) {
|
|
2443
|
+
res.status(200);
|
|
2444
|
+
res.setHeader && res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
2445
|
+
res.setHeader && res.setHeader("cache-control", "public, max-age=3600");
|
|
2446
|
+
var hostHeader = req.headers && (req.headers.host || req.headers.Host) || "";
|
|
2447
|
+
var origin = hostHeader ? ("https://" + hostHeader) : "";
|
|
2448
|
+
var body =
|
|
2449
|
+
"User-agent: *\n" +
|
|
2450
|
+
"Allow: /\n" +
|
|
2451
|
+
"Disallow: /admin\n" +
|
|
2452
|
+
"Disallow: /cart\n" +
|
|
2453
|
+
"Disallow: /checkout\n" +
|
|
2454
|
+
"Disallow: /pay/\n" +
|
|
2455
|
+
"Disallow: /orders/\n" +
|
|
2456
|
+
"Disallow: /account\n" +
|
|
2457
|
+
(origin ? ("Sitemap: " + origin + "/sitemap.xml\n") : "");
|
|
2458
|
+
res.end ? res.end(body) : res.send(body);
|
|
2459
|
+
});
|
|
2460
|
+
|
|
2461
|
+
// sitemap.xml — lists every active product slug + the home page
|
|
2462
|
+
// + the framework-API landing. Cached short (5 minutes) so a
|
|
2463
|
+
// catalog update propagates without an operator action. The XML
|
|
2464
|
+
// is hand-rolled (no node:xml dep) because the surface is
|
|
2465
|
+
// ~3 fields per row and the XML-escape is trivial.
|
|
2466
|
+
router.get("/sitemap.xml", async function (req, res) {
|
|
2467
|
+
function _xmlEsc(s) {
|
|
2468
|
+
return String(s == null ? "" : s)
|
|
2469
|
+
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
2470
|
+
.replace(/"/g, """).replace(/'/g, "'");
|
|
2471
|
+
}
|
|
2472
|
+
var hostHeader = req.headers && (req.headers.host || req.headers.Host) || "";
|
|
2473
|
+
var origin = hostHeader ? ("https://" + hostHeader) : "";
|
|
2474
|
+
var urls = [];
|
|
2475
|
+
urls.push({ loc: origin + "/", changefreq: "daily", priority: "1.0" });
|
|
2476
|
+
urls.push({ loc: origin + "/admin", changefreq: "monthly", priority: "0.3" });
|
|
2477
|
+
try {
|
|
2478
|
+
var page = await deps.catalog.products.list({ status: "active", limit: 1000 });
|
|
2479
|
+
for (var i = 0; i < page.rows.length; i += 1) {
|
|
2480
|
+
var p = page.rows[i];
|
|
2481
|
+
var lastmod = new Date(p.updated_at || p.created_at || Date.now()).toISOString().slice(0, 10);
|
|
2482
|
+
urls.push({
|
|
2483
|
+
loc: origin + "/products/" + p.slug,
|
|
2484
|
+
lastmod: lastmod,
|
|
2485
|
+
changefreq: "weekly",
|
|
2486
|
+
priority: "0.8",
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
} catch (_e) { /* drop-silent — catalog unreachable means sitemap drops product rows */ }
|
|
2490
|
+
var body = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
|
2491
|
+
"<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n";
|
|
2492
|
+
for (var u = 0; u < urls.length; u += 1) {
|
|
2493
|
+
var item = urls[u];
|
|
2494
|
+
body += " <url>\n";
|
|
2495
|
+
body += " <loc>" + _xmlEsc(item.loc) + "</loc>\n";
|
|
2496
|
+
if (item.lastmod) body += " <lastmod>" + _xmlEsc(item.lastmod) + "</lastmod>\n";
|
|
2497
|
+
body += " <changefreq>" + _xmlEsc(item.changefreq) + "</changefreq>\n";
|
|
2498
|
+
body += " <priority>" + _xmlEsc(item.priority) + "</priority>\n";
|
|
2499
|
+
body += " </url>\n";
|
|
2500
|
+
}
|
|
2501
|
+
body += "</urlset>\n";
|
|
2502
|
+
res.status(200);
|
|
2503
|
+
res.setHeader && res.setHeader("content-type", "application/xml; charset=utf-8");
|
|
2504
|
+
res.setHeader && res.setHeader("cache-control", "public, max-age=300");
|
|
2505
|
+
res.end ? res.end(body) : res.send(body);
|
|
2506
|
+
});
|
|
2507
|
+
|
|
2401
2508
|
// Designed admin landing — the rest of /admin/* is JSON. This
|
|
2402
2509
|
// single GET gives footer links + curious visitors a designed
|
|
2403
2510
|
// page explaining the API-only posture instead of a 404.
|
package/package.json
CHANGED