@decocms/blocks 7.25.0 → 7.26.1
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 +50 -0
- package/src/cms/schema.ts +82 -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,56 @@ 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("registers the sections so existing pages resolve, without polluting the picker", () => {
|
|
275
|
+
const meta = composeMeta(emptySiteMeta());
|
|
276
|
+
for (const key of [
|
|
277
|
+
"commerce/sections/Seo/SeoPLPV2.tsx",
|
|
278
|
+
"commerce/sections/Seo/SeoPDPV2.tsx",
|
|
279
|
+
]) {
|
|
280
|
+
// Registered as a manifest block + def → the SEO editor can resolve a
|
|
281
|
+
// schema for a page already on this type.
|
|
282
|
+
expect(meta.manifest.blocks.sections).toHaveProperty(key);
|
|
283
|
+
expect(meta.schema.definitions).toHaveProperty(b64(key));
|
|
284
|
+
// But deliberately NOT offered as a new pickable section everywhere
|
|
285
|
+
// (legacy commerce types have no component in start).
|
|
286
|
+
expect(meta.schema.root.sections.anyOf).not.toContainEqual(b64Ref(key));
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
241
291
|
describe("composeMeta idempotency", () => {
|
|
242
292
|
it("returns an already-composed meta unchanged (no double-compose)", () => {
|
|
243
293
|
const once = composeMeta(emptySiteMeta());
|
package/src/cms/schema.ts
CHANGED
|
@@ -833,6 +833,88 @@ function buildFrameworkSections(sectionAnyOf: any[]) {
|
|
|
833
833
|
};
|
|
834
834
|
extraAnyOf.push({ $ref: `#/definitions/${seoV2Key}` });
|
|
835
835
|
|
|
836
|
+
// --- commerce/sections/Seo/SeoPLPV2.tsx ---
|
|
837
|
+
// Product-listing SEO. Its schema is synthesized here (like the website Seo
|
|
838
|
+
// sections) because the commerce apps ship loaders/types, not sections — so
|
|
839
|
+
// there is no manifest to emit these props from. The Studio's SEO editor
|
|
840
|
+
// already knows this type as its "plp" mode (see seo-form-mode.ts); it just
|
|
841
|
+
// needs the props schema to render the override fields. `jsonLD` is the
|
|
842
|
+
// data-source block-ref set once when the page is created; it round-trips via
|
|
843
|
+
// the form (kept `hide`) rather than being re-picked in the SEO panel.
|
|
844
|
+
//
|
|
845
|
+
// NOT pushed to `extraAnyOf`: unlike the website Seo sections these are legacy
|
|
846
|
+
// deco-cx types with no component in start, so we only register the def +
|
|
847
|
+
// manifest block (enough for the SEO editor to resolve a schema for pages
|
|
848
|
+
// already on these types) without offering them as a new pickable section on
|
|
849
|
+
// every — including non-commerce — site.
|
|
850
|
+
const SEO_PLP_V2_TYPE = "commerce/sections/Seo/SeoPLPV2.tsx";
|
|
851
|
+
const seoPlpV2Key = toBase64(SEO_PLP_V2_TYPE);
|
|
852
|
+
definitions[seoPlpV2Key] = {
|
|
853
|
+
title: SEO_PLP_V2_TYPE,
|
|
854
|
+
type: "object",
|
|
855
|
+
required: ["__resolveType"],
|
|
856
|
+
properties: {
|
|
857
|
+
__resolveType: {
|
|
858
|
+
type: "string",
|
|
859
|
+
enum: [SEO_PLP_V2_TYPE],
|
|
860
|
+
default: SEO_PLP_V2_TYPE,
|
|
861
|
+
},
|
|
862
|
+
jsonLD: {
|
|
863
|
+
type: "object",
|
|
864
|
+
title: "Data Source",
|
|
865
|
+
additionalProperties: true,
|
|
866
|
+
hide: true,
|
|
867
|
+
},
|
|
868
|
+
title: { type: "string", title: "Title Override" },
|
|
869
|
+
description: { type: "string", title: "Description Override" },
|
|
870
|
+
noIndexing: { type: "boolean", title: "Disable indexing" },
|
|
871
|
+
configJsonLD: {
|
|
872
|
+
type: "object",
|
|
873
|
+
title: "Structured Data",
|
|
874
|
+
properties: {
|
|
875
|
+
removeVideos: { type: "boolean", title: "Remove videos" },
|
|
876
|
+
ignoreStructuredData: { type: "boolean", title: "Ignore Structured Data" },
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
};
|
|
881
|
+
manifestBlocks[SEO_PLP_V2_TYPE] = {
|
|
882
|
+
$ref: `#/definitions/${seoPlpV2Key}`,
|
|
883
|
+
namespace: "commerce",
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
// --- commerce/sections/Seo/SeoPDPV2.tsx ---
|
|
887
|
+
// Product-details SEO. Studio "pdp" mode counterpart of the PLP section above.
|
|
888
|
+
const SEO_PDP_V2_TYPE = "commerce/sections/Seo/SeoPDPV2.tsx";
|
|
889
|
+
const seoPdpV2Key = toBase64(SEO_PDP_V2_TYPE);
|
|
890
|
+
definitions[seoPdpV2Key] = {
|
|
891
|
+
title: SEO_PDP_V2_TYPE,
|
|
892
|
+
type: "object",
|
|
893
|
+
required: ["__resolveType"],
|
|
894
|
+
properties: {
|
|
895
|
+
__resolveType: {
|
|
896
|
+
type: "string",
|
|
897
|
+
enum: [SEO_PDP_V2_TYPE],
|
|
898
|
+
default: SEO_PDP_V2_TYPE,
|
|
899
|
+
},
|
|
900
|
+
jsonLD: {
|
|
901
|
+
type: "object",
|
|
902
|
+
title: "Data Source",
|
|
903
|
+
additionalProperties: true,
|
|
904
|
+
hide: true,
|
|
905
|
+
},
|
|
906
|
+
omitVariants: { type: "boolean", title: "Omit variants" },
|
|
907
|
+
title: { type: "string", title: "Title Override" },
|
|
908
|
+
description: { type: "string", title: "Description Override" },
|
|
909
|
+
noIndexing: { type: "boolean", title: "Disable indexing" },
|
|
910
|
+
ignoreStructuredData: { type: "boolean", title: "Ignore Structured Data" },
|
|
911
|
+
},
|
|
912
|
+
};
|
|
913
|
+
manifestBlocks[SEO_PDP_V2_TYPE] = {
|
|
914
|
+
$ref: `#/definitions/${seoPdpV2Key}`,
|
|
915
|
+
namespace: "commerce",
|
|
916
|
+
};
|
|
917
|
+
|
|
836
918
|
// --- website/flags/multivariate/section.ts ---
|
|
837
919
|
const MV_SECTION_TYPE = "website/flags/multivariate/section.ts";
|
|
838
920
|
const mvSectionKey = toBase64(MV_SECTION_TYPE);
|