multisite-cms-mcp 1.2.3 → 1.3.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/dist/index.js +6 -6
- package/dist/tools/get-conversion-guide.d.ts.map +1 -1
- package/dist/tools/get-conversion-guide.js +134 -295
- package/dist/tools/get-example.d.ts +1 -1
- package/dist/tools/get-example.d.ts.map +1 -1
- package/dist/tools/get-example.js +222 -374
- package/dist/tools/get-schema.d.ts +1 -1
- package/dist/tools/get-schema.d.ts.map +1 -1
- package/dist/tools/get-schema.js +130 -450
- package/dist/tools/get-tenant-schema.d.ts +9 -3
- package/dist/tools/get-tenant-schema.d.ts.map +1 -1
- package/dist/tools/get-tenant-schema.js +16 -4
- package/dist/tools/sync-schema.d.ts +1 -1
- package/dist/tools/sync-schema.d.ts.map +1 -1
- package/dist/tools/sync-schema.js +51 -97
- package/dist/tools/validate-manifest.d.ts.map +1 -1
- package/dist/tools/validate-manifest.js +26 -83
- package/dist/tools/validate-package.d.ts.map +1 -1
- package/dist/tools/validate-package.js +46 -41
- package/dist/tools/validate-template.d.ts +2 -2
- package/dist/tools/validate-template.d.ts.map +1 -1
- package/dist/tools/validate-template.js +84 -196
- package/package.json +1 -1
|
@@ -14,36 +14,34 @@ async function validatePackage(fileList, manifestContent, templateContents) {
|
|
|
14
14
|
const hasManifest = fileList.some(f => f === 'manifest.json' || f.endsWith('/manifest.json'));
|
|
15
15
|
const hasPagesFolder = fileList.some(f => f.includes('pages/'));
|
|
16
16
|
const hasPublicFolder = fileList.some(f => f.includes('public/'));
|
|
17
|
-
// Note: Templates folder is optional - only needed for CMS pages
|
|
18
|
-
// Check is performed but variable not stored since it's informational only
|
|
19
17
|
if (!hasManifest) {
|
|
20
18
|
errors.push('- Missing manifest.json at package root');
|
|
21
19
|
}
|
|
22
20
|
else {
|
|
23
|
-
validations.push('
|
|
21
|
+
validations.push('manifest.json found');
|
|
24
22
|
}
|
|
25
23
|
if (!hasPagesFolder) {
|
|
26
24
|
warnings.push('- No pages/ folder found - static pages should be in pages/');
|
|
27
25
|
}
|
|
28
26
|
else {
|
|
29
|
-
validations.push('
|
|
27
|
+
validations.push('pages/ folder found');
|
|
30
28
|
}
|
|
31
29
|
if (!hasPublicFolder) {
|
|
32
30
|
warnings.push('- No public/ folder found - assets (CSS, JS, images) should be in public/');
|
|
33
31
|
}
|
|
34
32
|
else {
|
|
35
|
-
validations.push('
|
|
33
|
+
validations.push('public/ folder found');
|
|
36
34
|
}
|
|
37
35
|
// 2. Validate manifest
|
|
38
36
|
const manifestResult = await (0, validate_manifest_1.validateManifest)(manifestContent);
|
|
39
|
-
if (manifestResult.
|
|
37
|
+
if (manifestResult.includes('INVALID')) {
|
|
40
38
|
errors.push('- Manifest validation failed (see details below)');
|
|
41
39
|
}
|
|
42
|
-
else if (manifestResult.
|
|
40
|
+
else if (manifestResult.includes('WARNINGS')) {
|
|
43
41
|
warnings.push('- Manifest has warnings (see details below)');
|
|
44
42
|
}
|
|
45
43
|
else {
|
|
46
|
-
validations.push('
|
|
44
|
+
validations.push('manifest.json is valid');
|
|
47
45
|
}
|
|
48
46
|
// 3. Parse manifest for template checks
|
|
49
47
|
let manifest = null;
|
|
@@ -65,19 +63,25 @@ async function validatePackage(fileList, manifestContent, templateContents) {
|
|
|
65
63
|
}
|
|
66
64
|
}
|
|
67
65
|
}
|
|
68
|
-
// 5. Check CMS template files exist
|
|
66
|
+
// 5. Check CMS template files exist (detect custom collections from manifest)
|
|
69
67
|
if (manifest?.cmsTemplates) {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
68
|
+
const templates = manifest.cmsTemplates;
|
|
69
|
+
const checkedTemplates = new Set();
|
|
70
|
+
for (const [key, value] of Object.entries(templates)) {
|
|
71
|
+
// Skip path keys and non-string values
|
|
72
|
+
if (key.endsWith('Path') || typeof value !== 'string' || !value.endsWith('.html')) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// Avoid duplicate checks
|
|
76
|
+
if (checkedTemplates.has(value))
|
|
77
|
+
continue;
|
|
78
|
+
checkedTemplates.add(value);
|
|
79
|
+
const fileExists = fileList.some(f => f === value || f.endsWith('/' + value));
|
|
80
|
+
if (!fileExists) {
|
|
81
|
+
errors.push(`- Template file not found: ${value} (cmsTemplates.${key})`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
validations.push(`${key} template exists`);
|
|
81
85
|
}
|
|
82
86
|
}
|
|
83
87
|
}
|
|
@@ -85,31 +89,32 @@ async function validatePackage(fileList, manifestContent, templateContents) {
|
|
|
85
89
|
const templateValidations = [];
|
|
86
90
|
if (templateContents) {
|
|
87
91
|
for (const [path, content] of Object.entries(templateContents)) {
|
|
92
|
+
// Determine template type based on path
|
|
88
93
|
let templateType = 'static_page';
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
// Check if it's an index template
|
|
95
|
+
if (path.includes('_index') || path.includes('-index')) {
|
|
96
|
+
templateType = 'custom_index';
|
|
91
97
|
}
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
// Check if it's a detail template
|
|
99
|
+
else if (path.includes('_detail') || path.includes('-detail') || path.includes('_post') || path.includes('-post')) {
|
|
100
|
+
templateType = 'custom_detail';
|
|
94
101
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
else if (path.includes('author') && path.includes('detail')) {
|
|
105
|
-
templateType = 'author_detail';
|
|
102
|
+
// Check if it's in templates/ folder (likely a CMS template)
|
|
103
|
+
else if (path.includes('templates/')) {
|
|
104
|
+
// If it has collection-like patterns
|
|
105
|
+
if (content.includes('{{#each')) {
|
|
106
|
+
templateType = 'custom_index';
|
|
107
|
+
}
|
|
108
|
+
else if (content.includes('{{name}}') || content.includes('{{slug}}')) {
|
|
109
|
+
templateType = 'custom_detail';
|
|
110
|
+
}
|
|
106
111
|
}
|
|
107
112
|
const result = await (0, validate_template_1.validateTemplate)(content, templateType);
|
|
108
|
-
if (result.
|
|
113
|
+
if (result.includes('ERRORS')) {
|
|
109
114
|
errors.push(`- Template ${path} has errors`);
|
|
110
115
|
templateValidations.push(`\n### ${path}\n${result}`);
|
|
111
116
|
}
|
|
112
|
-
else if (result.
|
|
117
|
+
else if (result.includes('WARNINGS')) {
|
|
113
118
|
warnings.push(`- Template ${path} has warnings`);
|
|
114
119
|
templateValidations.push(`\n### ${path}\n${result}`);
|
|
115
120
|
}
|
|
@@ -141,9 +146,9 @@ async function validatePackage(fileList, manifestContent, templateContents) {
|
|
|
141
146
|
// Build result
|
|
142
147
|
let output = '';
|
|
143
148
|
if (errors.length === 0) {
|
|
144
|
-
output =
|
|
149
|
+
output = `PACKAGE STRUCTURE VALID
|
|
145
150
|
|
|
146
|
-
${validations.join('\n')}
|
|
151
|
+
${validations.map(v => `- ${v}`).join('\n')}
|
|
147
152
|
|
|
148
153
|
Summary:
|
|
149
154
|
- ${htmlFiles.length} HTML file(s)
|
|
@@ -158,13 +163,13 @@ ${warnings.join('\n')}`;
|
|
|
158
163
|
}
|
|
159
164
|
}
|
|
160
165
|
else {
|
|
161
|
-
output =
|
|
166
|
+
output = `PACKAGE HAS ERRORS
|
|
162
167
|
|
|
163
168
|
Errors (must fix):
|
|
164
169
|
${errors.join('\n')}
|
|
165
170
|
|
|
166
171
|
What passed:
|
|
167
|
-
${validations.join('\n')}`;
|
|
172
|
+
${validations.map(v => `- ${v}`).join('\n')}`;
|
|
168
173
|
if (warnings.length > 0) {
|
|
169
174
|
output += `
|
|
170
175
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
type TemplateType = '
|
|
1
|
+
type TemplateType = 'custom_index' | 'custom_detail' | 'static_page';
|
|
2
2
|
/**
|
|
3
3
|
* Validates an HTML template for correct CMS token usage
|
|
4
4
|
*
|
|
5
5
|
* @param html - The HTML template content
|
|
6
|
-
* @param templateType - The type of template
|
|
6
|
+
* @param templateType - The type of template (custom_index, custom_detail, static_page)
|
|
7
7
|
* @param collectionSlug - For custom collections, the collection slug
|
|
8
8
|
* @param projectId - Optional: Project ID or name to validate against actual schema
|
|
9
9
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-template.d.ts","sourceRoot":"","sources":["../../src/tools/validate-template.ts"],"names":[],"mappings":"AAMA,KAAK,YAAY,GAAG,
|
|
1
|
+
{"version":3,"file":"validate-template.d.ts","sourceRoot":"","sources":["../../src/tools/validate-template.ts"],"names":[],"mappings":"AAMA,KAAK,YAAY,GAAG,cAAc,GAAG,eAAe,GAAG,aAAa,CAAC;AAmJrE;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,YAAY,EAC1B,cAAc,CAAC,EAAE,MAAM,EACvB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAoXjB"}
|
|
@@ -2,73 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.validateTemplate = validateTemplate;
|
|
4
4
|
const api_client_1 = require("../lib/api-client");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
{ name: 'publishedAt', isRichText: false },
|
|
14
|
-
{ name: 'url', isRichText: false },
|
|
15
|
-
];
|
|
16
|
-
const AUTHOR_FIELDS = [
|
|
17
|
-
{ name: 'name', isRichText: false, isRequired: true },
|
|
18
|
-
{ name: 'slug', isRichText: false, isRequired: true },
|
|
19
|
-
{ name: 'bio', isRichText: true },
|
|
20
|
-
{ name: 'picture', isRichText: false },
|
|
21
|
-
{ name: 'email', isRichText: false },
|
|
22
|
-
{ name: 'twitterProfileLink', isRichText: false },
|
|
23
|
-
{ name: 'linkedinProfileLink', isRichText: false },
|
|
24
|
-
{ name: 'url', isRichText: false },
|
|
25
|
-
];
|
|
26
|
-
const TEAM_FIELDS = [
|
|
27
|
-
{ name: 'name', isRichText: false, isRequired: true },
|
|
28
|
-
{ name: 'slug', isRichText: false },
|
|
29
|
-
{ name: 'role', isRichText: false },
|
|
30
|
-
{ name: 'bio', isRichText: true },
|
|
31
|
-
{ name: 'photo', isRichText: false },
|
|
32
|
-
{ name: 'email', isRichText: false },
|
|
33
|
-
{ name: 'order', isRichText: false },
|
|
34
|
-
];
|
|
35
|
-
const DOWNLOAD_FIELDS = [
|
|
36
|
-
{ name: 'title', isRichText: false, isRequired: true },
|
|
37
|
-
{ name: 'slug', isRichText: false },
|
|
38
|
-
{ name: 'description', isRichText: false },
|
|
39
|
-
{ name: 'fileUrl', isRichText: false },
|
|
40
|
-
{ name: 'category', isRichText: false },
|
|
41
|
-
{ name: 'order', isRichText: false },
|
|
42
|
-
];
|
|
43
|
-
// Common wrong field names and their corrections
|
|
44
|
-
const COMMON_MISTAKES = {
|
|
45
|
-
'title': 'name (for blog posts) or title (for downloads)',
|
|
46
|
-
'body': 'postBody',
|
|
47
|
-
'content': 'postBody',
|
|
48
|
-
'excerpt': 'postSummary',
|
|
49
|
-
'summary': 'postSummary',
|
|
50
|
-
'image': 'mainImage or thumbnailImage',
|
|
51
|
-
'coverImage': 'mainImage',
|
|
52
|
-
'coverImageUrl': 'mainImage',
|
|
53
|
-
'heroImage': 'mainImage',
|
|
54
|
-
'thumbnail': 'thumbnailImage',
|
|
55
|
-
'date': 'publishedAt',
|
|
56
|
-
'createdAt': 'publishedAt',
|
|
57
|
-
'author_name': 'author.name',
|
|
58
|
-
'authorName': 'author.name',
|
|
59
|
-
'link': 'url',
|
|
60
|
-
'href': 'url',
|
|
61
|
-
'headshot': 'photo (for team) or picture (for authors)',
|
|
62
|
-
'headshotUrl': 'photo (for team) or picture (for authors)',
|
|
63
|
-
'position': 'role',
|
|
64
|
-
'jobTitle': 'role',
|
|
65
|
-
};
|
|
66
|
-
// Built-in collection standard fields (not including custom fields)
|
|
67
|
-
const BUILTIN_STANDARD_FIELDS = {
|
|
68
|
-
blogs: ['name', 'slug', 'mainImage', 'thumbnailImage', 'postSummary', 'postBody', 'featured', 'publishedAt', 'author', 'url'],
|
|
69
|
-
authors: ['name', 'slug', 'bio', 'picture', 'email', 'twitterProfileLink', 'linkedinProfileLink', 'url'],
|
|
70
|
-
team: ['name', 'slug', 'role', 'bio', 'photo', 'email', 'order'],
|
|
71
|
-
downloads: ['title', 'slug', 'description', 'fileUrl', 'category', 'order'],
|
|
5
|
+
// Common wrong field names and suggestions (generic)
|
|
6
|
+
const COMMON_SUGGESTIONS = {
|
|
7
|
+
'body': 'Consider using a descriptive name like "content" or "description"',
|
|
8
|
+
'content': 'Good field name - make sure it exists in your collection',
|
|
9
|
+
'image': 'Consider using a descriptive name like "heroImage", "thumbnail", etc.',
|
|
10
|
+
'link': 'Use "url" as the field name for links',
|
|
11
|
+
'href': 'Use "url" as the field name for links',
|
|
12
|
+
'date': 'Use "publishedAt" (auto-tracked) or define a custom date field',
|
|
72
13
|
};
|
|
73
14
|
/**
|
|
74
15
|
* Resolve project identifier to tenant ID
|
|
@@ -95,17 +36,8 @@ async function fetchTenantSchema(tenantId) {
|
|
|
95
36
|
if ((0, api_client_1.isApiError)(collectionsRes)) {
|
|
96
37
|
return null;
|
|
97
38
|
}
|
|
98
|
-
// Fetch builtin collection fields
|
|
99
|
-
const builtinRes = await (0, api_client_1.apiRequest)('/api/cms/builtin-collections', { tenantId });
|
|
100
|
-
const builtinFields = {};
|
|
101
|
-
if (!(0, api_client_1.isApiError)(builtinRes)) {
|
|
102
|
-
for (const bc of builtinRes.data) {
|
|
103
|
-
builtinFields[bc.type] = bc.fields;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
39
|
return {
|
|
107
40
|
collections: collectionsRes.data,
|
|
108
|
-
builtinFields,
|
|
109
41
|
};
|
|
110
42
|
}
|
|
111
43
|
/**
|
|
@@ -135,42 +67,41 @@ function extractTokenFieldNames(html) {
|
|
|
135
67
|
/**
|
|
136
68
|
* Check which fields are missing from schema
|
|
137
69
|
*/
|
|
138
|
-
function findMissingFields(usedFields, collectionType,
|
|
70
|
+
function findMissingFields(usedFields, collectionType, schema) {
|
|
139
71
|
const missing = [];
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
72
|
+
// Check against custom collection fields
|
|
73
|
+
const collection = schema.collections.find(c => c.slug === collectionType);
|
|
74
|
+
if (collection) {
|
|
75
|
+
// Built-in fields available on all items
|
|
76
|
+
const builtInFields = ['name', 'slug', 'url', 'publishedAt', 'createdAt', 'updatedAt'];
|
|
77
|
+
const collectionFields = [...builtInFields, ...collection.fields.map(f => f.slug)];
|
|
145
78
|
for (const field of usedFields) {
|
|
146
|
-
if (!
|
|
79
|
+
if (!collectionFields.includes(field)) {
|
|
147
80
|
missing.push(field);
|
|
148
81
|
}
|
|
149
82
|
}
|
|
150
83
|
}
|
|
151
84
|
else {
|
|
152
|
-
//
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
const collectionFields = ['name', 'slug', 'publishedAt', ...collection.fields.map(f => f.slug)];
|
|
156
|
-
for (const field of usedFields) {
|
|
157
|
-
if (!collectionFields.includes(field)) {
|
|
158
|
-
missing.push(field);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
// Collection doesn't exist - all fields are "missing"
|
|
164
|
-
return usedFields;
|
|
165
|
-
}
|
|
85
|
+
// Collection doesn't exist - all fields except built-ins are "missing"
|
|
86
|
+
const builtInFields = ['name', 'slug', 'url', 'publishedAt', 'createdAt', 'updatedAt'];
|
|
87
|
+
return usedFields.filter(f => !builtInFields.includes(f));
|
|
166
88
|
}
|
|
167
89
|
return missing;
|
|
168
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Get richText fields from collection
|
|
93
|
+
*/
|
|
94
|
+
function getRichTextFields(collectionSlug, schema) {
|
|
95
|
+
const collection = schema.collections.find(c => c.slug === collectionSlug);
|
|
96
|
+
if (!collection)
|
|
97
|
+
return [];
|
|
98
|
+
return collection.fields.filter(f => f.type === 'richText').map(f => f.slug);
|
|
99
|
+
}
|
|
169
100
|
/**
|
|
170
101
|
* Validates an HTML template for correct CMS token usage
|
|
171
102
|
*
|
|
172
103
|
* @param html - The HTML template content
|
|
173
|
-
* @param templateType - The type of template
|
|
104
|
+
* @param templateType - The type of template (custom_index, custom_detail, static_page)
|
|
174
105
|
* @param collectionSlug - For custom collections, the collection slug
|
|
175
106
|
* @param projectId - Optional: Project ID or name to validate against actual schema
|
|
176
107
|
*/
|
|
@@ -178,36 +109,7 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
|
|
|
178
109
|
const errors = [];
|
|
179
110
|
const warnings = [];
|
|
180
111
|
const suggestions = [];
|
|
181
|
-
|
|
182
|
-
let expectedFields = [];
|
|
183
|
-
let collectionName = '';
|
|
184
|
-
switch (templateType) {
|
|
185
|
-
case 'blog_index':
|
|
186
|
-
case 'blog_post':
|
|
187
|
-
expectedFields = BLOG_FIELDS;
|
|
188
|
-
collectionName = 'blogs';
|
|
189
|
-
break;
|
|
190
|
-
case 'authors_index':
|
|
191
|
-
case 'author_detail':
|
|
192
|
-
expectedFields = AUTHOR_FIELDS;
|
|
193
|
-
collectionName = 'authors';
|
|
194
|
-
break;
|
|
195
|
-
case 'team':
|
|
196
|
-
expectedFields = TEAM_FIELDS;
|
|
197
|
-
collectionName = 'team';
|
|
198
|
-
break;
|
|
199
|
-
case 'downloads':
|
|
200
|
-
expectedFields = DOWNLOAD_FIELDS;
|
|
201
|
-
collectionName = 'downloads';
|
|
202
|
-
break;
|
|
203
|
-
case 'custom_index':
|
|
204
|
-
case 'custom_detail':
|
|
205
|
-
collectionName = collectionSlug || 'collection';
|
|
206
|
-
break;
|
|
207
|
-
case 'static_page':
|
|
208
|
-
// Static pages just use data-edit-key
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
112
|
+
let richTextFields = [];
|
|
211
113
|
// Extract all tokens from the HTML
|
|
212
114
|
const doubleTokens = html.match(/\{\{([^{}#/]+)\}\}/g) || [];
|
|
213
115
|
const tripleTokens = html.match(/\{\{\{([^{}]+)\}\}\}/g) || [];
|
|
@@ -219,38 +121,21 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
|
|
|
219
121
|
if (tokensWithSpaces.length > 0 || tokensWithSpaces2.length > 0) {
|
|
220
122
|
warnings.push('- Some tokens have spaces inside braces (e.g., {{ name }} instead of {{name}}). While supported, {{name}} without spaces is preferred.');
|
|
221
123
|
}
|
|
222
|
-
// Check for common
|
|
124
|
+
// Check for common suggestions
|
|
223
125
|
for (const token of doubleTokens) {
|
|
224
126
|
const fieldName = token.replace(/\{\{|\}\}/g, '').trim();
|
|
225
127
|
// Skip control structures
|
|
226
128
|
if (fieldName.startsWith('#') || fieldName.startsWith('/') || fieldName === 'else' || fieldName.startsWith('@')) {
|
|
227
129
|
continue;
|
|
228
130
|
}
|
|
229
|
-
// Check against common
|
|
131
|
+
// Check against common suggestions
|
|
230
132
|
const baseName = fieldName.split('.')[0];
|
|
231
|
-
if (
|
|
232
|
-
|
|
133
|
+
if (COMMON_SUGGESTIONS[baseName]) {
|
|
134
|
+
suggestions.push(`- Token {{${baseName}}}: ${COMMON_SUGGESTIONS[baseName]}`);
|
|
233
135
|
}
|
|
234
136
|
}
|
|
235
|
-
// Check richText fields are using triple braces
|
|
236
|
-
for (const field of expectedFields) {
|
|
237
|
-
if (field.isRichText) {
|
|
238
|
-
const doublePattern = new RegExp(`\\{\\{${field.name}\\}\\}`, 'g');
|
|
239
|
-
const triplePattern = new RegExp(`\\{\\{\\{${field.name}\\}\\}\\}`, 'g');
|
|
240
|
-
const hasDouble = doublePattern.test(html);
|
|
241
|
-
const hasTriple = triplePattern.test(html);
|
|
242
|
-
if (hasDouble && !hasTriple) {
|
|
243
|
-
errors.push(`- {{${field.name}}} must use triple braces {{{${field.name}}}} because it contains HTML`);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
// Check for nested author fields in double braces (should be fine)
|
|
248
|
-
const authorBioDouble = /\{\{author\.bio\}\}/g.test(html);
|
|
249
|
-
if (authorBioDouble) {
|
|
250
|
-
errors.push('- {{author.bio}} must use triple braces {{{author.bio}}} because it contains HTML');
|
|
251
|
-
}
|
|
252
137
|
// Check loop syntax for index templates
|
|
253
|
-
if (templateType
|
|
138
|
+
if (templateType === 'custom_index') {
|
|
254
139
|
if (eachLoops.length === 0) {
|
|
255
140
|
warnings.push(`- No {{#each}} loop found - index templates usually need a loop to display items`);
|
|
256
141
|
}
|
|
@@ -260,8 +145,8 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
|
|
|
260
145
|
const match = loop.match(/\{\{#each\s+(\w+)/);
|
|
261
146
|
if (match) {
|
|
262
147
|
const usedCollection = match[1];
|
|
263
|
-
if (
|
|
264
|
-
warnings.push(`- Loop uses '${usedCollection}' but expected '${
|
|
148
|
+
if (collectionSlug && usedCollection !== collectionSlug) {
|
|
149
|
+
warnings.push(`- Loop uses '${usedCollection}' but expected '${collectionSlug}'`);
|
|
265
150
|
}
|
|
266
151
|
}
|
|
267
152
|
}
|
|
@@ -302,6 +187,23 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
|
|
|
302
187
|
if (wrongAssetPaths.length > 5) {
|
|
303
188
|
warnings.push(`- ...and ${wrongAssetPaths.length - 5} more asset paths that may need /public/ prefix`);
|
|
304
189
|
}
|
|
190
|
+
// Check YouTube iframes for required attributes
|
|
191
|
+
const allIframes = html.match(/<iframe[^>]*>/gi) || [];
|
|
192
|
+
for (const iframe of allIframes) {
|
|
193
|
+
const isYouTubeEmbed = /youtube\.com|youtu\.be/i.test(iframe);
|
|
194
|
+
const hasTemplateSrc = /src=["'][^"']*\{\{[^}]+\}\}[^"']*["']/i.test(iframe);
|
|
195
|
+
if (isYouTubeEmbed || hasTemplateSrc) {
|
|
196
|
+
if (!/referrerpolicy/i.test(iframe)) {
|
|
197
|
+
errors.push(`- YouTube iframe missing referrerpolicy attribute. Add: referrerpolicy="strict-origin-when-cross-origin" (without this, YouTube will show Error 153)`);
|
|
198
|
+
}
|
|
199
|
+
if (!/allowfullscreen/i.test(iframe)) {
|
|
200
|
+
warnings.push(`- iframe missing allowfullscreen attribute - fullscreen button won't work`);
|
|
201
|
+
}
|
|
202
|
+
if (!/title=/i.test(iframe)) {
|
|
203
|
+
suggestions.push(`- Consider adding a title attribute to iframe for accessibility`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
305
207
|
// Validate site tokens
|
|
306
208
|
const siteTokens = html.match(/\{\{site\.(\w+)\}\}/g) || [];
|
|
307
209
|
for (const token of siteTokens) {
|
|
@@ -320,7 +222,6 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
|
|
|
320
222
|
// Validate parent context references (../)
|
|
321
223
|
const parentRefs = html.match(/\{\{\.\.\/([\w.]+)\}\}/g) || [];
|
|
322
224
|
if (parentRefs.length > 0) {
|
|
323
|
-
// Check if they're used inside a loop
|
|
324
225
|
if (eachLoops.length === 0) {
|
|
325
226
|
warnings.push(`- Found ${parentRefs.length} parent reference(s) like {{../name}} but no {{#each}} loop - these only work inside loops`);
|
|
326
227
|
}
|
|
@@ -332,7 +233,6 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
|
|
|
332
233
|
const eqHelpers = html.match(/\{\{#if\s+\(eq\s+[^)]+\)\s*\}\}/g) || [];
|
|
333
234
|
if (eqHelpers.length > 0) {
|
|
334
235
|
suggestions.push(`- Found ${eqHelpers.length} equality comparison(s) like {{#if (eq field1 field2)}} - compares two values`);
|
|
335
|
-
// Check if closing {{/if}} exists for each
|
|
336
236
|
for (const helper of eqHelpers) {
|
|
337
237
|
if (!html.includes('{{/if}}')) {
|
|
338
238
|
errors.push(`- Missing {{/if}} to close: ${helper}`);
|
|
@@ -353,49 +253,35 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
|
|
|
353
253
|
if (resolved) {
|
|
354
254
|
const schema = await fetchTenantSchema(resolved.tenantId);
|
|
355
255
|
if (schema) {
|
|
356
|
-
|
|
357
|
-
let targetCollection = collectionSlug || '';
|
|
358
|
-
let isBuiltin = false;
|
|
359
|
-
switch (templateType) {
|
|
360
|
-
case 'blog_index':
|
|
361
|
-
case 'blog_post':
|
|
362
|
-
targetCollection = 'blogs';
|
|
363
|
-
isBuiltin = true;
|
|
364
|
-
break;
|
|
365
|
-
case 'authors_index':
|
|
366
|
-
case 'author_detail':
|
|
367
|
-
targetCollection = 'authors';
|
|
368
|
-
isBuiltin = true;
|
|
369
|
-
break;
|
|
370
|
-
case 'team':
|
|
371
|
-
targetCollection = 'team';
|
|
372
|
-
isBuiltin = true;
|
|
373
|
-
break;
|
|
374
|
-
case 'downloads':
|
|
375
|
-
targetCollection = 'downloads';
|
|
376
|
-
isBuiltin = true;
|
|
377
|
-
break;
|
|
378
|
-
case 'custom_index':
|
|
379
|
-
case 'custom_detail':
|
|
380
|
-
isBuiltin = false;
|
|
381
|
-
break;
|
|
382
|
-
}
|
|
256
|
+
const targetCollection = collectionSlug || '';
|
|
383
257
|
if (targetCollection && templateType !== 'static_page') {
|
|
258
|
+
// Get richText fields for triple brace validation
|
|
259
|
+
richTextFields = getRichTextFields(targetCollection, schema);
|
|
260
|
+
// Check richText fields are using triple braces
|
|
261
|
+
for (const fieldSlug of richTextFields) {
|
|
262
|
+
const doublePattern = new RegExp(`\\{\\{${fieldSlug}\\}\\}`, 'g');
|
|
263
|
+
const triplePattern = new RegExp(`\\{\\{\\{${fieldSlug}\\}\\}\\}`, 'g');
|
|
264
|
+
const hasDouble = doublePattern.test(html);
|
|
265
|
+
const hasTriple = triplePattern.test(html);
|
|
266
|
+
if (hasDouble && !hasTriple) {
|
|
267
|
+
errors.push(`- {{${fieldSlug}}} must use triple braces {{{${fieldSlug}}}} because it contains HTML (richText field)`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
384
270
|
// Extract fields used in template
|
|
385
271
|
const usedFields = extractTokenFieldNames(html);
|
|
386
|
-
// Check if
|
|
387
|
-
const collectionExists =
|
|
388
|
-
if (!
|
|
272
|
+
// Check if collection exists
|
|
273
|
+
const collectionExists = schema.collections.some(c => c.slug === targetCollection);
|
|
274
|
+
if (!collectionExists) {
|
|
389
275
|
// Collection doesn't exist - need to create it
|
|
390
276
|
schemaValidation = `
|
|
391
277
|
|
|
392
|
-
##
|
|
278
|
+
## ACTION REQUIRED: Collection Does Not Exist
|
|
393
279
|
|
|
394
280
|
The collection "${targetCollection}" does **not exist** in this project.
|
|
395
281
|
|
|
396
282
|
---
|
|
397
283
|
|
|
398
|
-
###
|
|
284
|
+
### YOU MUST CREATE THIS COLLECTION
|
|
399
285
|
|
|
400
286
|
Use the \`sync_schema\` tool to create the collection and its fields before deploying.
|
|
401
287
|
|
|
@@ -412,7 +298,7 @@ Use the \`sync_schema\` tool to create the collection and its fields before depl
|
|
|
412
298
|
"name": "${targetCollection.charAt(0).toUpperCase() + targetCollection.slice(1)}",
|
|
413
299
|
"nameSingular": "${targetCollection.endsWith('s') ? targetCollection.slice(0, -1).charAt(0).toUpperCase() + targetCollection.slice(0, -1).slice(1) : targetCollection.charAt(0).toUpperCase() + targetCollection.slice(1)}",
|
|
414
300
|
"fields": [
|
|
415
|
-
${usedFields.filter(f => !['name', 'slug', 'publishedAt'].includes(f)).map(f => ` { "slug": "${f}", "name": "${f.charAt(0).toUpperCase() + f.slice(1).replace(/([A-Z])/g, ' $1').trim()}", "type": "YOUR_TYPE" }`).join(',\n')}
|
|
301
|
+
${usedFields.filter(f => !['name', 'slug', 'url', 'publishedAt', 'createdAt', 'updatedAt'].includes(f)).map(f => ` { "slug": "${f}", "name": "${f.charAt(0).toUpperCase() + f.slice(1).replace(/([A-Z])/g, ' $1').trim()}", "type": "YOUR_TYPE" }`).join(',\n')}
|
|
416
302
|
]
|
|
417
303
|
}
|
|
418
304
|
]
|
|
@@ -428,17 +314,18 @@ ${usedFields.filter(f => !['name', 'slug', 'publishedAt'].includes(f)).map(f =>
|
|
|
428
314
|
- \`date\` - Date picker
|
|
429
315
|
- \`number\` - Numeric values
|
|
430
316
|
- \`select\` - Dropdown (requires \`options\` parameter)
|
|
317
|
+
- \`relation\` - Link to another collection (requires \`referenceCollection\` parameter)
|
|
431
318
|
|
|
432
319
|
**DO NOT SKIP THIS STEP** - Templates will not render without the collection.
|
|
433
320
|
`;
|
|
434
321
|
}
|
|
435
322
|
else {
|
|
436
323
|
// Find missing fields
|
|
437
|
-
const missingFields = findMissingFields(usedFields, targetCollection,
|
|
324
|
+
const missingFields = findMissingFields(usedFields, targetCollection, schema);
|
|
438
325
|
if (missingFields.length > 0) {
|
|
439
326
|
schemaValidation = `
|
|
440
327
|
|
|
441
|
-
##
|
|
328
|
+
## ACTION REQUIRED: Missing Fields
|
|
442
329
|
|
|
443
330
|
The following fields are used in the template but **do not exist** in the "${targetCollection}" collection:
|
|
444
331
|
|
|
@@ -446,7 +333,7 @@ ${missingFields.map(f => `- \`${f}\``).join('\n')}
|
|
|
446
333
|
|
|
447
334
|
---
|
|
448
335
|
|
|
449
|
-
###
|
|
336
|
+
### YOU MUST CREATE THESE FIELDS
|
|
450
337
|
|
|
451
338
|
Use the \`sync_schema\` tool to create the missing fields before deploying. This is required for your template to work correctly.
|
|
452
339
|
|
|
@@ -460,7 +347,7 @@ Use the \`sync_schema\` tool to create the missing fields before deploying. This
|
|
|
460
347
|
"fieldsToAdd": [
|
|
461
348
|
{
|
|
462
349
|
"collectionSlug": "${targetCollection}",
|
|
463
|
-
"isBuiltin":
|
|
350
|
+
"isBuiltin": false,
|
|
464
351
|
"fields": [
|
|
465
352
|
${missingFields.map(f => ` { "slug": "${f}", "name": "${f.charAt(0).toUpperCase() + f.slice(1).replace(/([A-Z])/g, ' $1').trim()}", "type": "YOUR_TYPE" }`).join(',\n')}
|
|
466
353
|
]
|
|
@@ -478,6 +365,7 @@ ${missingFields.map(f => ` { "slug": "${f}", "name": "${f.charAt(0).toUpp
|
|
|
478
365
|
- \`date\` - Date picker
|
|
479
366
|
- \`number\` - Numeric values
|
|
480
367
|
- \`select\` - Dropdown (requires \`options\` parameter)
|
|
368
|
+
- \`relation\` - Link to another collection (requires \`referenceCollection\` parameter)
|
|
481
369
|
|
|
482
370
|
**DO NOT SKIP THIS STEP** - Templates will not render correctly without these fields.
|
|
483
371
|
`;
|
|
@@ -485,7 +373,7 @@ ${missingFields.map(f => ` { "slug": "${f}", "name": "${f.charAt(0).toUpp
|
|
|
485
373
|
else {
|
|
486
374
|
schemaValidation = `
|
|
487
375
|
|
|
488
|
-
##
|
|
376
|
+
## Schema Validation Passed
|
|
489
377
|
|
|
490
378
|
All fields used in this template exist in the "${targetCollection}" collection.
|
|
491
379
|
`;
|
|
@@ -499,7 +387,7 @@ All fields used in this template exist in the "${targetCollection}" collection.
|
|
|
499
387
|
// projectId provided but not authenticated
|
|
500
388
|
schemaValidation = `
|
|
501
389
|
|
|
502
|
-
##
|
|
390
|
+
## Schema Validation Skipped
|
|
503
391
|
|
|
504
392
|
A projectId was provided but you're not authenticated.
|
|
505
393
|
To validate against your project's schema, authenticate first using the device flow or FASTMODE_AUTH_TOKEN.
|
|
@@ -510,7 +398,7 @@ Without authentication, only syntax validation is performed.
|
|
|
510
398
|
// Build result
|
|
511
399
|
let output = '';
|
|
512
400
|
if (errors.length === 0 && warnings.length === 0) {
|
|
513
|
-
output =
|
|
401
|
+
output = `TEMPLATE VALID
|
|
514
402
|
|
|
515
403
|
The ${templateType} template structure looks correct.
|
|
516
404
|
|
|
@@ -527,7 +415,7 @@ ${suggestions.join('\n')}`;
|
|
|
527
415
|
}
|
|
528
416
|
}
|
|
529
417
|
else if (errors.length === 0) {
|
|
530
|
-
output =
|
|
418
|
+
output = `TEMPLATE VALID WITH WARNINGS
|
|
531
419
|
|
|
532
420
|
Warnings:
|
|
533
421
|
${warnings.join('\n')}`;
|
|
@@ -539,7 +427,7 @@ ${suggestions.join('\n')}`;
|
|
|
539
427
|
}
|
|
540
428
|
}
|
|
541
429
|
else {
|
|
542
|
-
output =
|
|
430
|
+
output = `TEMPLATE HAS ERRORS
|
|
543
431
|
|
|
544
432
|
Errors (must fix):
|
|
545
433
|
${errors.join('\n')}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "multisite-cms-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "MCP server for Fast Mode CMS. Convert websites, validate packages, and deploy directly to Fast Mode. Includes authentication, project creation, schema sync, and one-click deployment.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|