@tokenoftrust/storefront-runner 2.4.76 → 2.4.77

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.
@@ -99,8 +99,15 @@ export function composeStructuredData(input: ComposeInput): Node | null {
99
99
  const ref = (id: string | undefined) => (id ? { "@id": id } : undefined);
100
100
  const pageUrl = (href: string) => pageUrlForm(absolute(href, canonicalBase), tenant.seo);
101
101
 
102
+ const pathOf = (value: string) => value.replace(/\/+$/, "") || "/";
103
+ const here = pathOf(new URL(canonical).pathname);
104
+ const scoped = (token: StructuredDataNodeToken) => {
105
+ const paths = profile.onlyOn?.[token];
106
+ return !paths || paths.some((p) => pathOf(p) === here);
107
+ };
108
+
102
109
  const nodes: Node[] = [];
103
- for (const token of tokens) {
110
+ for (const token of tokens.filter(scoped)) {
104
111
  const node = buildNode(token);
105
112
  if (node) nodes.push(node);
106
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.4.76",
3
+ "version": "2.4.77",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -34,7 +34,7 @@ export const SITE_TYPES = ["commerce", "marketing"];
34
34
  export const SEO_KEYS = Object.freeze(["trailingSlash", "structuredData"]);
35
35
 
36
36
  /** Every `seo.structuredData` key. */
37
- export const STRUCTURED_DATA_KEYS = Object.freeze(["graphs", "names", "reviews"]);
37
+ export const STRUCTURED_DATA_KEYS = Object.freeze(["graphs", "names", "reviews", "onlyOn"]);
38
38
 
39
39
  /**
40
40
  * The page types a structured-data profile declares a graph for. `productGroup` is a product
@@ -135,6 +135,21 @@ export function seoDeclarationProblems(seo) {
135
135
  if (data.reviews !== undefined && typeof data.reviews !== "boolean") {
136
136
  problems.push("`seo.structuredData.reviews` must be true or false");
137
137
  }
138
+ if (data.onlyOn !== undefined) {
139
+ if (!isPlainObject(data.onlyOn)) {
140
+ problems.push("`seo.structuredData.onlyOn` must map a node type to the paths it is emitted on");
141
+ } else {
142
+ for (const [token, paths] of Object.entries(/** @type {Record<string, unknown>} */ (data.onlyOn))) {
143
+ const at = `seo.structuredData.onlyOn.${token}`;
144
+ if (!STRUCTURED_DATA_NODE_TOKENS.includes(token)) {
145
+ problems.push(`\`${at}\`: ${JSON.stringify(token)} is not a node type the renderer builds`);
146
+ } else if (!Array.isArray(paths) || paths.length === 0
147
+ || !paths.every((p) => typeof p === "string" && p.startsWith("/") && !p.startsWith("//"))) {
148
+ problems.push(`\`${at}\` must be a non-empty array of root-absolute paths such as "/faq/"`);
149
+ }
150
+ }
151
+ }
152
+ }
138
153
  return problems;
139
154
  }
140
155
 
@@ -78,7 +78,7 @@ export const SITE_TYPES_LIST = Object.freeze(["commerce", "marketing"]);
78
78
  export const SEO_KEYS = Object.freeze(["trailingSlash", "structuredData"]);
79
79
 
80
80
  /** Every `seo.structuredData` key. */
81
- export const STRUCTURED_DATA_KEYS = Object.freeze(["graphs", "names", "reviews"]);
81
+ export const STRUCTURED_DATA_KEYS = Object.freeze(["graphs", "names", "reviews", "onlyOn"]);
82
82
 
83
83
  /**
84
84
  * The page types a structured-data profile declares a graph for. `productGroup` is a product
@@ -181,6 +181,21 @@ export function seoDeclarationProblems(seo) {
181
181
  if (data.reviews !== undefined && typeof data.reviews !== "boolean") {
182
182
  problems.push("`seo.structuredData.reviews` must be true or false");
183
183
  }
184
+ if (data.onlyOn !== undefined) {
185
+ if (!isPlainObject(data.onlyOn)) {
186
+ problems.push("`seo.structuredData.onlyOn` must map a node type to the paths it is emitted on");
187
+ } else {
188
+ for (const [token, paths] of Object.entries(/** @type {Record<string, unknown>} */ (data.onlyOn))) {
189
+ const at = `seo.structuredData.onlyOn.${token}`;
190
+ if (!STRUCTURED_DATA_NODE_TOKENS.includes(token)) {
191
+ problems.push(`\`${at}\`: ${JSON.stringify(token)} is not a node type the renderer builds`);
192
+ } else if (!Array.isArray(paths) || paths.length === 0
193
+ || !paths.every((p) => typeof p === "string" && p.startsWith("/") && !p.startsWith("//"))) {
194
+ problems.push(`\`${at}\` must be a non-empty array of root-absolute paths such as "/faq/"`);
195
+ }
196
+ }
197
+ }
198
+ }
184
199
  return problems;
185
200
  }
186
201
 
@@ -789,6 +789,11 @@ export interface SeoDeclaration {
789
789
  names?: Partial<Record<"Product" | "ProductGroup" | "Article" | "BlogPosting", "documentTitle" | "title">>;
790
790
  /** Emit real sourced aggregateRating/review on Product. Absent ⇒ false. */
791
791
  reviews?: boolean;
792
+ /**
793
+ * Node types emitted only on the listed paths (trailing slash ignored), wherever a graph
794
+ * lists them — the per-URL presence a source site had (an FAQPage on some products only).
795
+ */
796
+ onlyOn?: Partial<Record<StructuredDataNodeToken, string[]>>;
792
797
  };
793
798
  }
794
799