@tokenoftrust/storefront-runner 2.4.75 → 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.
- package/apps/storefront/src/lib/seo/structuredData.ts +8 -1
- package/apps/storefront/src/pages/products/[handle].astro +1 -0
- package/package.json +1 -1
- package/packages/cli/src/declared-config.mjs +16 -1
- package/packages/public-runtime/src/declared-tenant-config.mjs +16 -1
- package/packages/public-runtime/src/tenant.ts +5 -0
|
@@ -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
|
}
|
|
@@ -306,6 +306,7 @@ const structuredData = {
|
|
|
306
306
|
image: ogPrimary && isUsableImageUrl(ogPrimary.url, ogPrimary.rehosted) ? ogPrimary.url : null,
|
|
307
307
|
product,
|
|
308
308
|
reviews: reviewLd(reviewAvg, reviewCount, reviews),
|
|
309
|
+
faq: details.faq?.items.map((it) => ({ q: it.question, a: it.answer })),
|
|
309
310
|
crumbs,
|
|
310
311
|
};
|
|
311
312
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "2.4.
|
|
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
|
|