multisite-cms-mcp 1.2.4 → 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 +5 -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.map +1 -1
- package/dist/tools/get-example.js +166 -409
- 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 +67 -201
- 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
|
-
|
|
233
|
-
}
|
|
234
|
-
}
|
|
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
|
-
}
|
|
133
|
+
if (COMMON_SUGGESTIONS[baseName]) {
|
|
134
|
+
suggestions.push(`- Token {{${baseName}}}: ${COMMON_SUGGESTIONS[baseName]}`);
|
|
245
135
|
}
|
|
246
136
|
}
|
|
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
|
}
|
|
@@ -303,22 +188,17 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
|
|
|
303
188
|
warnings.push(`- ...and ${wrongAssetPaths.length - 5} more asset paths that may need /public/ prefix`);
|
|
304
189
|
}
|
|
305
190
|
// Check YouTube iframes for required attributes
|
|
306
|
-
// Match all iframes - we'll check if they're video embeds
|
|
307
191
|
const allIframes = html.match(/<iframe[^>]*>/gi) || [];
|
|
308
192
|
for (const iframe of allIframes) {
|
|
309
|
-
// Check if it's likely a video embed (YouTube URL or template token in src)
|
|
310
193
|
const isYouTubeEmbed = /youtube\.com|youtu\.be/i.test(iframe);
|
|
311
194
|
const hasTemplateSrc = /src=["'][^"']*\{\{[^}]+\}\}[^"']*["']/i.test(iframe);
|
|
312
195
|
if (isYouTubeEmbed || hasTemplateSrc) {
|
|
313
|
-
// Check for referrerpolicy
|
|
314
196
|
if (!/referrerpolicy/i.test(iframe)) {
|
|
315
197
|
errors.push(`- YouTube iframe missing referrerpolicy attribute. Add: referrerpolicy="strict-origin-when-cross-origin" (without this, YouTube will show Error 153)`);
|
|
316
198
|
}
|
|
317
|
-
// Check for allowfullscreen
|
|
318
199
|
if (!/allowfullscreen/i.test(iframe)) {
|
|
319
200
|
warnings.push(`- iframe missing allowfullscreen attribute - fullscreen button won't work`);
|
|
320
201
|
}
|
|
321
|
-
// Check for title (accessibility)
|
|
322
202
|
if (!/title=/i.test(iframe)) {
|
|
323
203
|
suggestions.push(`- Consider adding a title attribute to iframe for accessibility`);
|
|
324
204
|
}
|
|
@@ -342,7 +222,6 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
|
|
|
342
222
|
// Validate parent context references (../)
|
|
343
223
|
const parentRefs = html.match(/\{\{\.\.\/([\w.]+)\}\}/g) || [];
|
|
344
224
|
if (parentRefs.length > 0) {
|
|
345
|
-
// Check if they're used inside a loop
|
|
346
225
|
if (eachLoops.length === 0) {
|
|
347
226
|
warnings.push(`- Found ${parentRefs.length} parent reference(s) like {{../name}} but no {{#each}} loop - these only work inside loops`);
|
|
348
227
|
}
|
|
@@ -354,7 +233,6 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
|
|
|
354
233
|
const eqHelpers = html.match(/\{\{#if\s+\(eq\s+[^)]+\)\s*\}\}/g) || [];
|
|
355
234
|
if (eqHelpers.length > 0) {
|
|
356
235
|
suggestions.push(`- Found ${eqHelpers.length} equality comparison(s) like {{#if (eq field1 field2)}} - compares two values`);
|
|
357
|
-
// Check if closing {{/if}} exists for each
|
|
358
236
|
for (const helper of eqHelpers) {
|
|
359
237
|
if (!html.includes('{{/if}}')) {
|
|
360
238
|
errors.push(`- Missing {{/if}} to close: ${helper}`);
|
|
@@ -375,49 +253,35 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
|
|
|
375
253
|
if (resolved) {
|
|
376
254
|
const schema = await fetchTenantSchema(resolved.tenantId);
|
|
377
255
|
if (schema) {
|
|
378
|
-
|
|
379
|
-
let targetCollection = collectionSlug || '';
|
|
380
|
-
let isBuiltin = false;
|
|
381
|
-
switch (templateType) {
|
|
382
|
-
case 'blog_index':
|
|
383
|
-
case 'blog_post':
|
|
384
|
-
targetCollection = 'blogs';
|
|
385
|
-
isBuiltin = true;
|
|
386
|
-
break;
|
|
387
|
-
case 'authors_index':
|
|
388
|
-
case 'author_detail':
|
|
389
|
-
targetCollection = 'authors';
|
|
390
|
-
isBuiltin = true;
|
|
391
|
-
break;
|
|
392
|
-
case 'team':
|
|
393
|
-
targetCollection = 'team';
|
|
394
|
-
isBuiltin = true;
|
|
395
|
-
break;
|
|
396
|
-
case 'downloads':
|
|
397
|
-
targetCollection = 'downloads';
|
|
398
|
-
isBuiltin = true;
|
|
399
|
-
break;
|
|
400
|
-
case 'custom_index':
|
|
401
|
-
case 'custom_detail':
|
|
402
|
-
isBuiltin = false;
|
|
403
|
-
break;
|
|
404
|
-
}
|
|
256
|
+
const targetCollection = collectionSlug || '';
|
|
405
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
|
+
}
|
|
406
270
|
// Extract fields used in template
|
|
407
271
|
const usedFields = extractTokenFieldNames(html);
|
|
408
|
-
// Check if
|
|
409
|
-
const collectionExists =
|
|
410
|
-
if (!
|
|
272
|
+
// Check if collection exists
|
|
273
|
+
const collectionExists = schema.collections.some(c => c.slug === targetCollection);
|
|
274
|
+
if (!collectionExists) {
|
|
411
275
|
// Collection doesn't exist - need to create it
|
|
412
276
|
schemaValidation = `
|
|
413
277
|
|
|
414
|
-
##
|
|
278
|
+
## ACTION REQUIRED: Collection Does Not Exist
|
|
415
279
|
|
|
416
280
|
The collection "${targetCollection}" does **not exist** in this project.
|
|
417
281
|
|
|
418
282
|
---
|
|
419
283
|
|
|
420
|
-
###
|
|
284
|
+
### YOU MUST CREATE THIS COLLECTION
|
|
421
285
|
|
|
422
286
|
Use the \`sync_schema\` tool to create the collection and its fields before deploying.
|
|
423
287
|
|
|
@@ -434,7 +298,7 @@ Use the \`sync_schema\` tool to create the collection and its fields before depl
|
|
|
434
298
|
"name": "${targetCollection.charAt(0).toUpperCase() + targetCollection.slice(1)}",
|
|
435
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)}",
|
|
436
300
|
"fields": [
|
|
437
|
-
${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')}
|
|
438
302
|
]
|
|
439
303
|
}
|
|
440
304
|
]
|
|
@@ -450,17 +314,18 @@ ${usedFields.filter(f => !['name', 'slug', 'publishedAt'].includes(f)).map(f =>
|
|
|
450
314
|
- \`date\` - Date picker
|
|
451
315
|
- \`number\` - Numeric values
|
|
452
316
|
- \`select\` - Dropdown (requires \`options\` parameter)
|
|
317
|
+
- \`relation\` - Link to another collection (requires \`referenceCollection\` parameter)
|
|
453
318
|
|
|
454
319
|
**DO NOT SKIP THIS STEP** - Templates will not render without the collection.
|
|
455
320
|
`;
|
|
456
321
|
}
|
|
457
322
|
else {
|
|
458
323
|
// Find missing fields
|
|
459
|
-
const missingFields = findMissingFields(usedFields, targetCollection,
|
|
324
|
+
const missingFields = findMissingFields(usedFields, targetCollection, schema);
|
|
460
325
|
if (missingFields.length > 0) {
|
|
461
326
|
schemaValidation = `
|
|
462
327
|
|
|
463
|
-
##
|
|
328
|
+
## ACTION REQUIRED: Missing Fields
|
|
464
329
|
|
|
465
330
|
The following fields are used in the template but **do not exist** in the "${targetCollection}" collection:
|
|
466
331
|
|
|
@@ -468,7 +333,7 @@ ${missingFields.map(f => `- \`${f}\``).join('\n')}
|
|
|
468
333
|
|
|
469
334
|
---
|
|
470
335
|
|
|
471
|
-
###
|
|
336
|
+
### YOU MUST CREATE THESE FIELDS
|
|
472
337
|
|
|
473
338
|
Use the \`sync_schema\` tool to create the missing fields before deploying. This is required for your template to work correctly.
|
|
474
339
|
|
|
@@ -482,7 +347,7 @@ Use the \`sync_schema\` tool to create the missing fields before deploying. This
|
|
|
482
347
|
"fieldsToAdd": [
|
|
483
348
|
{
|
|
484
349
|
"collectionSlug": "${targetCollection}",
|
|
485
|
-
"isBuiltin":
|
|
350
|
+
"isBuiltin": false,
|
|
486
351
|
"fields": [
|
|
487
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')}
|
|
488
353
|
]
|
|
@@ -500,6 +365,7 @@ ${missingFields.map(f => ` { "slug": "${f}", "name": "${f.charAt(0).toUpp
|
|
|
500
365
|
- \`date\` - Date picker
|
|
501
366
|
- \`number\` - Numeric values
|
|
502
367
|
- \`select\` - Dropdown (requires \`options\` parameter)
|
|
368
|
+
- \`relation\` - Link to another collection (requires \`referenceCollection\` parameter)
|
|
503
369
|
|
|
504
370
|
**DO NOT SKIP THIS STEP** - Templates will not render correctly without these fields.
|
|
505
371
|
`;
|
|
@@ -507,7 +373,7 @@ ${missingFields.map(f => ` { "slug": "${f}", "name": "${f.charAt(0).toUpp
|
|
|
507
373
|
else {
|
|
508
374
|
schemaValidation = `
|
|
509
375
|
|
|
510
|
-
##
|
|
376
|
+
## Schema Validation Passed
|
|
511
377
|
|
|
512
378
|
All fields used in this template exist in the "${targetCollection}" collection.
|
|
513
379
|
`;
|
|
@@ -521,7 +387,7 @@ All fields used in this template exist in the "${targetCollection}" collection.
|
|
|
521
387
|
// projectId provided but not authenticated
|
|
522
388
|
schemaValidation = `
|
|
523
389
|
|
|
524
|
-
##
|
|
390
|
+
## Schema Validation Skipped
|
|
525
391
|
|
|
526
392
|
A projectId was provided but you're not authenticated.
|
|
527
393
|
To validate against your project's schema, authenticate first using the device flow or FASTMODE_AUTH_TOKEN.
|
|
@@ -532,7 +398,7 @@ Without authentication, only syntax validation is performed.
|
|
|
532
398
|
// Build result
|
|
533
399
|
let output = '';
|
|
534
400
|
if (errors.length === 0 && warnings.length === 0) {
|
|
535
|
-
output =
|
|
401
|
+
output = `TEMPLATE VALID
|
|
536
402
|
|
|
537
403
|
The ${templateType} template structure looks correct.
|
|
538
404
|
|
|
@@ -549,7 +415,7 @@ ${suggestions.join('\n')}`;
|
|
|
549
415
|
}
|
|
550
416
|
}
|
|
551
417
|
else if (errors.length === 0) {
|
|
552
|
-
output =
|
|
418
|
+
output = `TEMPLATE VALID WITH WARNINGS
|
|
553
419
|
|
|
554
420
|
Warnings:
|
|
555
421
|
${warnings.join('\n')}`;
|
|
@@ -561,7 +427,7 @@ ${suggestions.join('\n')}`;
|
|
|
561
427
|
}
|
|
562
428
|
}
|
|
563
429
|
else {
|
|
564
|
-
output =
|
|
430
|
+
output = `TEMPLATE HAS ERRORS
|
|
565
431
|
|
|
566
432
|
Errors (must fix):
|
|
567
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": {
|