multisite-cms-mcp 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"get-example.d.ts","sourceRoot":"","sources":["../../src/tools/get-example.ts"],"names":[],"mappings":"AAAA,KAAK,WAAW,GACZ,gBAAgB,GAChB,uBAAuB,GACvB,0BAA0B,GAC1B,qBAAqB,GACrB,oBAAoB,GACpB,eAAe,GACf,oBAAoB,GACpB,kBAAkB,GAClB,wBAAwB,GACxB,4BAA4B,GAC5B,eAAe,GACf,aAAa,GACb,gBAAgB,GAChB,iBAAiB,GACjB,gBAAgB,GAChB,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,qBAAqB,GACrB,eAAe,CAAC;AAmgCpB;;GAEG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAE1E"}
1
+ {"version":3,"file":"get-example.d.ts","sourceRoot":"","sources":["../../src/tools/get-example.ts"],"names":[],"mappings":"AAAA,KAAK,WAAW,GACZ,gBAAgB,GAChB,uBAAuB,GACvB,0BAA0B,GAC1B,qBAAqB,GACrB,oBAAoB,GACpB,eAAe,GACf,oBAAoB,GACpB,kBAAkB,GAClB,wBAAwB,GACxB,4BAA4B,GAC5B,eAAe,GACf,aAAa,GACb,gBAAgB,GAChB,iBAAiB,GACjB,gBAAgB,GAChB,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,qBAAqB,GACrB,eAAe,CAAC;AAiiCpB;;GAEG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAE1E"}
@@ -458,10 +458,10 @@ For any user-defined collection. All collections have built-in \`name\`, \`slug\
458
458
  \`\`\``,
459
459
  form_handling: `# Form Handling
460
460
 
461
- Forms are automatically captured by the CMS:
461
+ Forms are automatically captured by the CMS using the \`data-form\` attribute:
462
462
 
463
463
  \`\`\`html
464
- <form data-form-name="contact" class="contact-form">
464
+ <form data-form="contact" class="contact-form">
465
465
  <div class="form-group">
466
466
  <label for="name">Name</label>
467
467
  <input type="text" name="name" id="name" required>
@@ -483,34 +483,64 @@ Forms are automatically captured by the CMS:
483
483
 
484
484
  ## Form Handler Script
485
485
 
486
+ Add this script to handle form submissions:
487
+
486
488
  \`\`\`javascript
487
- document.querySelectorAll('form[data-form-name]').forEach(form => {
489
+ // Handle all forms with data-form attribute
490
+ document.querySelectorAll('form[data-form]').forEach(form => {
488
491
  form.addEventListener('submit', async (e) => {
489
492
  e.preventDefault();
490
493
 
491
- const formName = form.dataset.formName || 'general';
492
- const formData = new FormData(form);
493
- const data = Object.fromEntries(formData);
494
+ const submitBtn = form.querySelector('button[type="submit"]');
495
+ const originalText = submitBtn?.textContent || 'Submit';
494
496
 
495
- // IMPORTANT: Endpoint is /_forms/{formName}
496
- const response = await fetch('/_forms/' + formName, {
497
- method: 'POST',
498
- headers: { 'Content-Type': 'application/json' },
499
- body: JSON.stringify(data)
500
- });
497
+ if (submitBtn) {
498
+ submitBtn.disabled = true;
499
+ submitBtn.textContent = 'Sending...';
500
+ }
501
501
 
502
- if (response.ok) {
503
- form.reset();
504
- alert(form.dataset.successMessage || 'Thank you!');
502
+ try {
503
+ const formName = form.dataset.form || 'general';
504
+ const formData = new FormData(form);
505
+ const data = Object.fromEntries(formData);
506
+
507
+ // Endpoint is /_forms/{formName}
508
+ const response = await fetch('/_forms/' + formName, {
509
+ method: 'POST',
510
+ headers: { 'Content-Type': 'application/json' },
511
+ body: JSON.stringify(data)
512
+ });
513
+
514
+ if (response.ok) {
515
+ // Option 1: Redirect to thank you page
516
+ // window.location.href = '/thank-you';
517
+
518
+ // Option 2: Show success message
519
+ form.reset();
520
+ alert(form.dataset.successMessage || 'Thank you! Your message has been sent.');
521
+ } else {
522
+ throw new Error('Form submission failed');
523
+ }
524
+ } catch (error) {
525
+ console.error('Form error:', error);
526
+ alert('There was an error. Please try again.');
527
+ } finally {
528
+ if (submitBtn) {
529
+ submitBtn.disabled = false;
530
+ submitBtn.textContent = originalText;
531
+ }
505
532
  }
506
533
  });
507
534
  });
508
535
  \`\`\`
509
536
 
510
537
  **Key points:**
511
- - Add \`data-form-name="xxx"\` to identify the form
512
- - Endpoint is \`/_forms/{formName}\` (NOT \`/api/forms/submit\`)
513
- - All \`name\` attributes are captured as fields`,
538
+ - Add \`data-form="formname"\` to identify the form (e.g., \`data-form="contact"\`)
539
+ - Endpoint is \`/_forms/{formName}\` (e.g., \`/_forms/contact\`)
540
+ - All inputs must have \`name\` attributes to be captured
541
+ - Add a submit button for the form to work
542
+
543
+ **Note:** The legacy \`data-form-name\` attribute is deprecated. Use \`data-form\` instead.`,
514
544
  asset_paths: `# Asset Path Rules
515
545
 
516
546
  **ALL asset paths must use /public/ prefix:**
@@ -1 +1 @@
1
- {"version":3,"file":"validate-manifest.d.ts","sourceRoot":"","sources":["../../src/tools/validate-manifest.ts"],"names":[],"mappings":"AAmBA;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkO5E"}
1
+ {"version":3,"file":"validate-manifest.d.ts","sourceRoot":"","sources":["../../src/tools/validate-manifest.ts"],"names":[],"mappings":"AAmBA;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2N5E"}
@@ -116,37 +116,34 @@ Tip: Use a JSON validator (like jsonlint.com) to find the exact error location.`
116
116
  }
