@blamejs/blamejs-shop 0.2.11 → 0.2.13
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/asset-manifest.json +3 -3
- package/lib/storefront.js +276 -24
- package/lib/translations.js +2 -2
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/.github/workflows/actions-lint.yml +71 -23
- package/lib/vendor/blamejs/.github/workflows/cflite_batch.yml +2 -0
- package/lib/vendor/blamejs/.github/workflows/cflite_pr.yml +2 -0
- package/lib/vendor/blamejs/.github/workflows/ci.yml +17 -0
- package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +18 -10
- package/lib/vendor/blamejs/.github/workflows/release-container.yml +29 -16
- package/lib/vendor/blamejs/.github/workflows/sha-to-tag-verify.yml +22 -3
- package/lib/vendor/blamejs/.github/zizmor.yml +29 -0
- package/lib/vendor/blamejs/.pinact.yaml +24 -8
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/lib/archive-wrap.js +1 -1
- package/lib/vendor/blamejs/lib/auth/jwt.js +7 -4
- package/lib/vendor/blamejs/lib/auth/saml.js +2 -2
- package/lib/vendor/blamejs/lib/cose.js +1 -1
- package/lib/vendor/blamejs/lib/did.js +1 -1
- package/lib/vendor/blamejs/lib/guard-archive.js +2 -2
- package/lib/vendor/blamejs/lib/guard-auth.js +5 -5
- package/lib/vendor/blamejs/lib/guard-cidr.js +4 -4
- package/lib/vendor/blamejs/lib/guard-csv.js +6 -6
- package/lib/vendor/blamejs/lib/guard-domain.js +4 -4
- package/lib/vendor/blamejs/lib/guard-email.js +6 -5
- package/lib/vendor/blamejs/lib/guard-filename.js +4 -4
- package/lib/vendor/blamejs/lib/guard-graphql.js +5 -5
- package/lib/vendor/blamejs/lib/guard-html.js +2 -2
- package/lib/vendor/blamejs/lib/guard-image.js +1 -1
- package/lib/vendor/blamejs/lib/guard-json.js +5 -5
- package/lib/vendor/blamejs/lib/guard-jsonpath.js +4 -4
- package/lib/vendor/blamejs/lib/guard-jwt.js +5 -5
- package/lib/vendor/blamejs/lib/guard-markdown.js +6 -6
- package/lib/vendor/blamejs/lib/guard-mime.js +6 -6
- package/lib/vendor/blamejs/lib/guard-oauth.js +5 -5
- package/lib/vendor/blamejs/lib/guard-pdf.js +1 -1
- package/lib/vendor/blamejs/lib/guard-regex.js +4 -4
- package/lib/vendor/blamejs/lib/guard-shell.js +4 -4
- package/lib/vendor/blamejs/lib/guard-svg.js +5 -5
- package/lib/vendor/blamejs/lib/guard-template.js +4 -4
- package/lib/vendor/blamejs/lib/guard-time.js +6 -6
- package/lib/vendor/blamejs/lib/guard-uuid.js +6 -6
- package/lib/vendor/blamejs/lib/guard-xml.js +4 -4
- package/lib/vendor/blamejs/lib/guard-yaml.js +5 -5
- package/lib/vendor/blamejs/lib/link-header.js +3 -2
- package/lib/vendor/blamejs/lib/mail-agent.js +4 -2
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +2 -2
- package/lib/vendor/blamejs/lib/tsa.js +2 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.23.json +42 -0
- package/lib/vendor/blamejs/release-notes/v0.13.24.json +26 -0
- package/lib/vendor/blamejs/test/smoke.js +25 -0
- 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.2.x
|
|
10
10
|
|
|
11
|
+
- v0.2.13 (2026-05-28) — **Product-grid and product-page visual polish.** Small visual refinements to the catalog grid and product page. Product tiles previously all carried the same fixed corner glow, so a grid of them read as one card stamped repeatedly; the glow position now rotates per tile so the grid looks like a designed set. The product page's buy box gets clearer elevation so it reads as a distinct purchase surface, and the fallback placeholder shown for a product with no image is now legible on the dark theme instead of a near-invisible mark. **Changed:** *Product tiles no longer share one identical glow* — Each product card's media glow now rotates through a set of positions per tile (matching the collection cards) instead of every tile glowing in the exact same corner, so a full grid reads as a designed set rather than a single card repeated. · *Clearer product-page buy box* — The product page's buy box gains a stronger border and a soft shadow so it stands out from the page as the purchase surface. · *Legible image-less placeholder* — The fallback artwork shown when a product has no image was nearly invisible on the dark canvas; its contrast is lifted (grid lines, shield detail, and caption) so it reads as an intentional placeholder. Applied identically on the edge and container render paths.
|
|
12
|
+
|
|
13
|
+
- v0.2.12 (2026-05-28) — **Truthful product availability and real SEO metadata — canonical URLs, structured data, and per-page descriptions.** Two related improvements to the storefront's honesty and discoverability. Product pages now report real availability from inventory (instead of a hardcoded 'In stock') and reflect it in their structured data, and the shipping line is suppressed for digital products. Separately, every page now emits a canonical URL and a populated Open Graph URL (previously empty — which broke social-share unfurls and let sort/currency query parameters fork into duplicate indexed pages), the home page emits the same Organization/Website structured data the edge already did, breadcrumb structured data uses absolute URLs, and the home, category, and search pages get their own meta descriptions instead of one shared default. The footer's account links are regrouped and a shipping-and-returns link was added. **Changed:** *Product availability is truthful* — The product page's stock badge and its `Product`/`AggregateOffer` structured-data `availability` are now driven by real per-SKU inventory (`InStock` / `OutOfStock`) rather than a hardcoded value, so a sold-out item reads as sold out everywhere and structured data no longer mismatches reality. The shipping-time line is suppressed for all-digital products (shown as delivered on purchase). Identical across the edge and container render paths. · *Footer account links regrouped* — The footer's operator-oriented column is now a customer 'Your account' grouping (Account / Orders / Contact), the public Admin link was removed, and a 'Shipping & returns' link was added to the policy area. · *Form fields are legible on the dark theme* — Text inputs, selects, and textareas (checkout, address book, search, sign-in) previously rendered with a 10%-opacity hairline that disappeared against the near-black canvas — fields read as empty voids. They now have a clearly-bounded outline and a lifted fill, stretch to fill their column, and the rule covers `tel`/`url`/`password` inputs it previously missed. **Fixed:** *Canonical + Open Graph URLs on every page* — Every page now includes `<link rel="canonical">` and a populated `og:url` (both were absent/empty before). This fixes social-share previews that unfurled with no link target and prevents tracking/sort/currency query-string variants from being indexed as duplicate pages. Emitted on both render paths, derived from the request's own origin. · *Consistent structured data + unique page descriptions* — The home page now emits the `Organization` and `WebSite` (search-action) structured data that previously only appeared on the edge render, breadcrumb structured-data item URLs are absolute (so the breadcrumb rich result is eligible), and the home, category, and search pages each carry their own meta/OG description instead of sharing one generic string.
|
|
14
|
+
|
|
11
15
|
- v0.2.11 (2026-05-27) — **A clearer checkout — itemised order summary, honest pricing, and recoverable errors.** Reworks the checkout and cart experience. Checkout now uses the same two-column layout as the cart, with a sticky order-summary rail that lists the actual items being bought (thumbnail, title, quantity, line total) rather than a bare number in a lonely column. The summary no longer shows a misleading 'Total' equal to the subtotal — it shows the subtotal plus an honest note that tax and shipping are calculated at the payment step, matching the cart. Adding an item now confirms it and offers to keep shopping, checkout and cart failures render as proper pages you can recover from instead of a blank error string, required fields announce themselves to screen readers, and the header 'Framework' link works from every page. **Changed:** *Checkout shows an itemised, sticky order summary* — The checkout page adopts the cart's two-column layout: the shipping form on the left and a sticky order summary on the right that lists each item (thumbnail, title, quantity, line total) with an 'Edit cart' link back to the cart. Previously the form was a narrow column with a bare subtotal box and no way back. · *Honest checkout pricing* — The order summary no longer renders a 'Total (plus tax + shipping)' line bound to the subtotal value (which read as the final price but wasn't). It now shows the subtotal and the same honest note the cart uses — tax and shipping are calculated at the payment step. · *Add-to-cart confirmation + continue shopping* — Adding an item lands on the cart with a dismissible 'Added to cart' confirmation, and the cart now has a 'Continue shopping' link so multi-item shoppers aren't dead-ended. **Fixed:** *Checkout and cart errors are recoverable, not blank pages* — A declined gift card, an invalid input, or a checkout failure previously dumped an unstyled plain-text error. These now render as styled pages with the reason and a way back — gift-card and loyalty errors re-display the checkout form with the message inline so the shopper can correct it without losing their place. · *Required fields announce to screen readers* — Required form fields kept only a red asterisk (color-only); they now include a visually-hidden '(required)' so assistive technology conveys it. · *Header 'Framework' link works on every page* — It pointed at an on-page anchor that only existed on the home page (a no-op elsewhere); it now resolves from any page.
|
|
12
16
|
|
|
13
17
|
- v0.2.10 (2026-05-27) — **Accessibility and mobile polish — the cookie bar stops covering the page, and forms behave on phones.** A round of default-theme refinements focused on the cookie consent bar, mobile ergonomics, and accessibility. The consent bar previously floated over page content — on a product page or checkout it could sit on top of the price, variant picker, and the add-to-cart / continue-to-payment button on a first visit. It is now a compact bottom bar that reserves its own space (so nothing important is ever hidden under it), scrolls within itself if the text is long, and receives keyboard focus when it appears. Alongside it: mobile form inputs no longer trigger iOS zoom-on-focus, the header touch targets meet the 44px minimum, inline text links are underlined, and a consistency pass tightened button shapes, table-label typography, and small-screen layout. **Changed:** *Visual consistency pass* — Action buttons share a single corner radius (primary and secondary no longer differ in shape), uppercase micro-labels use the monospace face consistently across tables and forms, the catalog grid shows two columns on small phones instead of one, the hero stat row breathes on narrow screens, and a few dark-theme borders/placeholders that read muddy were moved onto the semantic token palette. **Fixed:** *Cookie consent bar no longer covers page content* — On a first visit the consent bar could overlap the product buy box and the checkout form (the price, variant selector, and primary button). It is now a compact bottom-docked bar that reserves matching space at the foot of the page until a choice is made, caps its height and scrolls internally for long text, and moves keyboard focus to its first control when shown — so the page's primary action is never hidden and the bar is reachable without tabbing the whole page. · *Mobile form inputs no longer zoom on iOS* — Text inputs, selects, and textareas render at a 16px minimum on touch devices, so mobile Safari no longer zooms the viewport when a field is focused (which previously left the page zoomed-in and pannable mid-checkout). · *Header touch targets meet the 44px minimum on mobile* — The account and cart controls in the header — the two primary controls on a phone — are now at least 44×44px. · *Clearer link and prompt-glyph affordances* — Inline text links in body copy are underlined (no longer distinguished by color alone), and the terminal prompt glyph on the dark accent bands renders in full contrast.
|
package/lib/asset-manifest.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.2.
|
|
2
|
+
"version": "0.2.13",
|
|
3
3
|
"assets": {
|
|
4
4
|
"css/admin.css": {
|
|
5
5
|
"integrity": "sha384-FgqvXcZygYkJjjOtXAeu9xi6AtYqkchPnJKcS0OeHm8lynhd9r4RfdpoT7P7j7/l",
|
|
6
6
|
"fingerprinted": "css/admin.afd7b964c1f6fe1b.css"
|
|
7
7
|
},
|
|
8
8
|
"css/main.css": {
|
|
9
|
-
"integrity": "sha384-
|
|
10
|
-
"fingerprinted": "css/main.
|
|
9
|
+
"integrity": "sha384-E56XcvNC5nbTWyHJJcepF6sVA1KjRNo/MWFL5F5w2PDN10JNaHdzIAP9a46m2Md8",
|
|
10
|
+
"fingerprinted": "css/main.a76d36ac566703d6.css"
|
|
11
11
|
},
|
|
12
12
|
"js/announcement.js": {
|
|
13
13
|
"integrity": "sha384-z4zcEMn+tScoVnYRE4nEf8N/oyvpxdpaxTNrT4QO/jURChid4+qjAvWkzatCaAPq",
|
package/lib/storefront.js
CHANGED
|
@@ -145,6 +145,7 @@ var LAYOUT =
|
|
|
145
145
|
" <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n" +
|
|
146
146
|
" <title>{{title}} — {{shop_name}}</title>\n" +
|
|
147
147
|
" <meta name=\"description\" content=\"{{og_description}}\">\n" +
|
|
148
|
+
" <link rel=\"canonical\" href=\"{{canonical_url}}\">\n" +
|
|
148
149
|
" <link rel=\"icon\" type=\"image/svg+xml\" href=\"/assets/brand/favicon.svg\">\n" +
|
|
149
150
|
" <link rel=\"icon\" type=\"image/png\" href=\"/assets/brand/favicon.png\">\n" +
|
|
150
151
|
" <link rel=\"apple-touch-icon\" href=\"/assets/brand/favicon.png\">\n" +
|
|
@@ -231,6 +232,7 @@ var LAYOUT =
|
|
|
231
232
|
" <li><a href=\"/?sort=sale\">{{footer_shop_sale}}</a></li>\n" +
|
|
232
233
|
" <li><a href=\"/compare\">{{footer_shop_compare}}</a></li>\n" +
|
|
233
234
|
" <li><a href=\"/cart\">{{footer_shop_cart}}</a></li>\n" +
|
|
235
|
+
" <li><a href=\"/terms\">{{footer_shop_shipping}}</a></li>\n" +
|
|
234
236
|
" </ul>\n" +
|
|
235
237
|
" </div>\n" +
|
|
236
238
|
" <div class=\"site-footer__col\">\n" +
|
|
@@ -247,7 +249,6 @@ var LAYOUT =
|
|
|
247
249
|
" <ul>\n" +
|
|
248
250
|
" <li><a href=\"/account\">{{footer_operators_account}}</a></li>\n" +
|
|
249
251
|
" <li><a href=\"/orders\">{{footer_operators_orders}}</a></li>\n" +
|
|
250
|
-
" <li><a href=\"/admin\">{{footer_operators_admin}}</a></li>\n" +
|
|
251
252
|
" <li><a href=\"mailto:hello@blamejs.shop\">{{footer_operators_contact}}</a></li>\n" +
|
|
252
253
|
" </ul>\n" +
|
|
253
254
|
" </div>\n" +
|
|
@@ -530,6 +531,37 @@ var _DEFAULT_LOCALE_CTX = {
|
|
|
530
531
|
switcher_html: "",
|
|
531
532
|
};
|
|
532
533
|
|
|
534
|
+
// Derive the absolute canonical + og:url for a request. Origin comes from
|
|
535
|
+
// the `Host` header the same way robots.txt/sitemap.xml resolve it (https
|
|
536
|
+
// is the only scheme the storefront serves on); the path is the request
|
|
537
|
+
// path. `canonical_url` strips the query (the canonical names the page,
|
|
538
|
+
// not a filtered/sorted view); `og_url` keeps the full URL so a share of
|
|
539
|
+
// a filtered listing unfurls to that exact view. A request with no Host
|
|
540
|
+
// header yields empty strings — the renderer then omits the absolute URLs
|
|
541
|
+
// rather than emit a host-less one. Defensive request-shape reader:
|
|
542
|
+
// returns defaults on any malformed input.
|
|
543
|
+
function _requestUrls(req) {
|
|
544
|
+
var host = (req && req.headers && (req.headers.host || req.headers.Host)) || "";
|
|
545
|
+
if (!host) return { canonical_url: "", og_url: "" };
|
|
546
|
+
var origin = "https://" + host;
|
|
547
|
+
var rawUrl = (req && typeof req.url === "string" && req.url.length) ? req.url : "/";
|
|
548
|
+
// req.url may be path-only ("/products/x?y=1") or absolute; normalise
|
|
549
|
+
// through URL with the derived origin as the base so either shape works.
|
|
550
|
+
var path = "/";
|
|
551
|
+
var search = "";
|
|
552
|
+
try {
|
|
553
|
+
var parsed = new URL(rawUrl, origin);
|
|
554
|
+
path = parsed.pathname || "/";
|
|
555
|
+
search = parsed.search || "";
|
|
556
|
+
} catch (_e) {
|
|
557
|
+
var qIdx = rawUrl.indexOf("?");
|
|
558
|
+
path = qIdx === -1 ? rawUrl : rawUrl.slice(0, qIdx);
|
|
559
|
+
search = qIdx === -1 ? "" : rawUrl.slice(qIdx);
|
|
560
|
+
if (path.charCodeAt(0) !== 47 /* "/" */) path = "/";
|
|
561
|
+
}
|
|
562
|
+
return { canonical_url: origin + path, og_url: origin + path + search };
|
|
563
|
+
}
|
|
564
|
+
|
|
533
565
|
function _wrap(opts) {
|
|
534
566
|
var themeCss = (opts && typeof opts.theme_css === "string" && opts.theme_css.length)
|
|
535
567
|
? opts.theme_css
|
|
@@ -547,7 +579,14 @@ function _wrap(opts) {
|
|
|
547
579
|
var ogTitle = opts.og_title || (opts.title ? opts.title + " — " + shopName : shopName);
|
|
548
580
|
var ogDescription = opts.og_description || "Open-source ecommerce framework built on blamejs. Server-rendered HTML, post-quantum crypto, zero npm runtime dependencies.";
|
|
549
581
|
var ogImage = opts.og_image || "/assets/brand/logo.png";
|
|
550
|
-
|
|
582
|
+
// Absolute request URL drives both `og:url` (full URL incl. query) and
|
|
583
|
+
// the canonical link (query stripped — the canonical names the page,
|
|
584
|
+
// not the filtered/sorted view). Renderers thread `opts.canonical_url`
|
|
585
|
+
// + `opts.og_url` (built by `_requestUrls` in the route handler); absent
|
|
586
|
+
// them (a unit test calling the renderer directly), both stay empty
|
|
587
|
+
// rather than emit a bogus host-less URL.
|
|
588
|
+
var canonicalUrl = opts.canonical_url || "";
|
|
589
|
+
var ogUrl = opts.og_url || canonicalUrl;
|
|
551
590
|
// Multi-currency display switcher — populated only when the operator
|
|
552
591
|
// configured >1 display currency (opts.currency_options). The block is
|
|
553
592
|
// empty otherwise, so a single-currency store renders unchanged.
|
|
@@ -600,6 +639,7 @@ function _wrap(opts) {
|
|
|
600
639
|
og_description: ogDescription,
|
|
601
640
|
og_image: ogImage,
|
|
602
641
|
og_url: ogUrl,
|
|
642
|
+
canonical_url: canonicalUrl,
|
|
603
643
|
lang: localeCtx.lang || "en",
|
|
604
644
|
dir: localeCtx.dir || "ltr",
|
|
605
645
|
nav_cart_aria: cartAria,
|
|
@@ -656,7 +696,7 @@ var PRODUCT_CARD_IMAGE =
|
|
|
656
696
|
var PRODUCT_CARD =
|
|
657
697
|
"<a class=\"product-card\" href=\"/products/{{slug}}\">\n" +
|
|
658
698
|
" <figure class=\"product-card__media product-card__media--placeholder\">\n" +
|
|
659
|
-
" <svg class=\"media-ph__svg\" viewBox=\"0 0 160 120\" aria-hidden=\"true\"><rect width=\"160\" height=\"120\" fill=\"none\"/><g stroke=\"currentColor\" stroke-opacity=\"0.
|
|
699
|
+
" <svg class=\"media-ph__svg\" viewBox=\"0 0 160 120\" aria-hidden=\"true\"><rect width=\"160\" height=\"120\" fill=\"none\"/><g stroke=\"currentColor\" stroke-opacity=\"0.18\" stroke-width=\"1\"><path d=\"M0 30 H160 M0 60 H160 M0 90 H160 M40 0 V120 M80 0 V120 M120 0 V120\"/></g><g fill=\"none\" stroke=\"#AD38DB\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M80 38 L104 50 L104 76 L80 88 L56 76 L56 50 Z\"/><path d=\"M56 50 L80 62 L104 50 M80 62 V88\" stroke=\"#C75BE8\"/><path d=\"M70 47 L74 50 L70 53\" stroke=\"currentColor\" stroke-width=\"2\"/><path d=\"M77 54 H86\" stroke=\"currentColor\" stroke-width=\"2\"/></g><text x=\"80\" y=\"106\" text-anchor=\"middle\" font-family=\"ui-monospace,Menlo,Consolas,monospace\" font-size=\"9\" letter-spacing=\"1.5\" fill=\"#9b9ba7\">no image yet</text></svg>\n" +
|
|
660
700
|
" </figure>\n" +
|
|
661
701
|
" <div class=\"product-card__meta\">\n" +
|
|
662
702
|
" <h3 class=\"product-card__title\">{{title}}</h3>\n" +
|
|
@@ -966,16 +1006,71 @@ function renderHome(opts) {
|
|
|
966
1006
|
// The hero + value band + catalog section give the home page a
|
|
967
1007
|
// designed surface even when no products are loaded yet —
|
|
968
1008
|
// visitors land on the storefront shell, not a tech demo.
|
|
969
|
-
|
|
1009
|
+
// Organization + WebSite JSON-LD — surfaces the storefront in Google's
|
|
1010
|
+
// knowledge-panel (logo + social) and registers the sitelinks search
|
|
1011
|
+
// box pointing at `/search?q=`. Mirrors the edge renderer
|
|
1012
|
+
// (`worker/render/home.js`) byte-for-byte so the structured data is
|
|
1013
|
+
// identical whichever substrate serves the home page. `_orgWebsiteJsonLd`
|
|
1014
|
+
// resolves the absolute base from the shop name (a bare host or a full
|
|
1015
|
+
// URL both normalise to `https://<host>`).
|
|
1016
|
+
var homeJsonLd = _orgWebsiteJsonLd(shopName);
|
|
1017
|
+
var body = hero + catalog + homeJsonLd;
|
|
970
1018
|
return _wrap(Object.assign({
|
|
971
1019
|
title: title,
|
|
972
1020
|
shop_name: shopName,
|
|
973
1021
|
cart_count: cartCount,
|
|
974
1022
|
theme_css: opts.theme_css,
|
|
1023
|
+
canonical_url: opts.canonical_url,
|
|
1024
|
+
og_url: opts.og_url,
|
|
1025
|
+
og_description: "Shop the blamejs.shop catalog — an open-source, server-rendered storefront with post-quantum crypto and zero npm runtime dependencies.",
|
|
975
1026
|
body: body,
|
|
976
1027
|
}, _currencyWrapOpts(opts)));
|
|
977
1028
|
}
|
|
978
1029
|
|
|
1030
|
+
// Absolute origin (scheme + host, no trailing slash) for building
|
|
1031
|
+
// fully-qualified structured-data URLs. Prefers the request-derived
|
|
1032
|
+
// canonical URL's origin (matches the host the visitor reached); falls
|
|
1033
|
+
// back to the shop-name host when the renderer is called without one (a
|
|
1034
|
+
// unit test, or a theme path). A bare host shop name and a full `https://`
|
|
1035
|
+
// value both normalise to `https://<host>`.
|
|
1036
|
+
function _absoluteBase(canonicalUrl, shopName) {
|
|
1037
|
+
if (typeof canonicalUrl === "string" && canonicalUrl.length) {
|
|
1038
|
+
try {
|
|
1039
|
+
var parsed = new URL(canonicalUrl);
|
|
1040
|
+
return parsed.origin;
|
|
1041
|
+
} catch (_e) { /* fall through to the shop-name base */ }
|
|
1042
|
+
}
|
|
1043
|
+
return "https://" + String(shopName || "blamejs.shop").replace(/^https?:\/\//, "");
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Schema.org Organization + WebSite JSON-LD for the home page. Shared by
|
|
1047
|
+
// the container `renderHome` and mirrored by the edge
|
|
1048
|
+
// `worker/render/home.js` — keep the two byte-identical. The base URL is
|
|
1049
|
+
// derived from the shop name (operators set SHOP_NAME to their host); a
|
|
1050
|
+
// bare host and a full `https://` value both normalise to `https://<host>`.
|
|
1051
|
+
function _orgWebsiteJsonLd(shopName) {
|
|
1052
|
+
var base = "https://" + String(shopName).replace(/^https?:\/\//, "");
|
|
1053
|
+
return _jsonLdScript({
|
|
1054
|
+
"@context": "https://schema.org",
|
|
1055
|
+
"@type": "Organization",
|
|
1056
|
+
"name": shopName,
|
|
1057
|
+
"url": base,
|
|
1058
|
+
"logo": base + "/assets/brand/logo.png",
|
|
1059
|
+
"sameAs": ["https://github.com/blamejs/blamejs.shop"],
|
|
1060
|
+
}) +
|
|
1061
|
+
_jsonLdScript({
|
|
1062
|
+
"@context": "https://schema.org",
|
|
1063
|
+
"@type": "WebSite",
|
|
1064
|
+
"name": shopName,
|
|
1065
|
+
"url": base,
|
|
1066
|
+
"potentialAction": {
|
|
1067
|
+
"@type": "SearchAction",
|
|
1068
|
+
"target": base + "/search?q={search_term_string}",
|
|
1069
|
+
"query-input": "required name=search_term_string",
|
|
1070
|
+
},
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
|
|
979
1074
|
// ---- search results -----------------------------------------------------
|
|
980
1075
|
|
|
981
1076
|
var SEARCH_HEADER =
|
|
@@ -1218,12 +1313,20 @@ function renderSearch(opts) {
|
|
|
1218
1313
|
} else {
|
|
1219
1314
|
body = header + correctionHtml + chipsHtml + resultsInner;
|
|
1220
1315
|
}
|
|
1316
|
+
var searchShopName = opts.shop_name || "blamejs.shop";
|
|
1317
|
+
// Per-page meta description — mirrors the edge renderer.
|
|
1318
|
+
var searchMetaDescription = qTrim.length > 0
|
|
1319
|
+
? ("Results for “" + qTrim + "” in the " + searchShopName + " catalog.")
|
|
1320
|
+
: ("Search the " + searchShopName + " catalog by title, SKU, or description.");
|
|
1221
1321
|
return _wrap(Object.assign({
|
|
1222
1322
|
title: "Search",
|
|
1223
|
-
shop_name:
|
|
1323
|
+
shop_name: searchShopName,
|
|
1224
1324
|
cart_count: opts.cart_count,
|
|
1225
1325
|
search_q: opts.q,
|
|
1226
1326
|
theme_css: opts.theme_css,
|
|
1327
|
+
og_description: searchMetaDescription,
|
|
1328
|
+
canonical_url: opts.canonical_url,
|
|
1329
|
+
og_url: opts.og_url,
|
|
1227
1330
|
body: body,
|
|
1228
1331
|
}, _currencyWrapOpts(opts)));
|
|
1229
1332
|
}
|
|
@@ -1335,6 +1438,78 @@ function _buildBuyBox(variants, escAttr) {
|
|
|
1335
1438
|
" " + trustLine;
|
|
1336
1439
|
}
|
|
1337
1440
|
|
|
1441
|
+
// Resolve a product's availability + shipping shape from the variant
|
|
1442
|
+
// list + (optional) inventory map the route loads. `in_stock` is true if
|
|
1443
|
+
// ANY variant is buyable; a SKU with no inventory row counts as available
|
|
1444
|
+
// (the never-block-on-missing-inventory stance the cart-hold path already
|
|
1445
|
+
// takes). `requires_shipping` is true if ANY variant ships physically —
|
|
1446
|
+
// an all-digital product (`requires_shipping = 0` on every variant)
|
|
1447
|
+
// suppresses the "Ships in 1–2 business days" line. Returns a normalised
|
|
1448
|
+
// `{ in_stock, requires_shipping }` so the two render paths drive the
|
|
1449
|
+
// badge + JSON-LD from the same shape. Defensive request-shape reader:
|
|
1450
|
+
// missing/garbage inputs resolve to the available + physical default.
|
|
1451
|
+
function _resolveAvailability(variants, inventoryBySku) {
|
|
1452
|
+
variants = Array.isArray(variants) ? variants : [];
|
|
1453
|
+
var inv = (inventoryBySku && typeof inventoryBySku === "object") ? inventoryBySku : null;
|
|
1454
|
+
var anyTracked = false;
|
|
1455
|
+
var anyInStock = false;
|
|
1456
|
+
var requiresShipping = false;
|
|
1457
|
+
for (var i = 0; i < variants.length; i += 1) {
|
|
1458
|
+
var v = variants[i] || {};
|
|
1459
|
+
// requires_shipping defaults to physical (true) unless the column is
|
|
1460
|
+
// explicitly 0/false — matches the catalog default for tangible goods.
|
|
1461
|
+
if (v.requires_shipping === undefined || v.requires_shipping === null ||
|
|
1462
|
+
Number(v.requires_shipping) !== 0) {
|
|
1463
|
+
requiresShipping = true;
|
|
1464
|
+
}
|
|
1465
|
+
if (inv && Object.prototype.hasOwnProperty.call(inv, v.sku)) {
|
|
1466
|
+
anyTracked = true;
|
|
1467
|
+
var row = inv[v.sku];
|
|
1468
|
+
var available = row ? (Number(row.stock_on_hand) - Number(row.stock_held)) : 0;
|
|
1469
|
+
if (available > 0) anyInStock = true;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
return {
|
|
1473
|
+
// No tracked variant → the operator hasn't opted into stock tracking,
|
|
1474
|
+
// so the product reads as in stock (never-block stance).
|
|
1475
|
+
in_stock: anyTracked ? anyInStock : true,
|
|
1476
|
+
requires_shipping: variants.length === 0 ? true : requiresShipping,
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// PDP availability badges, driven by the resolved availability shape so
|
|
1481
|
+
// the displayed state matches the JSON-LD `availability`. Mirrored byte-
|
|
1482
|
+
// for-byte by worker/render/product.js#_buildAvailability.
|
|
1483
|
+
function _buildAvailability(availability) {
|
|
1484
|
+
var a = availability || { in_stock: true, requires_shipping: true };
|
|
1485
|
+
var stockBadge = a.in_stock
|
|
1486
|
+
? "<span class=\"pdp__badge pdp__badge--ok\"><span class=\"dot dot--live\" aria-hidden=\"true\"></span> In stock</span>"
|
|
1487
|
+
: "<span class=\"pdp__badge pdp__badge--out\">Out of stock</span>";
|
|
1488
|
+
// The "Ships in 1–2 business days" line only applies to a physical good;
|
|
1489
|
+
// an all-digital product suppresses it (nothing ships).
|
|
1490
|
+
var shipBadge = a.requires_shipping
|
|
1491
|
+
? "<span class=\"pdp__badge\">Ships in 1–2 business days</span>"
|
|
1492
|
+
: "<span class=\"pdp__badge\">Digital — delivered on purchase</span>";
|
|
1493
|
+
return "<div class=\"pdp__meta\">\n" +
|
|
1494
|
+
" " + stockBadge + "\n" +
|
|
1495
|
+
" " + shipBadge + "\n" +
|
|
1496
|
+
" <span class=\"pdp__badge\">Stripe-secured checkout</span>\n" +
|
|
1497
|
+
" </div>";
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// Short shipping/returns line under the buy box pointing shoppers at the
|
|
1501
|
+
// public policy page. Physical goods get a shipping-and-returns line; a
|
|
1502
|
+
// digital-only product drops the shipping half (no parcel to return).
|
|
1503
|
+
// Mirrored byte-for-byte by worker/render/product.js#_pdpShippingNote.
|
|
1504
|
+
function _pdpShippingNote(availability) {
|
|
1505
|
+
var a = availability || { in_stock: true, requires_shipping: true };
|
|
1506
|
+
var copy = a.requires_shipping
|
|
1507
|
+
? "Free returns within 30 days. "
|
|
1508
|
+
: "";
|
|
1509
|
+
return "<p class=\"pdp__shipping-note\">" + copy +
|
|
1510
|
+
"See our <a href=\"/terms\">shipping & returns policy</a>.</p>";
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1338
1513
|
var PRODUCT_PAGE =
|
|
1339
1514
|
"<section class=\"pdp\">\n" +
|
|
1340
1515
|
" <nav class=\"breadcrumb\" aria-label=\"Breadcrumb\">\n" +
|
|
@@ -1349,12 +1524,9 @@ var PRODUCT_PAGE =
|
|
|
1349
1524
|
" <p class=\"eyebrow\">Catalog product</p>\n" +
|
|
1350
1525
|
" <h1 class=\"pdp__title\">{{title}}</h1>\n" +
|
|
1351
1526
|
" <p class=\"pdp__description\">{{description}}</p>\n" +
|
|
1352
|
-
"
|
|
1353
|
-
" <span class=\"pdp__badge pdp__badge--ok\"><span class=\"dot dot--live\" aria-hidden=\"true\"></span> In stock</span>\n" +
|
|
1354
|
-
" <span class=\"pdp__badge\">Ships in 1–2 business days</span>\n" +
|
|
1355
|
-
" <span class=\"pdp__badge\">Stripe-secured checkout</span>\n" +
|
|
1356
|
-
" </div>\n" +
|
|
1527
|
+
" RAW_AVAILABILITY_PLACEHOLDER\n" +
|
|
1357
1528
|
" RAW_BUYBOX_PLACEHOLDER\n" +
|
|
1529
|
+
" RAW_SHIPPING_NOTE_PLACEHOLDER\n" +
|
|
1358
1530
|
" RAW_QTYBREAK_PLACEHOLDER\n" +
|
|
1359
1531
|
" RAW_WISHLIST_PLACEHOLDER\n" +
|
|
1360
1532
|
" RAW_COMPARE_PLACEHOLDER\n" +
|
|
@@ -2398,7 +2570,10 @@ function renderCategoryIndex(opts) {
|
|
|
2398
2570
|
"</section>";
|
|
2399
2571
|
return _wrap({
|
|
2400
2572
|
title: "Categories", shop_name: opts.shop_name || "blamejs.shop",
|
|
2401
|
-
cart_count: opts.cart_count == null ? 0 : opts.cart_count, theme_css: opts.theme_css,
|
|
2573
|
+
cart_count: opts.cart_count == null ? 0 : opts.cart_count, theme_css: opts.theme_css,
|
|
2574
|
+
og_description: "Browse every product category in the " + (opts.shop_name || "blamejs.shop") + " catalog.",
|
|
2575
|
+
canonical_url: opts.canonical_url, og_url: opts.og_url,
|
|
2576
|
+
body: body,
|
|
2402
2577
|
});
|
|
2403
2578
|
}
|
|
2404
2579
|
|
|
@@ -2463,9 +2638,17 @@ function renderCategory(opts) {
|
|
|
2463
2638
|
hero +
|
|
2464
2639
|
grid +
|
|
2465
2640
|
"</section>";
|
|
2641
|
+
// Per-page meta description: the category's own description when set,
|
|
2642
|
+
// otherwise a "Shop {category}…" pitch.
|
|
2643
|
+
var catMetaDescription = (cat.description && String(cat.description).trim().length)
|
|
2644
|
+
? String(cat.description)
|
|
2645
|
+
: ("Shop " + cat.title + " at " + (opts.shop_name || "blamejs.shop") + ".");
|
|
2466
2646
|
return _wrap({
|
|
2467
2647
|
title: cat.title, shop_name: opts.shop_name || "blamejs.shop",
|
|
2468
|
-
cart_count: opts.cart_count == null ? 0 : opts.cart_count, theme_css: opts.theme_css,
|
|
2648
|
+
cart_count: opts.cart_count == null ? 0 : opts.cart_count, theme_css: opts.theme_css,
|
|
2649
|
+
og_description: catMetaDescription,
|
|
2650
|
+
canonical_url: opts.canonical_url, og_url: opts.og_url,
|
|
2651
|
+
body: body,
|
|
2469
2652
|
});
|
|
2470
2653
|
}
|
|
2471
2654
|
|
|
@@ -3218,7 +3401,15 @@ function renderProduct(opts) {
|
|
|
3218
3401
|
asset_css_main: opts.theme.assetUrl("css/main.css"),
|
|
3219
3402
|
});
|
|
3220
3403
|
}
|
|
3404
|
+
// Truthful availability + shipping shape — drives the on-page badges
|
|
3405
|
+
// AND the JSON-LD `availability` from the same resolved values so the
|
|
3406
|
+
// two never disagree. `opts.inventory` is the per-SKU map the route
|
|
3407
|
+
// loads ({ sku: { stock_on_hand, stock_held } }); absent it, the
|
|
3408
|
+
// product reads as in stock (never-block-on-missing-inventory stance).
|
|
3409
|
+
var availability = _resolveAvailability(variants, opts.inventory);
|
|
3221
3410
|
var buyboxHtml = _buildBuyBox(rendered, b.template.escapeHtml);
|
|
3411
|
+
var availabilityHtml = _buildAvailability(availability);
|
|
3412
|
+
var shippingNoteHtml = _pdpShippingNote(availability);
|
|
3222
3413
|
var galleryHtml = _buildPdpGallery(opts.product, opts.media || [], opts.asset_prefix || "/assets/");
|
|
3223
3414
|
var reviewsHtml = _buildReviews(opts.review_summary, opts.reviews, opts.review_cta);
|
|
3224
3415
|
var qaHtml = _buildProductQa(opts.qa_questions, opts.qa_cta);
|
|
@@ -3231,7 +3422,9 @@ function renderProduct(opts) {
|
|
|
3231
3422
|
description: description,
|
|
3232
3423
|
})
|
|
3233
3424
|
.replace("RAW_GALLERY_PLACEHOLDER", galleryHtml)
|
|
3425
|
+
.replace("RAW_AVAILABILITY_PLACEHOLDER", availabilityHtml)
|
|
3234
3426
|
.replace("RAW_BUYBOX_PLACEHOLDER", buyboxHtml)
|
|
3427
|
+
.replace("RAW_SHIPPING_NOTE_PLACEHOLDER", shippingNoteHtml)
|
|
3235
3428
|
.replace("RAW_QTYBREAK_PLACEHOLDER", qtyBreaksHtml)
|
|
3236
3429
|
.replace("RAW_WISHLIST_PLACEHOLDER", wishlistHtml)
|
|
3237
3430
|
.replace("RAW_COMPARE_PLACEHOLDER", compareHtml)
|
|
@@ -3243,6 +3436,11 @@ function renderProduct(opts) {
|
|
|
3243
3436
|
// the default shop-level description + brand logo.
|
|
3244
3437
|
var heroMedia = (opts.media && opts.media[0]) || null;
|
|
3245
3438
|
var ogImage = heroMedia ? ((opts.asset_prefix || "/assets/") + heroMedia.r2_key) : "/assets/brand/logo.png";
|
|
3439
|
+
// Absolute base for the BreadcrumbList `item` URLs — derived from the
|
|
3440
|
+
// PDP's own canonical (origin stripped of the /products/slug path) so
|
|
3441
|
+
// the structured-data trail carries fully-qualified URLs. Falls back to
|
|
3442
|
+
// the shop-name host when the renderer is called without a request URL.
|
|
3443
|
+
var absoluteBase = _absoluteBase(opts.canonical_url, shopName);
|
|
3246
3444
|
|
|
3247
3445
|
// Product + AggregateOffer JSON-LD, with AggregateRating folded in
|
|
3248
3446
|
// when published reviews exist. Kept byte-compatible with the edge
|
|
@@ -3282,7 +3480,9 @@ function renderProduct(opts) {
|
|
|
3282
3480
|
"lowPrice": (lowMinor / divisor).toFixed(divisor === 1 ? 0 : 2),
|
|
3283
3481
|
"highPrice": (hiMinor / divisor).toFixed(divisor === 1 ? 0 : 2),
|
|
3284
3482
|
"offerCount": variants.length,
|
|
3285
|
-
"availability":
|
|
3483
|
+
"availability": availability.in_stock
|
|
3484
|
+
? "https://schema.org/InStock"
|
|
3485
|
+
: "https://schema.org/OutOfStock",
|
|
3286
3486
|
},
|
|
3287
3487
|
});
|
|
3288
3488
|
}
|
|
@@ -3290,8 +3490,8 @@ function renderProduct(opts) {
|
|
|
3290
3490
|
"@context": "https://schema.org",
|
|
3291
3491
|
"@type": "BreadcrumbList",
|
|
3292
3492
|
"itemListElement": [
|
|
3293
|
-
{ "@type": "ListItem", "position": 1, "name": "Shop", "item": "/" },
|
|
3294
|
-
{ "@type": "ListItem", "position": 2, "name": opts.product.title, "item": "/products/" + opts.product.slug },
|
|
3493
|
+
{ "@type": "ListItem", "position": 1, "name": "Shop", "item": absoluteBase + "/" },
|
|
3494
|
+
{ "@type": "ListItem", "position": 2, "name": opts.product.title, "item": absoluteBase + "/products/" + opts.product.slug },
|
|
3295
3495
|
],
|
|
3296
3496
|
});
|
|
3297
3497
|
jsonLd = (jsonLd || "") + breadcrumbJsonLd;
|
|
@@ -3305,6 +3505,8 @@ function renderProduct(opts) {
|
|
|
3305
3505
|
og_title: opts.product.title + " — " + shopName,
|
|
3306
3506
|
og_description: description || ("Browse " + opts.product.title + " on " + shopName + "."),
|
|
3307
3507
|
og_image: ogImage,
|
|
3508
|
+
canonical_url: opts.canonical_url,
|
|
3509
|
+
og_url: opts.og_url,
|
|
3308
3510
|
body: body + jsonLd,
|
|
3309
3511
|
}, _currencyWrapOpts(opts)));
|
|
3310
3512
|
}
|
|
@@ -5838,7 +6040,7 @@ function mount(router, deps) {
|
|
|
5838
6040
|
}));
|
|
5839
6041
|
}
|
|
5840
6042
|
var ccy = await _currencyForReq(req);
|
|
5841
|
-
var html = renderHome(Object.assign({ products: products, shop_name: shopName, theme: theme }, ccy));
|
|
6043
|
+
var html = renderHome(Object.assign({ products: products, shop_name: shopName, theme: theme }, _requestUrls(req), ccy));
|
|
5842
6044
|
_send(res, 200, html);
|
|
5843
6045
|
});
|
|
5844
6046
|
|
|
@@ -5969,7 +6171,7 @@ function mount(router, deps) {
|
|
|
5969
6171
|
corrected_query: correctedQ,
|
|
5970
6172
|
shop_name: shopName,
|
|
5971
6173
|
cart_count: cartCount,
|
|
5972
|
-
}, ccy)));
|
|
6174
|
+
}, _requestUrls(req), ccy)));
|
|
5973
6175
|
});
|
|
5974
6176
|
|
|
5975
6177
|
router.get("/products/:slug", async function (req, res) {
|
|
@@ -5979,9 +6181,22 @@ function mount(router, deps) {
|
|
|
5979
6181
|
if (!product) return _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme }));
|
|
5980
6182
|
var variants = await deps.catalog.variants.listForProduct(product.id);
|
|
5981
6183
|
var prices = {};
|
|
6184
|
+
// Per-SKU inventory map driving the truthful availability badge +
|
|
6185
|
+
// JSON-LD. Best-effort: a SKU with no inventory row (or a read
|
|
6186
|
+
// failure) is omitted from the map, which the renderer treats as
|
|
6187
|
+
// available — the never-block-on-missing-inventory stance the
|
|
6188
|
+
// cart-hold path already takes. Only populated when the operator has
|
|
6189
|
+
// wired the inventory primitive.
|
|
6190
|
+
var inventory = {};
|
|
5982
6191
|
for (var i = 0; i < variants.length; i += 1) {
|
|
5983
6192
|
var p = await deps.catalog.prices.current(variants[i].id, "USD");
|
|
5984
6193
|
if (p) prices[variants[i].id] = p;
|
|
6194
|
+
if (deps.catalog.inventory && typeof deps.catalog.inventory.get === "function") {
|
|
6195
|
+
try {
|
|
6196
|
+
var invRow = await deps.catalog.inventory.get(variants[i].sku);
|
|
6197
|
+
if (invRow) inventory[variants[i].sku] = invRow;
|
|
6198
|
+
} catch (_e) { /* drop-silent — missing inventory reads as available */ }
|
|
6199
|
+
}
|
|
5985
6200
|
}
|
|
5986
6201
|
// Media — first row drives the hero image, the next three feed
|
|
5987
6202
|
// the thumbnail strip. `listForProduct` is product-level only;
|
|
@@ -6068,6 +6283,7 @@ function mount(router, deps) {
|
|
|
6068
6283
|
variants: variants,
|
|
6069
6284
|
prices: prices,
|
|
6070
6285
|
media: media,
|
|
6286
|
+
inventory: inventory,
|
|
6071
6287
|
review_summary: reviewSummary,
|
|
6072
6288
|
reviews: reviewRows,
|
|
6073
6289
|
review_cta: reviewCta,
|
|
@@ -6079,7 +6295,7 @@ function mount(router, deps) {
|
|
|
6079
6295
|
shop_name: shopName,
|
|
6080
6296
|
cart_count: cartCount,
|
|
6081
6297
|
theme: theme,
|
|
6082
|
-
}, ccy));
|
|
6298
|
+
}, _requestUrls(req), ccy));
|
|
6083
6299
|
_send(res, 200, html);
|
|
6084
6300
|
});
|
|
6085
6301
|
|
|
@@ -6354,9 +6570,9 @@ function mount(router, deps) {
|
|
|
6354
6570
|
if (cats[i].active) active.push(cats[i]);
|
|
6355
6571
|
}
|
|
6356
6572
|
var cartCount = await _cartCountForReq(req);
|
|
6357
|
-
_send(res, 200, renderCategoryIndex({
|
|
6573
|
+
_send(res, 200, renderCategoryIndex(Object.assign({
|
|
6358
6574
|
categories: active, shop_name: shopName, cart_count: cartCount, asset_prefix: _cardAssetPrefix,
|
|
6359
|
-
}));
|
|
6575
|
+
}, _requestUrls(req))));
|
|
6360
6576
|
});
|
|
6361
6577
|
|
|
6362
6578
|
router.get("/categories/:slug", async function (req, res) {
|
|
@@ -6386,10 +6602,10 @@ function mount(router, deps) {
|
|
|
6386
6602
|
if (children[i].active) activeChildren.push(children[i]);
|
|
6387
6603
|
}
|
|
6388
6604
|
var cartCount = await _cartCountForReq(req);
|
|
6389
|
-
_send(res, 200, renderCategory({
|
|
6605
|
+
_send(res, 200, renderCategory(Object.assign({
|
|
6390
6606
|
category: cat, breadcrumbs: crumbs, children: activeChildren,
|
|
6391
6607
|
shop_name: shopName, cart_count: cartCount, asset_prefix: _cardAssetPrefix,
|
|
6392
|
-
}));
|
|
6608
|
+
}, _requestUrls(req))));
|
|
6393
6609
|
});
|
|
6394
6610
|
}
|
|
6395
6611
|
|
|
@@ -9103,8 +9319,12 @@ function mount(router, deps) {
|
|
|
9103
9319
|
var hostHeader = req.headers && (req.headers.host || req.headers.Host) || "";
|
|
9104
9320
|
var origin = hostHeader ? ("https://" + hostHeader) : "";
|
|
9105
9321
|
var urls = [];
|
|
9106
|
-
|
|
9107
|
-
|
|
9322
|
+
// The home page + the public legal pages. `/admin` is intentionally
|
|
9323
|
+
// omitted — robots.txt disallows it, so listing it in the sitemap
|
|
9324
|
+
// would contradict the crawl policy.
|
|
9325
|
+
urls.push({ loc: origin + "/", changefreq: "daily", priority: "1.0" });
|
|
9326
|
+
urls.push({ loc: origin + "/privacy", changefreq: "yearly", priority: "0.3" });
|
|
9327
|
+
urls.push({ loc: origin + "/terms", changefreq: "yearly", priority: "0.3" });
|
|
9108
9328
|
try {
|
|
9109
9329
|
var page = await deps.catalog.products.list({ status: "active", limit: 1000 });
|
|
9110
9330
|
for (var i = 0; i < page.rows.length; i += 1) {
|
|
@@ -9118,6 +9338,38 @@ function mount(router, deps) {
|
|
|
9118
9338
|
});
|
|
9119
9339
|
}
|
|
9120
9340
|
} catch (_e) { /* drop-silent — catalog unreachable means sitemap drops product rows */ }
|
|
9341
|
+
// Active category browse pages, when the categoryNavigation primitive
|
|
9342
|
+
// is wired. `tree({})` returns the full nested active tree (archived
|
|
9343
|
+
// rows already dropped); we flatten it to one URL per category. Best-
|
|
9344
|
+
// effort — a read failure drops the category rows rather than 500-ing
|
|
9345
|
+
// the sitemap.
|
|
9346
|
+
if (deps.categoryNavigation && typeof deps.categoryNavigation.tree === "function") {
|
|
9347
|
+
try {
|
|
9348
|
+
var catTree = await deps.categoryNavigation.tree({});
|
|
9349
|
+
var stack = Array.isArray(catTree) ? catTree.slice() : [];
|
|
9350
|
+
while (stack.length) {
|
|
9351
|
+
var node = stack.pop();
|
|
9352
|
+
if (node && node.active && node.slug) {
|
|
9353
|
+
urls.push({ loc: origin + "/categories/" + node.slug, changefreq: "weekly", priority: "0.6" });
|
|
9354
|
+
}
|
|
9355
|
+
if (node && Array.isArray(node.children)) {
|
|
9356
|
+
for (var cj = 0; cj < node.children.length; cj += 1) stack.push(node.children[cj]);
|
|
9357
|
+
}
|
|
9358
|
+
}
|
|
9359
|
+
} catch (_e) { /* drop-silent — categories unreachable drops those rows */ }
|
|
9360
|
+
}
|
|
9361
|
+
// Active collection browse pages, when the collections primitive is
|
|
9362
|
+
// wired. Same best-effort discipline.
|
|
9363
|
+
if (deps.collections && typeof deps.collections.list === "function") {
|
|
9364
|
+
try {
|
|
9365
|
+
var cols = await deps.collections.list({ active_only: true });
|
|
9366
|
+
for (var coi = 0; coi < cols.length; coi += 1) {
|
|
9367
|
+
if (cols[coi] && cols[coi].slug) {
|
|
9368
|
+
urls.push({ loc: origin + "/collections/" + cols[coi].slug, changefreq: "weekly", priority: "0.6" });
|
|
9369
|
+
}
|
|
9370
|
+
}
|
|
9371
|
+
} catch (_e) { /* drop-silent — collections unreachable drops those rows */ }
|
|
9372
|
+
}
|
|
9121
9373
|
var body = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
|
9122
9374
|
"<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n";
|
|
9123
9375
|
for (var u = 0; u < urls.length; u += 1) {
|
package/lib/translations.js
CHANGED
|
@@ -593,6 +593,7 @@ var CHROME_DEFAULTS = Object.freeze({
|
|
|
593
593
|
footer_shop_sale: "On sale",
|
|
594
594
|
footer_shop_compare: "Compare",
|
|
595
595
|
footer_shop_cart: "Cart",
|
|
596
|
+
footer_shop_shipping: "Shipping & returns",
|
|
596
597
|
|
|
597
598
|
footer_framework_heading: "Framework",
|
|
598
599
|
footer_framework_source: "Source on GitHub",
|
|
@@ -600,10 +601,9 @@ var CHROME_DEFAULTS = Object.freeze({
|
|
|
600
601
|
footer_framework_security: "Security policy",
|
|
601
602
|
footer_framework_changelog: "Changelog",
|
|
602
603
|
|
|
603
|
-
footer_operators_heading: "
|
|
604
|
+
footer_operators_heading: "Your account",
|
|
604
605
|
footer_operators_account: "Account",
|
|
605
606
|
footer_operators_orders: "Orders",
|
|
606
|
-
footer_operators_admin: "Admin",
|
|
607
607
|
footer_operators_contact: "Contact",
|
|
608
608
|
|
|
609
609
|
footer_copy_suffix: "built on blamejs · Apache 2.0 licensed.",
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
|
|
4
4
|
"packages": {
|
|
5
5
|
"blamejs": {
|
|
6
|
-
"version": "0.13.
|
|
7
|
-
"tag": "v0.13.
|
|
6
|
+
"version": "0.13.24",
|
|
7
|
+
"tag": "v0.13.24",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|