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.
@@ -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('manifest.json found');
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('pages/ folder found');
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('public/ folder found');
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.startsWith('')) {
37
+ if (manifestResult.includes('INVALID')) {
40
38
  errors.push('- Manifest validation failed (see details below)');
41
39
  }
42
- else if (manifestResult.startsWith('⚠️')) {
40
+ else if (manifestResult.includes('WARNINGS')) {
43
41
  warnings.push('- Manifest has warnings (see details below)');
44
42
  }
45
43
  else {
46
- validations.push('manifest.json is valid');
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 templateFields = ['blogIndex', 'blogPost', 'team', 'downloads', 'authorsIndex', 'authorDetail'];
71
- for (const field of templateFields) {
72
- const templateFile = manifest.cmsTemplates[field];
73
- if (templateFile) {
74
- const fileExists = fileList.some(f => f === templateFile || f.endsWith('/' + templateFile));
75
- if (!fileExists) {
76
- errors.push(`- Template file not found: ${templateFile} (cmsTemplates.${field})`);
77
- }
78
- else {
79
- validations.push(`✓ ${field} template exists`);
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
- if (path.includes('blog_index') || path.includes('insights_index') || path.includes('news_index')) {
90
- templateType = 'blog_index';
94
+ // Check if it's an index template
95
+ if (path.includes('_index') || path.includes('-index')) {
96
+ templateType = 'custom_index';
91
97
  }
92
- else if (path.includes('blog_post') || path.includes('insights_detail') || path.includes('news_detail')) {
93
- templateType = 'blog_post';
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
- else if (path.includes('team')) {
96
- templateType = 'team';
97
- }
98
- else if (path.includes('download')) {
99
- templateType = 'downloads';
100
- }
101
- else if (path.includes('author') && path.includes('index')) {
102
- templateType = 'authors_index';
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.startsWith('')) {
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.startsWith('⚠️')) {
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 = `✅ PACKAGE STRUCTURE VALID
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 = `❌ PACKAGE HAS ERRORS
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 = 'blog_index' | 'blog_post' | 'team' | 'downloads' | 'authors_index' | 'author_detail' | 'custom_index' | 'custom_detail' | 'static_page';
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,YAAY,GAAG,WAAW,GAAG,MAAM,GAAG,WAAW,GAAG,eAAe,GAAG,eAAe,GAAG,cAAc,GAAG,eAAe,GAAG,aAAa,CAAC;AA6O7J;;;;;;;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,CAubjB"}
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
- const BLOG_FIELDS = [
6
- { name: 'name', isRichText: false, isRequired: true },
7
- { name: 'slug', isRichText: false, isRequired: true },
8
- { name: 'mainImage', isRichText: false },
9
- { name: 'thumbnailImage', isRichText: false },
10
- { name: 'postSummary', isRichText: false },
11
- { name: 'postBody', isRichText: true },
12
- { name: 'featured', isRichText: false },
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, isBuiltin, schema) {
70
+ function findMissingFields(usedFields, collectionType, schema) {
139
71
  const missing = [];
140
- if (isBuiltin) {
141
- // Check against builtin standard fields + custom fields
142
- const standardFields = BUILTIN_STANDARD_FIELDS[collectionType] || [];
143
- const customFields = schema.builtinFields[collectionType]?.map(f => f.slug) || [];
144
- const allFields = [...standardFields, ...customFields];
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 (!allFields.includes(field)) {
79
+ if (!collectionFields.includes(field)) {
147
80
  missing.push(field);
148
81
  }
149
82
  }
150
83
  }
151
84
  else {
152
- // Check against custom collection fields
153
- const collection = schema.collections.find(c => c.slug === collectionType);
154
- if (collection) {
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
- // Get expected fields based on template type
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 mistakes
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 mistakes
131
+ // Check against common suggestions
230
132
  const baseName = fieldName.split('.')[0];
231
- if (COMMON_MISTAKES[baseName]) {
232
- errors.push(`- Wrong token: {{${fieldName}}} → Use ${COMMON_MISTAKES[baseName]}`);
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.endsWith('_index') || templateType === 'team' || templateType === 'downloads') {
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 (collectionName && usedCollection !== collectionName) {
264
- warnings.push(`- Loop uses '${usedCollection}' but expected '${collectionName}'`);
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
- // Determine which collection we're validating
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 custom collection exists
409
- const collectionExists = isBuiltin || schema.collections.some(c => c.slug === targetCollection);
410
- if (!isBuiltin && !collectionExists) {
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
- ## 🚨 ACTION REQUIRED: Collection Does Not Exist
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
- ### YOU MUST CREATE THIS COLLECTION
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, isBuiltin, schema);
324
+ const missingFields = findMissingFields(usedFields, targetCollection, schema);
460
325
  if (missingFields.length > 0) {
461
326
  schemaValidation = `
462
327
 
463
- ## 🚨 ACTION REQUIRED: Missing Fields
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
- ### YOU MUST CREATE THESE FIELDS
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": ${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
- ## Schema Validation Passed
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
- ## ℹ️ Schema Validation Skipped
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 = `✅ TEMPLATE VALID
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 = `⚠️ TEMPLATE VALID WITH WARNINGS
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 = `❌ TEMPLATE HAS ERRORS
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.2.4",
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": {