117
117
  }
118
118
  }
119
- // Check cmsTemplates
119
+ // Check cmsTemplates - enforce strict pattern-based validation
120
120
  const templates = m.cmsTemplates;
121
121
  const customCollections = new Set();
122
122
  if (templates) {
123
- // Detect and validate collection templates (flat format)
124
- // Pattern: {slug}Index, {slug}Detail, {slug}IndexPath, {slug}DetailPath
125
- const allKeys = Object.keys(templates);
126
- for (const key of allKeys) {
127
- // Skip deprecated keys
128
- if (['collectionPaths', 'collectionTemplates'].includes(key)) {
123
+ // All keys must match the unified format: {slug}Index, {slug}Detail, {slug}IndexPath, {slug}DetailPath
124
+ const VALID_KEY_PATTERN = /^[a-z][a-zA-Z0-9]*(Index|Detail|IndexPath|DetailPath)$/;
125
+ const KEY_PARTS_PATTERN = /^([a-z][a-zA-Z0-9]*)(Index|Detail|IndexPath|DetailPath)$/;
126
+ for (const key of Object.keys(templates)) {
127
+ if (!VALID_KEY_PATTERN.test(key)) {
128
+ // Provide helpful error message with suggestion
129
+ let suggestion = '';
130
+ if (key.toLowerCase().includes('post')) {
131
+ suggestion = ` Did you mean "${key.replace(/[Pp]ost/g, 'Detail')}"?`;
132
+ }
133
+ else if (!key.includes('Index') && !key.includes('Detail')) {
134
+ suggestion = ` Did you mean "${key}Index" or "${key}Detail"?`;
135
+ }
136
+ errors.push(`- Invalid cmsTemplates key "${key}". ` +
137
+ `All keys must use format: {slug}Index, {slug}Detail, {slug}IndexPath, or {slug}DetailPath.${suggestion}`);
129
138
  continue;
130
139
  }
131
- // Extract collection slug from key pattern
132
- let slug = null;
133
- if (key.endsWith('Index') && !key.endsWith('IndexPath')) {
134
- slug = key.slice(0, -5); // Remove 'Index'
135
- }
136
- else if (key.endsWith('Detail') && !key.endsWith('DetailPath')) {
137
- slug = key.slice(0, -6); // Remove 'Detail'
138
- }
139
- else if (key.endsWith('IndexPath')) {
140
- slug = key.slice(0, -9); // Remove 'IndexPath'
141
- }
142
- else if (key.endsWith('DetailPath')) {
143
- slug = key.slice(0, -10); // Remove 'DetailPath'
144
- }
145
- if (slug && slug.length > 0) {
146
- customCollections.add(slug);
140
+ // Extract collection slug from valid keys
141
+ const match = key.match(KEY_PARTS_PATTERN);
142
+ if (match) {
143
+ customCollections.add(match[1]);
147
144
  }
148
145
  }
149
- // Validate each collection
146
+ // Validate each detected collection
150
147
  for (const slug of customCollections) {
151
148
  const indexTemplate = templates[`${slug}Index`];
152
149
  const detailTemplate = templates[`${slug}Detail`];
@@ -183,12 +180,6 @@ Tip: Use a JSON validator (like jsonlint.com) to find the exact error location.`
183
180
  if (detailPath && !detailTemplate) {
184
181
  warnings.push(`- Collection "${slug}": has ${slug}DetailPath but no ${slug}Detail template`);
185
182
  }
186
- // Note: indexPath and detailPath can be the same (e.g., both "/videos")
187
- // The system distinguishes by whether a slug segment exists after the path
188
- }
189
- // Warn about deprecated collectionTemplates format
190
- if (templates.collectionTemplates) {
191
- warnings.push('- cmsTemplates.collectionTemplates: This nested format is deprecated. Use flat format instead: {slug}Index, {slug}Detail, {slug}IndexPath, {slug}DetailPath');
192
183
  }
193
184
  }
194
185
  // Build result
@@ -1 +1 @@
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"}
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;AAgRrE;;;;;;;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,CAqbjB"}
@@ -97,6 +97,111 @@ function getRichTextFields(collectionSlug, schema) {
97
97
  return [];
98
98
  return collection.fields.filter(f => f.type === 'richText').map(f => f.slug);
99
99
  }
100
+ /**
101
+ * Validate forms in HTML templates
102
+ * Checks for proper data-form attribute, input names, and submit buttons
103
+ */
104
+ function validateForms(html) {
105
+ const errors = [];
106
+ const warnings = [];
107
+ const suggestions = [];
108
+ // Find all form elements
109
+ const formPattern = /<form[^>]*>/gi;
110
+ const forms = html.match(formPattern) || [];
111
+ if (forms.length === 0) {
112
+ return { errors, warnings, suggestions };
113
+ }
114
+ // Track forms for validation
115
+ let formsWithDataForm = 0;
116
+ let formsWithLegacyAttr = 0;
117
+ for (const formTag of forms) {
118
+ // Check for data-form attribute (correct format)
119
+ const hasDataForm = /data-form=["'][^"']+["']/i.test(formTag);
120
+ // Check for legacy data-form-name attribute
121
+ const hasLegacyDataFormName = /data-form-name=["'][^"']+["']/i.test(formTag);
122
+ if (hasDataForm) {
123
+ formsWithDataForm++;
124
+ }
125
+ else if (hasLegacyDataFormName) {
126
+ formsWithLegacyAttr++;
127
+ warnings.push(`- Form uses deprecated data-form-name attribute. Migrate to data-form="formname" for consistency.`);
128
+ }
129
+ else {
130
+ // Form without any data-form attribute - check if it looks like a CMS form
131
+ // Don't error on forms that might be external (like search forms)
132
+ if (!formTag.includes('action=')) {
133
+ errors.push(`- Form is missing data-form attribute. Add data-form="formname" to identify the form for CMS submission.`);
134
+ }
135
+ }
136
+ }
137
+ // Extract form content for deeper validation
138
+ // Find each form block
139
+ const formBlocks = html.match(/<form[^>]*data-form[^>]*>[\s\S]*?<\/form>/gi) || [];
140
+ for (const formBlock of formBlocks) {
141
+ // Check for inputs with name attributes
142
+ const inputs = formBlock.match(/<input[^>]*>/gi) || [];
143
+ const textareas = formBlock.match(/<textarea[^>]*>/gi) || [];
144
+ const selects = formBlock.match(/<select[^>]*>/gi) || [];
145
+ const allInputElements = [...inputs, ...textareas, ...selects];
146
+ let inputsWithName = 0;
147
+ let inputsWithoutName = 0;
148
+ for (const input of allInputElements) {
149
+ // Skip hidden inputs and submit buttons
150
+ if (/type=["'](?:submit|button|hidden)["']/i.test(input)) {
151
+ continue;
152
+ }
153
+ if (/name=["'][^"']+["']/i.test(input)) {
154
+ inputsWithName++;
155
+ }
156
+ else {
157
+ inputsWithoutName++;
158
+ }
159
+ }
160
+ if (inputsWithoutName > 0) {
161
+ errors.push(`- Found ${inputsWithoutName} form input(s) without name attribute. All inputs must have name="fieldname" to be captured.`);
162
+ }
163
+ if (inputsWithName === 0 && allInputElements.length > 0) {
164
+ errors.push(`- Form has no inputs with name attributes - no data will be captured.`);
165
+ }
166
+ // Check for submit button
167
+ const hasSubmitButton = /<button[^>]*type=["']submit["'][^>]*>/i.test(formBlock) ||
168
+ /<input[^>]*type=["']submit["'][^>]*>/i.test(formBlock) ||
169
+ /<button[^>]*>(?!.*type=["']button["'])/i.test(formBlock); // button without type defaults to submit
170
+ if (!hasSubmitButton) {
171
+ warnings.push(`- Form may be missing a submit button. Add <button type="submit">Submit</button> for the form to work.`);
172
+ }
173
+ // Check for form handler script
174
+ const hasFormHandlerScript = html.includes("form[data-form]") || html.includes("form[data-form-name]");
175
+ if (!hasFormHandlerScript) {
176
+ suggestions.push(`- No form handler script detected. Make sure to include JavaScript that handles form submission to /_forms/{formName}`);
177
+ }
178
+ }
179
+ // Summary suggestion
180
+ if (formsWithDataForm > 0) {
181
+ suggestions.push(`- Found ${formsWithDataForm} form(s) with data-form attribute - forms will submit to /_forms/{formName}`);
182
+ }
183
+ // Check for thank-you page redirect pattern
184
+ const hasThankYouRedirect = /window\.location\.href\s*=\s*['"]\/thank-you['"]/i.test(html) ||
185
+ /window\.location\s*=\s*['"]\/thank-you['"]/i.test(html);
186
+ if (formsWithDataForm > 0 && !hasThankYouRedirect) {
187
+ suggestions.push(`- Consider adding a /thank-you page and redirecting there on successful submission for better UX`);
188
+ }
189
+ return { errors, warnings, suggestions };
190
+ }
191
+ /**
192
+ * Extract collection slugs referenced in {{#each}} loops on static pages
193
+ */
194
+ function extractCollectionReferences(html) {
195
+ const collections = [];
196
+ const eachLoops = html.match(/\{\{#each\s+(\w+)[^}]*\}\}/g) || [];
197
+ for (const loop of eachLoops) {
198
+ const match = loop.match(/\{\{#each\s+(\w+)/);
199
+ if (match) {
200
+ collections.push(match[1]);
201
+ }
202
+ }
203
+ return [...new Set(collections)]; // Unique collections
204
+ }
100
205
  /**
101
206
  * Validates an HTML template for correct CMS token usage
102
207
  *
@@ -204,6 +309,11 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
204
309
  }
205
310
  }
206
311
  }
312
+ // ============ Form Validation ============
313
+ const formValidation = validateForms(html);
314
+ errors.push(...formValidation.errors);
315
+ warnings.push(...formValidation.warnings);
316
+ suggestions.push(...formValidation.suggestions);
207
317
  // Validate site tokens
208
318
  const siteTokens = html.match(/\{\{site\.(\w+)\}\}/g) || [];
209
319
  for (const token of siteTokens) {
@@ -253,6 +363,60 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
253
363
  if (resolved) {
254
364
  const schema = await fetchTenantSchema(resolved.tenantId);
255
365
  if (schema) {
366
+ // For static pages, validate any collection references in {{#each}} loops
367
+ if (templateType === 'static_page') {
368
+ const referencedCollections = extractCollectionReferences(html);
369
+ if (referencedCollections.length > 0) {
370
+ suggestions.push(`- Found ${referencedCollections.length} collection reference(s) in static page: ${referencedCollections.join(', ')}`);
371
+ // Check each referenced collection exists
372
+ const missingCollections = [];
373
+ const existingCollections = [];
374
+ for (const collSlug of referencedCollections) {
375
+ const exists = schema.collections.some(c => c.slug === collSlug);
376
+ if (exists) {
377
+ existingCollections.push(collSlug);
378
+ }
379
+ else {
380
+ missingCollections.push(collSlug);
381
+ }
382
+ }
383
+ if (missingCollections.length > 0) {
384
+ schemaValidation += `
385
+
386
+ ## ACTION REQUIRED: Missing Collections
387
+
388
+ The following collections are referenced in this static page but **do not exist**:
389
+
390
+ ${missingCollections.map(c => `- \`${c}\``).join('\n')}
391
+
392
+ You must create these collections using \`sync_schema\` before deployment.
393
+
394
+ **Example:**
395
+ \`\`\`json
396
+ {
397
+ "projectId": "${resolved.tenantId}",
398
+ "collections": [
399
+ ${missingCollections.map(c => ` {
400
+ "slug": "${c}",
401
+ "name": "${c.charAt(0).toUpperCase() + c.slice(1)}",
402
+ "nameSingular": "${c.endsWith('s') ? c.slice(0, -1).charAt(0).toUpperCase() + c.slice(0, -1).slice(1) : c}",
403
+ "fields": []
404
+ }`).join(',\n')}
405
+ ]
406
+ }
407
+ \`\`\`
408
+ `;
409
+ }
410
+ if (existingCollections.length > 0) {
411
+ schemaValidation += `
412
+
413
+ ## Static Page Collection References Validated
414
+
415
+ The following collections exist and can be used: ${existingCollections.map(c => `\`${c}\``).join(', ')}
416
+ `;
417
+ }
418
+ }
419
+ }
256
420
  const targetCollection = collectionSlug || '';
257
421
  if (targetCollection && templateType !== 'static_page') {
258
422
  // Get richText fields for triple brace validation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multisite-cms-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.5.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": {