@decocms/blocks 7.26.0 → 7.26.2
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/package.json +1 -1
- package/src/cms/resolve.test.ts +100 -0
- package/src/cms/resolve.ts +108 -0
- package/src/cms/schema.test.ts +64 -0
- package/src/cms/schema.ts +89 -0
package/package.json
CHANGED
package/src/cms/resolve.test.ts
CHANGED
|
@@ -23,6 +23,7 @@ vi.mock("./registry", () => ({
|
|
|
23
23
|
import {
|
|
24
24
|
clearCommerceLoaders,
|
|
25
25
|
DEFAULT_FOLD_THRESHOLD,
|
|
26
|
+
extractSeoFromProps,
|
|
26
27
|
getAsyncRenderingConfig,
|
|
27
28
|
isEagerRequest,
|
|
28
29
|
registerCommerceLoader,
|
|
@@ -632,6 +633,105 @@ describe("resolvePageSeoBlock — bot-aware commerce SEO", () => {
|
|
|
632
633
|
// deferredSections array that loadDeferredSection would then try to resolve
|
|
633
634
|
// without the per-request commerce app context.
|
|
634
635
|
|
|
636
|
+
describe("extractSeoFromProps — commerce jsonLD structured data", () => {
|
|
637
|
+
const plp = (overrides: Record<string, unknown> = {}) => ({
|
|
638
|
+
"@type": "ProductListingPage",
|
|
639
|
+
breadcrumb: {
|
|
640
|
+
"@type": "BreadcrumbList",
|
|
641
|
+
itemListElement: [
|
|
642
|
+
{ "@type": "ListItem", position: 1, item: "https://x.com/a" },
|
|
643
|
+
{ "@type": "ListItem", position: 2, item: "https://x.com/a/b" },
|
|
644
|
+
],
|
|
645
|
+
},
|
|
646
|
+
products: [{ "@type": "Product", name: "P1" }],
|
|
647
|
+
seo: { title: "PLP Title", description: "PLP Desc" },
|
|
648
|
+
...overrides,
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it("derives title/description/canonical and emits the ItemList JSON-LD (PLP)", () => {
|
|
652
|
+
// The casaevideo.com.br/eletroportateis regression: a page.seo pointing at
|
|
653
|
+
// commerce/sections/Seo/SeoPLPV2.tsx resolved its jsonLD but emitted no
|
|
654
|
+
// schema.org. There is no section component in start to run this transform.
|
|
655
|
+
const seo = extractSeoFromProps({
|
|
656
|
+
__resolveType: "commerce/sections/Seo/SeoPLPV2.tsx",
|
|
657
|
+
jsonLD: plp(),
|
|
658
|
+
});
|
|
659
|
+
expect(seo.title).toBe("PLP Title");
|
|
660
|
+
expect(seo.description).toBe("PLP Desc");
|
|
661
|
+
// Highest-position breadcrumb item wins.
|
|
662
|
+
expect(seo.canonical).toBe("https://x.com/a/b");
|
|
663
|
+
expect(seo.noIndexing).toBe(false);
|
|
664
|
+
expect(seo.jsonLDs).toHaveLength(1);
|
|
665
|
+
expect(seo.jsonLDs?.[0]).toMatchObject({ "@type": "ProductListingPage" });
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it("emits the product image and JSON-LD for a PDP", () => {
|
|
669
|
+
const seo = extractSeoFromProps({
|
|
670
|
+
__resolveType: "commerce/sections/Seo/SeoPDPV2.tsx",
|
|
671
|
+
jsonLD: {
|
|
672
|
+
"@type": "ProductDetailsPage",
|
|
673
|
+
breadcrumbList: { "@type": "BreadcrumbList", itemListElement: [] },
|
|
674
|
+
product: {
|
|
675
|
+
"@type": "Product",
|
|
676
|
+
name: "Widget",
|
|
677
|
+
image: [{ url: "https://x.com/w.jpg" }],
|
|
678
|
+
},
|
|
679
|
+
seo: { title: "Widget", description: "A widget" },
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
expect(seo.title).toBe("Widget");
|
|
683
|
+
expect(seo.image).toBe("https://x.com/w.jpg");
|
|
684
|
+
expect(seo.jsonLDs?.[0]).toMatchObject({ "@type": "ProductDetailsPage" });
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it("lets manual override fields win over the jsonLD-derived ones", () => {
|
|
688
|
+
const seo = extractSeoFromProps({
|
|
689
|
+
jsonLD: plp(),
|
|
690
|
+
title: "Manual Title",
|
|
691
|
+
canonical: "https://x.com/manual",
|
|
692
|
+
});
|
|
693
|
+
expect(seo.title).toBe("Manual Title");
|
|
694
|
+
expect(seo.canonical).toBe("https://x.com/manual");
|
|
695
|
+
expect(seo.jsonLDs).toHaveLength(1);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it("omits structured data when ignoreStructuredData is set", () => {
|
|
699
|
+
const seo = extractSeoFromProps({
|
|
700
|
+
jsonLD: plp(),
|
|
701
|
+
configJsonLD: { ignoreStructuredData: true },
|
|
702
|
+
});
|
|
703
|
+
expect(seo.jsonLDs).toBeUndefined();
|
|
704
|
+
// Metadata is still derived — only the ItemList is suppressed.
|
|
705
|
+
expect(seo.title).toBe("PLP Title");
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it("marks an empty listing noIndexing and emits no ItemList", () => {
|
|
709
|
+
const seo = extractSeoFromProps({ jsonLD: plp({ products: [] }) });
|
|
710
|
+
expect(seo.noIndexing).toBe(true);
|
|
711
|
+
expect(seo.jsonLDs).toBeUndefined();
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it("does not touch a section that already emitted jsonLDs", () => {
|
|
715
|
+
const existing = [{ "@type": "WebSite" }];
|
|
716
|
+
const seo = extractSeoFromProps({ jsonLD: plp(), jsonLDs: existing });
|
|
717
|
+
expect(seo.jsonLDs).toBe(existing);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it("removeVideos clones rather than mutating the source jsonLD", () => {
|
|
721
|
+
const source = plp({
|
|
722
|
+
products: [{ "@type": "Product", name: "P1", video: [{ "@type": "VideoObject" }] }],
|
|
723
|
+
});
|
|
724
|
+
const seo = extractSeoFromProps({
|
|
725
|
+
jsonLD: source,
|
|
726
|
+
configJsonLD: { removeVideos: true },
|
|
727
|
+
});
|
|
728
|
+
expect(seo.jsonLDs?.[0].products[0].video).toBeUndefined();
|
|
729
|
+
// Source untouched — it may be shared with a body section.
|
|
730
|
+
// biome-ignore lint/suspicious/noExplicitAny: test fixture
|
|
731
|
+
expect((source.products[0] as any).video).toBeDefined();
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
|
|
635
735
|
describe("resolveDecoPage — #277 client-side navigation disables deferral", () => {
|
|
636
736
|
const lazySec = {
|
|
637
737
|
__resolveType: WELL_KNOWN_TYPES.LAZY,
|
package/src/cms/resolve.ts
CHANGED
|
@@ -1693,6 +1693,110 @@ export function isSeoSection(key: string): boolean {
|
|
|
1693
1693
|
return seoSectionKeys.has(key);
|
|
1694
1694
|
}
|
|
1695
1695
|
|
|
1696
|
+
/**
|
|
1697
|
+
* Canonical URL from a schema.org BreadcrumbList — the `item` of its
|
|
1698
|
+
* highest-position entry. Inlined from `@decocms/apps-commerce/utils/canonical`
|
|
1699
|
+
* because `blocks` cannot depend on `apps-commerce` (apps-commerce depends on
|
|
1700
|
+
* blocks, so the edge would be circular).
|
|
1701
|
+
*/
|
|
1702
|
+
function canonicalFromBreadcrumb(breadcrumb: unknown): string | undefined {
|
|
1703
|
+
const items = (
|
|
1704
|
+
breadcrumb as { itemListElement?: { position: number; item: string }[] } | undefined
|
|
1705
|
+
)?.itemListElement;
|
|
1706
|
+
if (!Array.isArray(items) || items.length === 0) return undefined;
|
|
1707
|
+
return items.reduce((acc, curr) => (acc.position < curr.position ? curr : acc)).item;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* Shallow-prune a commerce `jsonLD` before emitting it as structured data.
|
|
1712
|
+
* Clones only when a prune option is set (the common path emits as-is), so a
|
|
1713
|
+
* jsonLD shared with a body section is never mutated out from under it.
|
|
1714
|
+
*/
|
|
1715
|
+
function pruneCommerceJsonLD(
|
|
1716
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1717
|
+
jsonLD: Record<string, any>,
|
|
1718
|
+
opts: { removeVideos: boolean; omitVariants: boolean },
|
|
1719
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1720
|
+
): Record<string, any> {
|
|
1721
|
+
if (!opts.removeVideos && !opts.omitVariants) return jsonLD;
|
|
1722
|
+
const clone = structuredClone(jsonLD);
|
|
1723
|
+
if (opts.removeVideos && Array.isArray(clone.products)) {
|
|
1724
|
+
for (const product of clone.products) {
|
|
1725
|
+
product.video = undefined;
|
|
1726
|
+
for (const variant of product.isVariantOf?.hasVariant ?? []) variant.video = undefined;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
if (opts.omitVariants && clone.product?.isVariantOf?.hasVariant) {
|
|
1730
|
+
clone.product.isVariantOf.hasVariant = [];
|
|
1731
|
+
}
|
|
1732
|
+
return clone;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
/**
|
|
1736
|
+
* Derive SEO metadata from a commerce `jsonLD` data source (a resolved
|
|
1737
|
+
* ProductListingPage / ProductDetailsPage), mirroring the deco-cx
|
|
1738
|
+
* `commerce/sections/Seo/Seo{PLP,PDP}V2.tsx` section loaders.
|
|
1739
|
+
*
|
|
1740
|
+
* Those legacy sections have no component in @decocms/start (the commerce apps
|
|
1741
|
+
* ship loaders/types, not sections), so nothing else runs this transform.
|
|
1742
|
+
* Without it, PLP/PDP pages whose `page.seo` points at those types emit a
|
|
1743
|
+
* <title>/canonical (from top-level override props) but NO schema.org
|
|
1744
|
+
* structured data — the casaevideo.com.br/eletroportateis regression.
|
|
1745
|
+
*
|
|
1746
|
+
* Manual override fields already read into `seo` always win; this only fills
|
|
1747
|
+
* gaps and appends the JSON-LD. The caller guards on `seo.jsonLDs` so a real
|
|
1748
|
+
* site SEO section that already computed structured data is never touched.
|
|
1749
|
+
*/
|
|
1750
|
+
function deriveCommerceSeoFromJsonLD(seo: PageSeo, props: Record<string, unknown>): void {
|
|
1751
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1752
|
+
const jsonLD = props.jsonLD as Record<string, any> | undefined;
|
|
1753
|
+
if (!jsonLD) return;
|
|
1754
|
+
const type = jsonLD["@type"];
|
|
1755
|
+
if (type !== "ProductListingPage" && type !== "ProductDetailsPage") return;
|
|
1756
|
+
|
|
1757
|
+
const jsonLDSeo = jsonLD.seo as
|
|
1758
|
+
| { title?: string; description?: string; canonical?: string; noIndexing?: boolean }
|
|
1759
|
+
| null
|
|
1760
|
+
| undefined;
|
|
1761
|
+
|
|
1762
|
+
if (!seo.title && jsonLDSeo?.title) seo.title = jsonLDSeo.title;
|
|
1763
|
+
if (!seo.description && jsonLDSeo?.description) seo.description = jsonLDSeo.description;
|
|
1764
|
+
if (!seo.canonical) {
|
|
1765
|
+
seo.canonical =
|
|
1766
|
+
jsonLDSeo?.canonical ||
|
|
1767
|
+
canonicalFromBreadcrumb(
|
|
1768
|
+
type === "ProductListingPage" ? jsonLD.breadcrumb : jsonLD.breadcrumbList,
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
if (type === "ProductDetailsPage" && !seo.image) {
|
|
1772
|
+
seo.image = jsonLD.product?.image?.[0]?.url;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
const isEmpty =
|
|
1776
|
+
type === "ProductListingPage"
|
|
1777
|
+
? !Array.isArray(jsonLD.products) || jsonLD.products.length === 0
|
|
1778
|
+
: !jsonLD.product;
|
|
1779
|
+
if (seo.noIndexing === undefined) {
|
|
1780
|
+
seo.noIndexing = isEmpty || jsonLDSeo?.noIndexing || false;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// Structured data — opt out via `ignoreStructuredData` (top-level on PDP,
|
|
1784
|
+
// under `configJsonLD` on PLP). An empty page contributes no ItemList.
|
|
1785
|
+
const configJsonLD = props.configJsonLD as
|
|
1786
|
+
| { ignoreStructuredData?: boolean; removeVideos?: boolean }
|
|
1787
|
+
| undefined;
|
|
1788
|
+
const ignore =
|
|
1789
|
+
props.ignoreStructuredData === true || configJsonLD?.ignoreStructuredData === true;
|
|
1790
|
+
if (ignore || isEmpty) return;
|
|
1791
|
+
|
|
1792
|
+
seo.jsonLDs = [
|
|
1793
|
+
pruneCommerceJsonLD(jsonLD, {
|
|
1794
|
+
removeVideos: configJsonLD?.removeVideos === true,
|
|
1795
|
+
omitVariants: props.omitVariants === true,
|
|
1796
|
+
}),
|
|
1797
|
+
];
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1696
1800
|
/**
|
|
1697
1801
|
* Pick standard SEO fields from a props object.
|
|
1698
1802
|
* Works for both framework SEO types (SeoV2) and site SEO sections (SEOPDP).
|
|
@@ -1709,6 +1813,10 @@ export function extractSeoFromProps(props: Record<string, unknown>): PageSeo {
|
|
|
1709
1813
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1710
1814
|
seo.jsonLDs = props.jsonLDs as Record<string, any>[];
|
|
1711
1815
|
}
|
|
1816
|
+
// Legacy commerce PLP/PDP sections carry a `jsonLD` data source with no
|
|
1817
|
+
// component to turn it into structured data — derive it here (only when a
|
|
1818
|
+
// real SEO section hasn't already emitted `jsonLDs`).
|
|
1819
|
+
if (!seo.jsonLDs) deriveCommerceSeoFromJsonLD(seo, props);
|
|
1712
1820
|
return seo;
|
|
1713
1821
|
}
|
|
1714
1822
|
|
package/src/cms/schema.test.ts
CHANGED
|
@@ -238,6 +238,70 @@ describe("composeMeta framework option", () => {
|
|
|
238
238
|
});
|
|
239
239
|
});
|
|
240
240
|
|
|
241
|
+
describe("commerce SEO section schemas", () => {
|
|
242
|
+
const b64Ref = (key: string) => ({ $ref: `#/definitions/${b64(key)}` });
|
|
243
|
+
|
|
244
|
+
it("emits editable props for the product-listing SEO section (plp mode)", () => {
|
|
245
|
+
const def = definitionFor("commerce/sections/Seo/SeoPLPV2.tsx");
|
|
246
|
+
// Without a schema here, resolveSchema() returns null and the Studio SEO
|
|
247
|
+
// editor renders no fields — the "não tem props" bug.
|
|
248
|
+
expect(def).toBeDefined();
|
|
249
|
+
const props = def.properties;
|
|
250
|
+
expect(props).toHaveProperty("title");
|
|
251
|
+
expect(props).toHaveProperty("description");
|
|
252
|
+
expect(props).toHaveProperty("noIndexing");
|
|
253
|
+
// PLP-specific structured-data config (see seo-form-mode.ts PLP_SEO_FIELD_KEYS).
|
|
254
|
+
expect(props.configJsonLD.properties).toHaveProperty("removeVideos");
|
|
255
|
+
expect(props.configJsonLD.properties).toHaveProperty("ignoreStructuredData");
|
|
256
|
+
// The data source is set once at creation and stays hidden in the SEO panel;
|
|
257
|
+
// it round-trips via the form rather than being re-picked here.
|
|
258
|
+
expect(props.jsonLD.hide).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("emits editable props for the product-details SEO section (pdp mode)", () => {
|
|
262
|
+
const def = definitionFor("commerce/sections/Seo/SeoPDPV2.tsx");
|
|
263
|
+
expect(def).toBeDefined();
|
|
264
|
+
const props = def.properties;
|
|
265
|
+
expect(props).toHaveProperty("title");
|
|
266
|
+
expect(props).toHaveProperty("description");
|
|
267
|
+
expect(props).toHaveProperty("noIndexing");
|
|
268
|
+
// PDP-specific fields (see seo-form-mode.ts PDP_SEO_FIELD_KEYS).
|
|
269
|
+
expect(props).toHaveProperty("omitVariants");
|
|
270
|
+
expect(props).toHaveProperty("ignoreStructuredData");
|
|
271
|
+
expect(props.jsonLD.hide).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("always registers the defs + manifest blocks so existing pages resolve", () => {
|
|
275
|
+
const meta = composeMeta(emptySiteMeta());
|
|
276
|
+
for (const key of [
|
|
277
|
+
"commerce/sections/Seo/SeoPLPV2.tsx",
|
|
278
|
+
"commerce/sections/Seo/SeoPDPV2.tsx",
|
|
279
|
+
]) {
|
|
280
|
+
expect(meta.manifest.blocks.sections).toHaveProperty(key);
|
|
281
|
+
expect(meta.schema.definitions).toHaveProperty(b64(key));
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("offers them as page.seo options only on a commerce site", () => {
|
|
286
|
+
// "commerce site" = product-list loaders registered (the signal
|
|
287
|
+
// buildFrameworkSections gates the picker on). The key must match
|
|
288
|
+
// inferLoaderTags so it is tagged "product-list".
|
|
289
|
+
registerAppSchemas({
|
|
290
|
+
namespace: "vtex",
|
|
291
|
+
loaders: {
|
|
292
|
+
"vtex/loaders/test/ProductListForSeoPicker.ts": { type: "object", properties: {} },
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
const meta = composeMeta(emptySiteMeta());
|
|
296
|
+
for (const key of [
|
|
297
|
+
"commerce/sections/Seo/SeoPLPV2.tsx",
|
|
298
|
+
"commerce/sections/Seo/SeoPDPV2.tsx",
|
|
299
|
+
]) {
|
|
300
|
+
expect(meta.schema.root.sections.anyOf).toContainEqual(b64Ref(key));
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
241
305
|
describe("composeMeta idempotency", () => {
|
|
242
306
|
it("returns an already-composed meta unchanged (no double-compose)", () => {
|
|
243
307
|
const once = composeMeta(emptySiteMeta());
|
package/src/cms/schema.ts
CHANGED
|
@@ -736,6 +736,12 @@ function buildFrameworkSections(sectionAnyOf: any[]) {
|
|
|
736
736
|
const manifestBlocks: Record<string, any> = {};
|
|
737
737
|
const extraAnyOf: any[] = [];
|
|
738
738
|
|
|
739
|
+
// A site is "commerce" when it registered product-list loaders (same signal
|
|
740
|
+
// wrapResolvableProperties uses). Gates whether the commerce SEO section types
|
|
741
|
+
// below are offered as pickable page.seo options — a blog site should not see
|
|
742
|
+
// "Product listing SEO" in the SEO type dropdown.
|
|
743
|
+
const hasCommerce = getProductListLoaderKeys().length > 0;
|
|
744
|
+
|
|
739
745
|
// --- website/sections/Rendering/Lazy.tsx ---
|
|
740
746
|
const LAZY_TYPE = "website/sections/Rendering/Lazy.tsx";
|
|
741
747
|
const lazyKey = toBase64(LAZY_TYPE);
|
|
@@ -833,6 +839,89 @@ function buildFrameworkSections(sectionAnyOf: any[]) {
|
|
|
833
839
|
};
|
|
834
840
|
extraAnyOf.push({ $ref: `#/definitions/${seoV2Key}` });
|
|
835
841
|
|
|
842
|
+
// --- commerce/sections/Seo/SeoPLPV2.tsx ---
|
|
843
|
+
// Product-listing SEO. Its schema is synthesized here (like the website Seo
|
|
844
|
+
// sections) because the commerce apps ship loaders/types, not sections — so
|
|
845
|
+
// there is no manifest to emit these props from. The Studio's SEO editor
|
|
846
|
+
// already knows this type as its "plp" mode (see seo-form-mode.ts); it just
|
|
847
|
+
// needs the props schema to render the override fields. `jsonLD` is the
|
|
848
|
+
// data-source block-ref set once when the page is created; it round-trips via
|
|
849
|
+
// the form (kept `hide`) rather than being re-picked in the SEO panel.
|
|
850
|
+
//
|
|
851
|
+
// The def + manifest block are always registered so the SEO editor can resolve
|
|
852
|
+
// a schema for pages already on these types. They are added to `extraAnyOf`
|
|
853
|
+
// (offered as a selectable page.seo type) only on commerce sites, so a blog
|
|
854
|
+
// does not get "Product listing SEO" in its SEO type dropdown.
|
|
855
|
+
const SEO_PLP_V2_TYPE = "commerce/sections/Seo/SeoPLPV2.tsx";
|
|
856
|
+
const seoPlpV2Key = toBase64(SEO_PLP_V2_TYPE);
|
|
857
|
+
definitions[seoPlpV2Key] = {
|
|
858
|
+
title: SEO_PLP_V2_TYPE,
|
|
859
|
+
type: "object",
|
|
860
|
+
required: ["__resolveType"],
|
|
861
|
+
properties: {
|
|
862
|
+
__resolveType: {
|
|
863
|
+
type: "string",
|
|
864
|
+
enum: [SEO_PLP_V2_TYPE],
|
|
865
|
+
default: SEO_PLP_V2_TYPE,
|
|
866
|
+
},
|
|
867
|
+
jsonLD: {
|
|
868
|
+
type: "object",
|
|
869
|
+
title: "Data Source",
|
|
870
|
+
additionalProperties: true,
|
|
871
|
+
hide: true,
|
|
872
|
+
},
|
|
873
|
+
title: { type: "string", title: "Title Override" },
|
|
874
|
+
description: { type: "string", title: "Description Override" },
|
|
875
|
+
noIndexing: { type: "boolean", title: "Disable indexing" },
|
|
876
|
+
configJsonLD: {
|
|
877
|
+
type: "object",
|
|
878
|
+
title: "Structured Data",
|
|
879
|
+
properties: {
|
|
880
|
+
removeVideos: { type: "boolean", title: "Remove videos" },
|
|
881
|
+
ignoreStructuredData: { type: "boolean", title: "Ignore Structured Data" },
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
};
|
|
886
|
+
manifestBlocks[SEO_PLP_V2_TYPE] = {
|
|
887
|
+
$ref: `#/definitions/${seoPlpV2Key}`,
|
|
888
|
+
namespace: "commerce",
|
|
889
|
+
};
|
|
890
|
+
if (hasCommerce) extraAnyOf.push({ $ref: `#/definitions/${seoPlpV2Key}` });
|
|
891
|
+
|
|
892
|
+
// --- commerce/sections/Seo/SeoPDPV2.tsx ---
|
|
893
|
+
// Product-details SEO. Studio "pdp" mode counterpart of the PLP section above.
|
|
894
|
+
const SEO_PDP_V2_TYPE = "commerce/sections/Seo/SeoPDPV2.tsx";
|
|
895
|
+
const seoPdpV2Key = toBase64(SEO_PDP_V2_TYPE);
|
|
896
|
+
definitions[seoPdpV2Key] = {
|
|
897
|
+
title: SEO_PDP_V2_TYPE,
|
|
898
|
+
type: "object",
|
|
899
|
+
required: ["__resolveType"],
|
|
900
|
+
properties: {
|
|
901
|
+
__resolveType: {
|
|
902
|
+
type: "string",
|
|
903
|
+
enum: [SEO_PDP_V2_TYPE],
|
|
904
|
+
default: SEO_PDP_V2_TYPE,
|
|
905
|
+
},
|
|
906
|
+
jsonLD: {
|
|
907
|
+
type: "object",
|
|
908
|
+
title: "Data Source",
|
|
909
|
+
additionalProperties: true,
|
|
910
|
+
hide: true,
|
|
911
|
+
},
|
|
912
|
+
omitVariants: { type: "boolean", title: "Omit variants" },
|
|
913
|
+
title: { type: "string", title: "Title Override" },
|
|
914
|
+
description: { type: "string", title: "Description Override" },
|
|
915
|
+
noIndexing: { type: "boolean", title: "Disable indexing" },
|
|
916
|
+
ignoreStructuredData: { type: "boolean", title: "Ignore Structured Data" },
|
|
917
|
+
},
|
|
918
|
+
};
|
|
919
|
+
manifestBlocks[SEO_PDP_V2_TYPE] = {
|
|
920
|
+
$ref: `#/definitions/${seoPdpV2Key}`,
|
|
921
|
+
namespace: "commerce",
|
|
922
|
+
};
|
|
923
|
+
if (hasCommerce) extraAnyOf.push({ $ref: `#/definitions/${seoPdpV2Key}` });
|
|
924
|
+
|
|
836
925
|
// --- website/flags/multivariate/section.ts ---
|
|
837
926
|
const MV_SECTION_TYPE = "website/flags/multivariate/section.ts";
|
|
838
927
|
const mvSectionKey = toBase64(MV_SECTION_TYPE);
|