canopycms 0.3.3 → 0.5.0
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 +91 -41
- package/dist/ai/generate.d.ts +3 -0
- package/dist/ai/generate.js +23 -33
- package/dist/ai/handler.js +1 -0
- package/dist/ai/types.d.ts +6 -11
- package/dist/api/reference-options.d.ts +2 -5
- package/dist/api/reference-options.js +21 -2
- package/dist/api/schema.d.ts +4 -4
- package/dist/build/generate-ai-content.js +1 -0
- package/dist/cli/cli.js +176 -66
- package/dist/cli/generate-ai-content.js +122 -62
- package/dist/cli/init.js +29 -4
- package/dist/config/index.d.ts +1 -1
- package/dist/config/schemas/config.d.ts +74 -0
- package/dist/config/schemas/config.js +21 -0
- package/dist/config/schemas/field.d.ts +30 -0
- package/dist/config/schemas/field.js +22 -0
- package/dist/config/types.d.ts +47 -8
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +33 -0
- package/dist/content-store.d.ts +5 -4
- package/dist/content-store.js +12 -7
- package/dist/editor/FormRenderer.js +15 -13
- package/dist/editor/copy.d.ts +5 -0
- package/dist/editor/copy.js +5 -0
- package/dist/editor/fields/BlockField.d.ts +2 -0
- package/dist/editor/fields/BlockField.js +15 -8
- package/dist/editor/fields/ItemList.d.ts +21 -3
- package/dist/editor/fields/ItemList.js +16 -8
- package/dist/editor/fields/NumberListField.d.ts +2 -0
- package/dist/editor/fields/NumberListField.js +6 -2
- package/dist/editor/fields/ObjectListField.js +9 -3
- package/dist/editor/fields/ReferenceField.d.ts +6 -7
- package/dist/editor/fields/ReferenceField.js +28 -122
- package/dist/editor/fields/ReferenceListField.d.ts +41 -0
- package/dist/editor/fields/ReferenceListField.js +139 -0
- package/dist/editor/fields/SelectField.d.ts +2 -0
- package/dist/editor/fields/SelectField.js +6 -3
- package/dist/editor/fields/StringListField.d.ts +10 -1
- package/dist/editor/fields/StringListField.js +18 -8
- package/dist/editor/fields/item-expansion.d.ts +7 -3
- package/dist/editor/fields/item-expansion.js +6 -4
- package/dist/editor/fields/list-item-keys.d.ts +6 -5
- package/dist/editor/fields/list-item-keys.js +45 -13
- package/dist/editor/fields/reference-noindex.d.ts +37 -0
- package/dist/editor/fields/reference-noindex.js +48 -0
- package/dist/editor/fields/use-reference-options.d.ts +36 -0
- package/dist/editor/fields/use-reference-options.js +121 -0
- package/dist/editor/form-outline.d.ts +2 -2
- package/dist/editor/form-outline.js +4 -2
- package/dist/editor/theme.js +5 -0
- package/dist/entry-schema-registry.d.ts +1 -1
- package/dist/entry-schema-registry.js +3 -2
- package/dist/entry-schema.d.ts +21 -9
- package/dist/entry-schema.js +5 -2
- package/dist/reference-resolver.d.ts +18 -1
- package/dist/reference-resolver.js +175 -8
- package/dist/schema/schema-store.d.ts +2 -2
- package/dist/static/index.d.ts +8 -4
- package/dist/static/index.js +38 -13
- package/dist/static/seo.d.ts +7 -5
- package/dist/static/seo.js +13 -6
- package/dist/utils/body-field.d.ts +9 -1
- package/dist/utils/body-field.js +18 -1
- package/dist/validation/entry-validator.d.ts +5 -0
- package/dist/validation/entry-validator.js +12 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -599,7 +599,7 @@ var init_mdx_allowlist = __esm({
|
|
|
599
599
|
|
|
600
600
|
// dist/config/schemas/field.js
|
|
601
601
|
import { z as z2 } from "zod";
|
|
602
|
-
var fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema;
|
|
602
|
+
var fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceSortFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema;
|
|
603
603
|
var init_field = __esm({
|
|
604
604
|
"dist/config/schemas/field.js"() {
|
|
605
605
|
"use strict";
|
|
@@ -612,7 +612,10 @@ var init_field = __esm({
|
|
|
612
612
|
required: z2.boolean().optional(),
|
|
613
613
|
list: z2.boolean().optional(),
|
|
614
614
|
isTitle: z2.boolean().optional(),
|
|
615
|
-
isBody: z2.boolean().optional()
|
|
615
|
+
isBody: z2.boolean().optional(),
|
|
616
|
+
// Listed so a parse keeps it (zod strips unknown keys); which fields may carry it is
|
|
617
|
+
// `ensureMaxItemsPlacement`'s call (config/validation.ts).
|
|
618
|
+
maxItems: z2.number().int().positive().optional()
|
|
616
619
|
});
|
|
617
620
|
selectOptionSchema = z2.union([
|
|
618
621
|
z2.string(),
|
|
@@ -670,6 +673,7 @@ var init_field = __esm({
|
|
|
670
673
|
type: z2.literal("select"),
|
|
671
674
|
options: z2.array(selectOptionSchema).min(1)
|
|
672
675
|
});
|
|
676
|
+
referenceSortFieldSchema = z2.string().regex(/^[^.]+(\.[^.]+)*$/, 'must be a field name or a dotted path such as "meta.publishedAt"');
|
|
673
677
|
referenceFieldSchema = fieldBaseSchema.extend({
|
|
674
678
|
type: z2.literal("reference"),
|
|
675
679
|
collections: z2.array(z2.string().min(1)).min(1).optional(),
|
|
@@ -680,7 +684,14 @@ var init_field = __esm({
|
|
|
680
684
|
// default, so a consumer that adopts this schema's parse output would silently delete a
|
|
681
685
|
// runtime-consumed flag and the feature would no-op with no error. (`resolvedSchema` is
|
|
682
686
|
// absent on purpose — it is type-inference-only and stripping it changes no behavior.)
|
|
683
|
-
includeBody: z2.boolean().optional()
|
|
687
|
+
includeBody: z2.boolean().optional(),
|
|
688
|
+
optionSort: z2.union([
|
|
689
|
+
z2.enum(["label", "collection"]),
|
|
690
|
+
z2.object({
|
|
691
|
+
field: referenceSortFieldSchema,
|
|
692
|
+
direction: z2.enum(["asc", "desc"]).optional()
|
|
693
|
+
}).strict()
|
|
694
|
+
]).optional()
|
|
684
695
|
});
|
|
685
696
|
fieldHolder = [z2.never()];
|
|
686
697
|
blockSchema = z2.object({
|
|
@@ -828,7 +839,7 @@ var init_media = __esm({
|
|
|
828
839
|
|
|
829
840
|
// dist/config/schemas/config.js
|
|
830
841
|
import { z as z6 } from "zod";
|
|
831
|
-
var defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE;
|
|
842
|
+
var defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, seoFieldNameSchema, seoSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE;
|
|
832
843
|
var init_config = __esm({
|
|
833
844
|
"dist/config/schemas/config.js"() {
|
|
834
845
|
"use strict";
|
|
@@ -875,6 +886,19 @@ var init_config = __esm({
|
|
|
875
886
|
// Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
|
|
876
887
|
SignInComponent: z6.custom().optional()
|
|
877
888
|
});
|
|
889
|
+
seoFieldNameSchema = z6.string().min(1).optional();
|
|
890
|
+
seoSchema = z6.object({
|
|
891
|
+
group: z6.string().min(1).optional(),
|
|
892
|
+
fields: z6.object({
|
|
893
|
+
title: seoFieldNameSchema,
|
|
894
|
+
description: seoFieldNameSchema,
|
|
895
|
+
ogImage: seoFieldNameSchema,
|
|
896
|
+
ogType: seoFieldNameSchema,
|
|
897
|
+
canonical: seoFieldNameSchema,
|
|
898
|
+
noindex: seoFieldNameSchema,
|
|
899
|
+
twitterCard: seoFieldNameSchema
|
|
900
|
+
}).strict().optional()
|
|
901
|
+
}).strict();
|
|
878
902
|
CanopyConfigSchema = z6.object({
|
|
879
903
|
media: mediaSchema.optional(),
|
|
880
904
|
// No outer .optional(): both schemas already .default('deny'), so the field is optional on
|
|
@@ -924,6 +948,7 @@ var init_config = __esm({
|
|
|
924
948
|
entryLinkUrl: z6.custom().optional(),
|
|
925
949
|
validateEntry: z6.custom().optional(),
|
|
926
950
|
mdxAllow: mdxAllowlistSchema.optional(),
|
|
951
|
+
seo: seoSchema.optional(),
|
|
927
952
|
dev: devConfigSchema.optional()
|
|
928
953
|
}).strict();
|
|
929
954
|
DEFAULT_PROD_WORKSPACE = "/mnt/efs";
|
|
@@ -2912,13 +2937,36 @@ function flattenGroupFields(fields) {
|
|
|
2912
2937
|
return result;
|
|
2913
2938
|
}
|
|
2914
2939
|
|
|
2940
|
+
// dist/utils/format.js
|
|
2941
|
+
var getFormatExtension = (format) => {
|
|
2942
|
+
if (format === "md")
|
|
2943
|
+
return ".md";
|
|
2944
|
+
if (format === "mdx")
|
|
2945
|
+
return ".mdx";
|
|
2946
|
+
if (format === "yaml")
|
|
2947
|
+
return ".yaml";
|
|
2948
|
+
return ".json";
|
|
2949
|
+
};
|
|
2950
|
+
var isDataOnlyFormat = (format) => format === "json" || format === "yaml";
|
|
2951
|
+
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2952
|
+
|
|
2915
2953
|
// dist/utils/body-field.js
|
|
2916
2954
|
function findBodyFieldName(fields) {
|
|
2955
|
+
return findExplicitBodyFieldName(fields) ?? "body";
|
|
2956
|
+
}
|
|
2957
|
+
function findExplicitBodyFieldName(fields) {
|
|
2917
2958
|
for (const field of flattenGroupFields(fields)) {
|
|
2918
2959
|
if ("isBody" in field && field.isBody)
|
|
2919
2960
|
return field.name;
|
|
2920
2961
|
}
|
|
2921
|
-
return
|
|
2962
|
+
return void 0;
|
|
2963
|
+
}
|
|
2964
|
+
function linkOnlyReferenceData(data, format, fields) {
|
|
2965
|
+
const bodyFieldName = isDataOnlyFormat(format) && fields ? findExplicitBodyFieldName(fields) : void 0;
|
|
2966
|
+
if (bodyFieldName === void 0 || !(bodyFieldName in data))
|
|
2967
|
+
return data;
|
|
2968
|
+
const { [bodyFieldName]: _body, ...rest } = data;
|
|
2969
|
+
return rest;
|
|
2922
2970
|
}
|
|
2923
2971
|
|
|
2924
2972
|
// dist/content-store.js
|
|
@@ -2934,7 +2982,7 @@ var DEFAULT_SEO_FIELD_NAMES = {
|
|
|
2934
2982
|
noindex: "noindex",
|
|
2935
2983
|
twitterCard: "twitterCard"
|
|
2936
2984
|
};
|
|
2937
|
-
var
|
|
2985
|
+
var asRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
2938
2986
|
var asText = (value) => {
|
|
2939
2987
|
if (typeof value !== "string")
|
|
2940
2988
|
return void 0;
|
|
@@ -2946,7 +2994,7 @@ var asImageUrl = (value) => {
|
|
|
2946
2994
|
const text = asText(value);
|
|
2947
2995
|
if (text)
|
|
2948
2996
|
return text;
|
|
2949
|
-
return asText(
|
|
2997
|
+
return asText(asRecord2(value)?.src);
|
|
2950
2998
|
};
|
|
2951
2999
|
var asOgType = (value) => {
|
|
2952
3000
|
const text = asText(value);
|
|
@@ -2957,9 +3005,13 @@ var asTwitterCard = (value) => {
|
|
|
2957
3005
|
return text === "summary" || text === "summary_large_image" ? text : void 0;
|
|
2958
3006
|
};
|
|
2959
3007
|
function extractSeoFields(entryData, opts = {}) {
|
|
2960
|
-
const names = { ...DEFAULT_SEO_FIELD_NAMES
|
|
2961
|
-
const
|
|
2962
|
-
|
|
3008
|
+
const names = { ...DEFAULT_SEO_FIELD_NAMES };
|
|
3009
|
+
for (const [key, name] of Object.entries(opts.fields ?? {})) {
|
|
3010
|
+
if (typeof name === "string" && name !== "")
|
|
3011
|
+
names[key] = name;
|
|
3012
|
+
}
|
|
3013
|
+
const entry = asRecord2(entryData);
|
|
3014
|
+
const source = opts.group ? asRecord2(entry?.[opts.group]) : entry;
|
|
2963
3015
|
return {
|
|
2964
3016
|
title: asText(source?.[names.title]) ?? asText(opts.fallbackTitle),
|
|
2965
3017
|
description: asText(source?.[names.description]) ?? asText(opts.fallbackDescription),
|
|
@@ -4459,19 +4511,6 @@ async function readFileWithMtimeIfExists(filePath) {
|
|
|
4459
4511
|
}
|
|
4460
4512
|
}
|
|
4461
4513
|
|
|
4462
|
-
// dist/utils/format.js
|
|
4463
|
-
var getFormatExtension = (format) => {
|
|
4464
|
-
if (format === "md")
|
|
4465
|
-
return ".md";
|
|
4466
|
-
if (format === "mdx")
|
|
4467
|
-
return ".mdx";
|
|
4468
|
-
if (format === "yaml")
|
|
4469
|
-
return ".yaml";
|
|
4470
|
-
return ".json";
|
|
4471
|
-
};
|
|
4472
|
-
var isDataOnlyFormat = (format) => format === "json" || format === "yaml";
|
|
4473
|
-
var asRecord2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
4474
|
-
|
|
4475
4514
|
// dist/paths/index.js
|
|
4476
4515
|
init_normalize();
|
|
4477
4516
|
|
|
@@ -5085,7 +5124,7 @@ var ContentStore = class {
|
|
|
5085
5124
|
fields = entryTypeConfig?.schema || [];
|
|
5086
5125
|
}
|
|
5087
5126
|
if (format === "json") {
|
|
5088
|
-
const data =
|
|
5127
|
+
const data = asRecord(JSON.parse(raw));
|
|
5089
5128
|
doc = {
|
|
5090
5129
|
collection: schemaItem.logicalPath,
|
|
5091
5130
|
collectionName: schemaItem.name,
|
|
@@ -5095,7 +5134,7 @@ var ContentStore = class {
|
|
|
5095
5134
|
absolutePath
|
|
5096
5135
|
};
|
|
5097
5136
|
} else if (format === "yaml") {
|
|
5098
|
-
const data =
|
|
5137
|
+
const data = asRecord(yamlParse(raw));
|
|
5099
5138
|
doc = {
|
|
5100
5139
|
collection: schemaItem.logicalPath,
|
|
5101
5140
|
collectionName: schemaItem.name,
|
|
@@ -5698,10 +5737,11 @@ var ContentStore = class {
|
|
|
5698
5737
|
* Measured (2000 occurrences of one target, local disk, the friendliest case for NOT
|
|
5699
5738
|
* caching): cloning a snippet-sized target is ~63x cheaper than re-reading and parsing it, a
|
|
5700
5739
|
* 265KB JSON target ~0.8x -- a wash at the pathological end, never a blow-up, and on the
|
|
5701
|
-
* EFS/NFS this targets the syscall the memo removes dominates both.
|
|
5702
|
-
*
|
|
5703
|
-
*
|
|
5704
|
-
* cloned once per referencing
|
|
5740
|
+
* EFS/NFS this targets the syscall the memo removes dominates both. A target resolves
|
|
5741
|
+
* without its body (an md/mdx target to its frontmatter, a json/yaml one without its `isBody`
|
|
5742
|
+
* field) unless the field sets `includeBody`, which is why `includeBody` is opt-in per
|
|
5743
|
+
* field: an embedded body sits inside the memoized object and is cloned once per referencing
|
|
5744
|
+
* entry.
|
|
5705
5745
|
*/
|
|
5706
5746
|
resolveSingleReference(id, idIndex, includeBody, cache, access) {
|
|
5707
5747
|
if (!cache)
|
|
@@ -5796,7 +5836,8 @@ var ContentStore = class {
|
|
|
5796
5836
|
}));
|
|
5797
5837
|
}
|
|
5798
5838
|
const bodyForEmbed = includeBody && "body" in doc ? { fieldName: doc.bodyFieldName, value: doc.body } : void 0;
|
|
5799
|
-
|
|
5839
|
+
const data = includeBody ? doc.data : linkOnlyReferenceData(doc.data, doc.format, this.entryTypeOf(location.collection, location.relativePath)?.schema);
|
|
5840
|
+
return buildResolvedReference(data, meta, bodyForEmbed);
|
|
5800
5841
|
} catch (error) {
|
|
5801
5842
|
if (isNodeError(error) && error.code === "ENOENT")
|
|
5802
5843
|
return STALE_LOOKUP;
|
|
@@ -6711,7 +6752,7 @@ function createReferenceRendering(idIndex, flatSchema, entryLinkUrl, markdownCop
|
|
|
6711
6752
|
|
|
6712
6753
|
// dist/ai/generate.js
|
|
6713
6754
|
async function generateAIContent(options) {
|
|
6714
|
-
const { store, flatSchema, contentRoot, config, entryLinkUrl, generatedAt, buildId } = options;
|
|
6755
|
+
const { store, flatSchema, contentRoot, config, entryLinkUrl, seo, generatedAt, buildId } = options;
|
|
6715
6756
|
assertValidNoindexConfig(config);
|
|
6716
6757
|
assertValidReferenceLink(config);
|
|
6717
6758
|
const files = /* @__PURE__ */ new Map();
|
|
@@ -6722,6 +6763,7 @@ async function generateAIContent(options) {
|
|
|
6722
6763
|
config,
|
|
6723
6764
|
idIndex,
|
|
6724
6765
|
entryLinkUrl,
|
|
6766
|
+
seo,
|
|
6725
6767
|
resolveTarget: createReferenceTargetResolver(store)
|
|
6726
6768
|
};
|
|
6727
6769
|
const collections = flatSchema.filter((item) => item.type === "collection");
|
|
@@ -6752,14 +6794,18 @@ async function generateAIContent(options) {
|
|
|
6752
6794
|
const pending = pendingById.get(id);
|
|
6753
6795
|
if (!pending || !exported.has(id))
|
|
6754
6796
|
return void 0;
|
|
6755
|
-
const {
|
|
6756
|
-
const
|
|
6797
|
+
const { format } = pending;
|
|
6798
|
+
const { slug, fields } = pending.entry;
|
|
6799
|
+
const storedData = structuredClone(pending.storedData);
|
|
6800
|
+
const target = buildResolvedReference(linkOnlyReferenceData(storedData, format, fields), {
|
|
6757
6801
|
id,
|
|
6758
6802
|
slug,
|
|
6759
6803
|
collection: pending.collectionPath,
|
|
6760
6804
|
urlPath: computeEntryUrl(pending.collectionPath, slug, contentRoot)
|
|
6761
6805
|
});
|
|
6762
|
-
|
|
6806
|
+
const dataBodyField = isDataOnlyFormat(format) ? findExplicitBodyFieldName(fields) : void 0;
|
|
6807
|
+
const dataBody = dataBodyField === void 0 ? void 0 : storedData[dataBodyField];
|
|
6808
|
+
return { target, body: pending.body ?? (typeof dataBody === "string" ? dataBody : void 0) };
|
|
6763
6809
|
};
|
|
6764
6810
|
const linkFor = config?.referenceLink ? await decideReferenceLinks(config.referenceLink, allPending, subjectOf, entryLinkUrl) : void 0;
|
|
6765
6811
|
const bodyLinkUrl = linkFor ? (location) => {
|
|
@@ -6869,7 +6915,7 @@ async function collectCollection(read, collection, flatSchema) {
|
|
|
6869
6915
|
return { collection, cleanPath, entries, subcollections };
|
|
6870
6916
|
}
|
|
6871
6917
|
async function collectEntries(read, collection, cleanPath) {
|
|
6872
|
-
const { store, contentRoot, config, idIndex, entryLinkUrl, resolveTarget } = read;
|
|
6918
|
+
const { store, contentRoot, config, idIndex, entryLinkUrl, seo, resolveTarget } = read;
|
|
6873
6919
|
const skipNoindex = config?.exclude?.noindex !== false;
|
|
6874
6920
|
const pending = [];
|
|
6875
6921
|
const listed = await store.getCollectionEntryPaths(collection.logicalPath);
|
|
@@ -6887,7 +6933,7 @@ async function collectEntries(read, collection, cleanPath) {
|
|
|
6887
6933
|
const doc = await store.read(listEntry.collection, listEntry.slug, {
|
|
6888
6934
|
resolveReferences: false
|
|
6889
6935
|
});
|
|
6890
|
-
if (skipNoindex && isNoindexEntry(doc.data,
|
|
6936
|
+
if (skipNoindex && isNoindexEntry(doc.data, seo))
|
|
6891
6937
|
continue;
|
|
6892
6938
|
const storedData = doc.data;
|
|
6893
6939
|
doc.data = await resolveReferenceFields(doc.data, entryTypeConfig.schema, resolveTarget);
|
|
@@ -6901,6 +6947,7 @@ async function collectEntries(read, collection, cleanPath) {
|
|
|
6901
6947
|
entry: aiEntry,
|
|
6902
6948
|
body,
|
|
6903
6949
|
storedData,
|
|
6950
|
+
format: doc.format,
|
|
6904
6951
|
linkedIds: [],
|
|
6905
6952
|
contentId: extractIdFromFilename(path13.basename(listEntry.relativePath)),
|
|
6906
6953
|
absolutePath: doc.absolutePath,
|
|
@@ -6953,24 +7000,8 @@ function assertValidNoindexConfig(config) {
|
|
|
6953
7000
|
if (noindex !== void 0 && typeof noindex !== "boolean") {
|
|
6954
7001
|
throw new Error(`AI content config: exclude.noindex must be a boolean, got ${typeof noindex}`);
|
|
6955
7002
|
}
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
return;
|
|
6959
|
-
if (typeof seo !== "object" || seo === null || Array.isArray(seo)) {
|
|
6960
|
-
throw new Error("AI content config: seo must be an object of { group?, fields? }");
|
|
6961
|
-
}
|
|
6962
|
-
const { group, fields } = seo;
|
|
6963
|
-
if (group !== void 0 && typeof group !== "string") {
|
|
6964
|
-
throw new Error("AI content config: seo.group must be a string");
|
|
6965
|
-
}
|
|
6966
|
-
if (fields === void 0)
|
|
6967
|
-
return;
|
|
6968
|
-
if (typeof fields !== "object" || fields === null || Array.isArray(fields) || Object.values(fields).some((name) => typeof name !== "string")) {
|
|
6969
|
-
throw new Error("AI content config: seo.fields must map SEO fields to field-name strings");
|
|
6970
|
-
}
|
|
6971
|
-
const unknown = Object.keys(fields).filter((key) => !(key in DEFAULT_SEO_FIELD_NAMES));
|
|
6972
|
-
if (unknown.length > 0) {
|
|
6973
|
-
throw new Error(`AI content config: unknown seo.fields key(s): ${unknown.join(", ")}`);
|
|
7003
|
+
if (config && "seo" in config) {
|
|
7004
|
+
throw new Error("AI content config: seo is not an AI content option. Set it once as seo in defineCanopyConfig, where the sitemap, page metadata and AI content all read it.");
|
|
6974
7005
|
}
|
|
6975
7006
|
}
|
|
6976
7007
|
function stripContentRoot(logicalPath, contentRoot) {
|
|
@@ -9794,10 +9825,10 @@ var readEntryData = async (filePath, format, bodyFieldName = "body") => {
|
|
|
9794
9825
|
try {
|
|
9795
9826
|
const raw = await fs20.readFile(filePath, "utf8");
|
|
9796
9827
|
if (format === "json") {
|
|
9797
|
-
return
|
|
9828
|
+
return asRecord(JSON.parse(raw));
|
|
9798
9829
|
}
|
|
9799
9830
|
if (format === "yaml") {
|
|
9800
|
-
return
|
|
9831
|
+
return asRecord(yamlParse2(raw));
|
|
9801
9832
|
}
|
|
9802
9833
|
const parsed = matter3(raw);
|
|
9803
9834
|
const data = { ...parsed.data ?? {} };
|
|
@@ -10121,6 +10152,14 @@ function validateEntryData(fields, data, pathPrefix = "", options = {}) {
|
|
|
10121
10152
|
}
|
|
10122
10153
|
if (value === void 0 || value === null)
|
|
10123
10154
|
continue;
|
|
10155
|
+
if (Array.isArray(value) && "maxItems" in field && typeof field.maxItems === "number" && value.length > field.maxItems) {
|
|
10156
|
+
const cap = field.maxItems;
|
|
10157
|
+
errors.push({
|
|
10158
|
+
fieldPath: path27,
|
|
10159
|
+
message: `At most ${cap} ${cap === 1 ? "item" : "items"}; this list has ${value.length}`,
|
|
10160
|
+
maxItems: { count: value.length, cap }
|
|
10161
|
+
});
|
|
10162
|
+
}
|
|
10124
10163
|
if (field.type === "block") {
|
|
10125
10164
|
const blockField = field;
|
|
10126
10165
|
if (!Array.isArray(value)) {
|
|
@@ -10221,14 +10260,34 @@ function assertBuildEntriesValid(items, phaseLabel) {
|
|
|
10221
10260
|
const invalid = findInvalidEntries(items);
|
|
10222
10261
|
if (invalid.length === 0)
|
|
10223
10262
|
return;
|
|
10224
|
-
const lines =
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10263
|
+
const lines = [];
|
|
10264
|
+
let hasOverCap = false;
|
|
10265
|
+
let hasOrdinary = false;
|
|
10266
|
+
for (const { entryPath, errors } of invalid) {
|
|
10267
|
+
const ordinary = errors.filter((e) => e.maxItems === void 0);
|
|
10268
|
+
if (ordinary.length > 0) {
|
|
10269
|
+
hasOrdinary = true;
|
|
10270
|
+
const [first, ...rest] = ordinary;
|
|
10271
|
+
const summary = rest.length > 0 ? `${first.fieldPath}: ${first.message} (+${rest.length} more)` : `${first.fieldPath}: ${first.message}`;
|
|
10272
|
+
lines.push(` - ${entryPath} \u2014 ${summary}`);
|
|
10273
|
+
}
|
|
10274
|
+
for (const { fieldPath, maxItems } of errors) {
|
|
10275
|
+
if (maxItems === void 0)
|
|
10276
|
+
continue;
|
|
10277
|
+
hasOverCap = true;
|
|
10278
|
+
lines.push(` - ${entryPath} \u2014 ${fieldPath}: ${maxItems.count} items, but maxItems is ${maxItems.cap}`);
|
|
10279
|
+
}
|
|
10280
|
+
}
|
|
10281
|
+
const advice = [];
|
|
10282
|
+
if (hasOverCap) {
|
|
10283
|
+
advice.push(`A list over its maxItems: open the entry in the editor and remove a pick from that list (or remove items) until it holds at most maxItems, then rebuild. This usually happens when two branches each add one item to the same list and are merged one after the other.`);
|
|
10284
|
+
}
|
|
10285
|
+
if (hasOrdinary) {
|
|
10286
|
+
advice.push(`${hasOverCap ? "The other errors are" : "These are"} likely abandoned create-scaffolds (empty entries left behind when a create was started but never finished). Finish editing the entry (fill in its required fields) or delete the abandoned draft, then rebuild.`);
|
|
10287
|
+
}
|
|
10229
10288
|
throw new Error(`CanopyCMS static build: found ${invalid.length} schema-invalid ${invalid.length === 1 ? "entry" : "entries"} during ${phaseLabel}:
|
|
10230
10289
|
${lines.join("\n")}
|
|
10231
|
-
|
|
10290
|
+
` + advice.join("\n"));
|
|
10232
10291
|
}
|
|
10233
10292
|
|
|
10234
10293
|
// dist/build/generate-ai-content.js
|
|
@@ -10325,6 +10384,7 @@ ${cached.issues.map((issue) => ` - ${issue.message}`).join("\n")}`);
|
|
|
10325
10384
|
contentRoot: contentRootName,
|
|
10326
10385
|
config: aiConfig,
|
|
10327
10386
|
entryLinkUrl: config.entryLinkUrl,
|
|
10387
|
+
seo: config.seo,
|
|
10328
10388
|
...resolveBuildStamp()
|
|
10329
10389
|
});
|
|
10330
10390
|
const absoluteOutputDir = path25.resolve(outputDir) + path25.sep;
|
package/dist/cli/init.js
CHANGED
|
@@ -160,7 +160,7 @@ var init_mdx_allowlist = __esm({
|
|
|
160
160
|
|
|
161
161
|
// dist/config/schemas/field.js
|
|
162
162
|
import { z } from "zod";
|
|
163
|
-
var fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema;
|
|
163
|
+
var fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceSortFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema;
|
|
164
164
|
var init_field = __esm({
|
|
165
165
|
"dist/config/schemas/field.js"() {
|
|
166
166
|
"use strict";
|
|
@@ -173,7 +173,10 @@ var init_field = __esm({
|
|
|
173
173
|
required: z.boolean().optional(),
|
|
174
174
|
list: z.boolean().optional(),
|
|
175
175
|
isTitle: z.boolean().optional(),
|
|
176
|
-
isBody: z.boolean().optional()
|
|
176
|
+
isBody: z.boolean().optional(),
|
|
177
|
+
// Listed so a parse keeps it (zod strips unknown keys); which fields may carry it is
|
|
178
|
+
// `ensureMaxItemsPlacement`'s call (config/validation.ts).
|
|
179
|
+
maxItems: z.number().int().positive().optional()
|
|
177
180
|
});
|
|
178
181
|
selectOptionSchema = z.union([
|
|
179
182
|
z.string(),
|
|
@@ -231,6 +234,7 @@ var init_field = __esm({
|
|
|
231
234
|
type: z.literal("select"),
|
|
232
235
|
options: z.array(selectOptionSchema).min(1)
|
|
233
236
|
});
|
|
237
|
+
referenceSortFieldSchema = z.string().regex(/^[^.]+(\.[^.]+)*$/, 'must be a field name or a dotted path such as "meta.publishedAt"');
|
|
234
238
|
referenceFieldSchema = fieldBaseSchema.extend({
|
|
235
239
|
type: z.literal("reference"),
|
|
236
240
|
collections: z.array(z.string().min(1)).min(1).optional(),
|
|
@@ -241,7 +245,14 @@ var init_field = __esm({
|
|
|
241
245
|
// default, so a consumer that adopts this schema's parse output would silently delete a
|
|
242
246
|
// runtime-consumed flag and the feature would no-op with no error. (`resolvedSchema` is
|
|
243
247
|
// absent on purpose — it is type-inference-only and stripping it changes no behavior.)
|
|
244
|
-
includeBody: z.boolean().optional()
|
|
248
|
+
includeBody: z.boolean().optional(),
|
|
249
|
+
optionSort: z.union([
|
|
250
|
+
z.enum(["label", "collection"]),
|
|
251
|
+
z.object({
|
|
252
|
+
field: referenceSortFieldSchema,
|
|
253
|
+
direction: z.enum(["asc", "desc"]).optional()
|
|
254
|
+
}).strict()
|
|
255
|
+
]).optional()
|
|
245
256
|
});
|
|
246
257
|
fieldHolder = [z.never()];
|
|
247
258
|
blockSchema = z.object({
|
|
@@ -439,7 +450,7 @@ var init_media = __esm({
|
|
|
439
450
|
|
|
440
451
|
// dist/config/schemas/config.js
|
|
441
452
|
import { z as z5 } from "zod";
|
|
442
|
-
var defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE;
|
|
453
|
+
var defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, seoFieldNameSchema, seoSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE;
|
|
443
454
|
var init_config = __esm({
|
|
444
455
|
"dist/config/schemas/config.js"() {
|
|
445
456
|
"use strict";
|
|
@@ -486,6 +497,19 @@ var init_config = __esm({
|
|
|
486
497
|
// Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
|
|
487
498
|
SignInComponent: z5.custom().optional()
|
|
488
499
|
});
|
|
500
|
+
seoFieldNameSchema = z5.string().min(1).optional();
|
|
501
|
+
seoSchema = z5.object({
|
|
502
|
+
group: z5.string().min(1).optional(),
|
|
503
|
+
fields: z5.object({
|
|
504
|
+
title: seoFieldNameSchema,
|
|
505
|
+
description: seoFieldNameSchema,
|
|
506
|
+
ogImage: seoFieldNameSchema,
|
|
507
|
+
ogType: seoFieldNameSchema,
|
|
508
|
+
canonical: seoFieldNameSchema,
|
|
509
|
+
noindex: seoFieldNameSchema,
|
|
510
|
+
twitterCard: seoFieldNameSchema
|
|
511
|
+
}).strict().optional()
|
|
512
|
+
}).strict();
|
|
489
513
|
CanopyConfigSchema = z5.object({
|
|
490
514
|
media: mediaSchema.optional(),
|
|
491
515
|
// No outer .optional(): both schemas already .default('deny'), so the field is optional on
|
|
@@ -535,6 +559,7 @@ var init_config = __esm({
|
|
|
535
559
|
entryLinkUrl: z5.custom().optional(),
|
|
536
560
|
validateEntry: z5.custom().optional(),
|
|
537
561
|
mdxAllow: mdxAllowlistSchema.optional(),
|
|
562
|
+
seo: seoSchema.optional(),
|
|
538
563
|
dev: devConfigSchema.optional()
|
|
539
564
|
}).strict();
|
|
540
565
|
DEFAULT_PROD_WORKSPACE = "/mnt/efs";
|
package/dist/config/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* import { defineCanopyConfig, type FieldConfig } from 'canopycms'
|
|
9
9
|
* ```
|
|
10
10
|
*/
|
|
11
|
-
export type { PrimitiveFieldType, FieldType, ContentFormat, MediaAdapterKind, PermissionLevel, PermissionTarget, PathPermission, SelectOption, ReferenceOption, FieldConfig, BlockConfig, BlockFieldConfig, SelectFieldConfig, ReferenceFieldConfig, ImageFieldConfig, MarkdownFieldConfig, MdxAllowlist, MdxComponentAllow, MdxPropAllow, ImageFieldValue, ObjectFieldConfig, InlineGroupFieldConfig, CustomFieldConfig, EntrySchema, BranchSchema, EntryTypeConfig, EntryTypeUnavailable, CollectionConfig, RootCollectionConfig, MediaConfig, CanopyEditorConfig, DefaultBranchAccess, DefaultPathAccess, DefaultPathAccessLevel, DefaultPathAccessLevels, DefaultBaseBranch, DefaultRemoteName, DefaultRemoteUrl, GitBotAuthorName, GitBotAuthorEmail, GithubTokenEnvVar, CanopyOperatingMode, ContentRoot, SourceRoot, CanopyConfig, CanopyConfigInput, CanopyConfigFragment, FlatSchemaItem, CanopyClientConfig, ClientOnlyFields, EditorSignInProps, EntryValidationIssue, ValidateEntryInput, ValidateEntryHook, } from './types.js';
|
|
11
|
+
export type { PrimitiveFieldType, FieldType, ContentFormat, MediaAdapterKind, PermissionLevel, PermissionTarget, PathPermission, SelectOption, ReferenceOption, FieldConfig, BlockConfig, BlockFieldConfig, SelectFieldConfig, ReferenceFieldConfig, ReferenceOptionSort, ImageFieldConfig, MarkdownFieldConfig, MdxAllowlist, MdxComponentAllow, MdxPropAllow, ImageFieldValue, ObjectFieldConfig, InlineGroupFieldConfig, CustomFieldConfig, EntrySchema, BranchSchema, EntryTypeConfig, EntryTypeUnavailable, CollectionConfig, RootCollectionConfig, MediaConfig, CanopyEditorConfig, DefaultBranchAccess, DefaultPathAccess, DefaultPathAccessLevel, DefaultPathAccessLevels, DefaultBaseBranch, DefaultRemoteName, DefaultRemoteUrl, GitBotAuthorName, GitBotAuthorEmail, GithubTokenEnvVar, CanopyOperatingMode, ContentRoot, SourceRoot, CanopyConfig, CanopyConfigInput, CanopyConfigFragment, FlatSchemaItem, CanopyClientConfig, ClientOnlyFields, EditorSignInProps, EntryValidationIssue, ValidateEntryInput, ValidateEntryHook, } from './types.js';
|
|
12
12
|
export { primitiveFieldTypes, fieldTypes } from './types.js';
|
|
13
13
|
export { CanopyConfigSchema, getConfigDefaults, DEFAULT_PROD_WORKSPACE } from './schemas/config.js';
|
|
14
14
|
export { fieldSchema, blockSchema, selectOptionSchema, referenceOptionSchema, imageFieldSchema, } from './schemas/field.js';
|
|
@@ -105,6 +105,56 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
105
105
|
entryLinkUrl: z.ZodOptional<z.ZodType<EntryLinkUrlResolver, z.ZodTypeDef, EntryLinkUrlResolver>>;
|
|
106
106
|
validateEntry: z.ZodOptional<z.ZodType<ValidateEntryHook, z.ZodTypeDef, ValidateEntryHook>>;
|
|
107
107
|
mdxAllow: z.ZodOptional<z.ZodType<import("../index.js").MdxAllowlist, z.ZodTypeDef, import("../index.js").MdxAllowlist>>;
|
|
108
|
+
seo: z.ZodOptional<z.ZodObject<{
|
|
109
|
+
group: z.ZodOptional<z.ZodString>;
|
|
110
|
+
fields: z.ZodOptional<z.ZodObject<{
|
|
111
|
+
title: z.ZodOptional<z.ZodString>;
|
|
112
|
+
description: z.ZodOptional<z.ZodString>;
|
|
113
|
+
ogImage: z.ZodOptional<z.ZodString>;
|
|
114
|
+
ogType: z.ZodOptional<z.ZodString>;
|
|
115
|
+
canonical: z.ZodOptional<z.ZodString>;
|
|
116
|
+
noindex: z.ZodOptional<z.ZodString>;
|
|
117
|
+
twitterCard: z.ZodOptional<z.ZodString>;
|
|
118
|
+
}, "strict", z.ZodTypeAny, {
|
|
119
|
+
description?: string | undefined;
|
|
120
|
+
title?: string | undefined;
|
|
121
|
+
ogImage?: string | undefined;
|
|
122
|
+
ogType?: string | undefined;
|
|
123
|
+
canonical?: string | undefined;
|
|
124
|
+
noindex?: string | undefined;
|
|
125
|
+
twitterCard?: string | undefined;
|
|
126
|
+
}, {
|
|
127
|
+
description?: string | undefined;
|
|
128
|
+
title?: string | undefined;
|
|
129
|
+
ogImage?: string | undefined;
|
|
130
|
+
ogType?: string | undefined;
|
|
131
|
+
canonical?: string | undefined;
|
|
132
|
+
noindex?: string | undefined;
|
|
133
|
+
twitterCard?: string | undefined;
|
|
134
|
+
}>>;
|
|
135
|
+
}, "strict", z.ZodTypeAny, {
|
|
136
|
+
fields?: {
|
|
137
|
+
description?: string | undefined;
|
|
138
|
+
title?: string | undefined;
|
|
139
|
+
ogImage?: string | undefined;
|
|
140
|
+
ogType?: string | undefined;
|
|
141
|
+
canonical?: string | undefined;
|
|
142
|
+
noindex?: string | undefined;
|
|
143
|
+
twitterCard?: string | undefined;
|
|
144
|
+
} | undefined;
|
|
145
|
+
group?: string | undefined;
|
|
146
|
+
}, {
|
|
147
|
+
fields?: {
|
|
148
|
+
description?: string | undefined;
|
|
149
|
+
title?: string | undefined;
|
|
150
|
+
ogImage?: string | undefined;
|
|
151
|
+
ogType?: string | undefined;
|
|
152
|
+
canonical?: string | undefined;
|
|
153
|
+
noindex?: string | undefined;
|
|
154
|
+
twitterCard?: string | undefined;
|
|
155
|
+
} | undefined;
|
|
156
|
+
group?: string | undefined;
|
|
157
|
+
}>>;
|
|
108
158
|
dev: z.ZodOptional<z.ZodObject<{
|
|
109
159
|
contentSync: z.ZodOptional<z.ZodDefault<z.ZodEnum<["off", "warn"]>>>;
|
|
110
160
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -167,6 +217,18 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
167
217
|
entryLinkUrl?: EntryLinkUrlResolver | undefined;
|
|
168
218
|
validateEntry?: ValidateEntryHook | undefined;
|
|
169
219
|
mdxAllow?: import("../index.js").MdxAllowlist | undefined;
|
|
220
|
+
seo?: {
|
|
221
|
+
fields?: {
|
|
222
|
+
description?: string | undefined;
|
|
223
|
+
title?: string | undefined;
|
|
224
|
+
ogImage?: string | undefined;
|
|
225
|
+
ogType?: string | undefined;
|
|
226
|
+
canonical?: string | undefined;
|
|
227
|
+
noindex?: string | undefined;
|
|
228
|
+
twitterCard?: string | undefined;
|
|
229
|
+
} | undefined;
|
|
230
|
+
group?: string | undefined;
|
|
231
|
+
} | undefined;
|
|
170
232
|
}, {
|
|
171
233
|
gitBotAuthorName: string;
|
|
172
234
|
gitBotAuthorEmail: string;
|
|
@@ -222,6 +284,18 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
222
284
|
entryLinkUrl?: EntryLinkUrlResolver | undefined;
|
|
223
285
|
validateEntry?: ValidateEntryHook | undefined;
|
|
224
286
|
mdxAllow?: import("../index.js").MdxAllowlist | undefined;
|
|
287
|
+
seo?: {
|
|
288
|
+
fields?: {
|
|
289
|
+
description?: string | undefined;
|
|
290
|
+
title?: string | undefined;
|
|
291
|
+
ogImage?: string | undefined;
|
|
292
|
+
ogType?: string | undefined;
|
|
293
|
+
canonical?: string | undefined;
|
|
294
|
+
noindex?: string | undefined;
|
|
295
|
+
twitterCard?: string | undefined;
|
|
296
|
+
} | undefined;
|
|
297
|
+
group?: string | undefined;
|
|
298
|
+
} | undefined;
|
|
225
299
|
}>;
|
|
226
300
|
/**
|
|
227
301
|
* Default workspace path for prod mode (used when CANOPYCMS_WORKSPACE_ROOT is not set): where
|
|
@@ -61,6 +61,26 @@ const editorConfigSchema = z.object({
|
|
|
61
61
|
// Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
|
|
62
62
|
SignInComponent: z.custom().optional(),
|
|
63
63
|
});
|
|
64
|
+
const seoFieldNameSchema = z.string().min(1).optional();
|
|
65
|
+
// Strict, so a misspelled key (`noIndex`) fails the parse instead of leaving every surface
|
|
66
|
+
// reading the default field name.
|
|
67
|
+
const seoSchema = z
|
|
68
|
+
.object({
|
|
69
|
+
group: z.string().min(1).optional(),
|
|
70
|
+
fields: z
|
|
71
|
+
.object({
|
|
72
|
+
title: seoFieldNameSchema,
|
|
73
|
+
description: seoFieldNameSchema,
|
|
74
|
+
ogImage: seoFieldNameSchema,
|
|
75
|
+
ogType: seoFieldNameSchema,
|
|
76
|
+
canonical: seoFieldNameSchema,
|
|
77
|
+
noindex: seoFieldNameSchema,
|
|
78
|
+
twitterCard: seoFieldNameSchema,
|
|
79
|
+
})
|
|
80
|
+
.strict()
|
|
81
|
+
.optional(),
|
|
82
|
+
})
|
|
83
|
+
.strict();
|
|
64
84
|
export const CanopyConfigSchema = z
|
|
65
85
|
.object({
|
|
66
86
|
media: mediaSchema.optional(),
|
|
@@ -110,6 +130,7 @@ export const CanopyConfigSchema = z
|
|
|
110
130
|
entryLinkUrl: z.custom().optional(),
|
|
111
131
|
validateEntry: z.custom().optional(),
|
|
112
132
|
mdxAllow: mdxAllowlistSchema.optional(),
|
|
133
|
+
seo: seoSchema.optional(),
|
|
113
134
|
dev: devConfigSchema.optional(),
|
|
114
135
|
})
|
|
115
136
|
.strict();
|