multisite-cms-mcp 1.5.5 → 1.5.7

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 CHANGED
@@ -118,6 +118,7 @@ const TOOLS = [
118
118
  'parent_context',
119
119
  'equality_comparison',
120
120
  'youtube_embed',
121
+ 'nested_collection_loop',
121
122
  ],
122
123
  description: 'The type of example to retrieve',
123
124
  },
@@ -133,8 +134,8 @@ const TOOLS = [
133
134
  properties: {
134
135
  section: {
135
136
  type: 'string',
136
- enum: ['full', 'analysis', 'structure', 'manifest', 'templates', 'tokens', 'forms', 'assets', 'checklist'],
137
- description: 'Specific section to retrieve, or "full" for everything',
137
+ enum: ['full', 'first_steps', 'analysis', 'structure', 'seo', 'manifest', 'templates', 'tokens', 'forms', 'assets', 'checklist'],
138
+ description: 'Specific section to retrieve, or "full" for everything. Use "first_steps" to get the required initial steps for establishing the target project.',
138
139
  },
139
140
  },
140
141
  required: [],
@@ -1,4 +1,4 @@
1
- type Section = 'full' | 'analysis' | 'structure' | 'seo' | 'manifest' | 'templates' | 'tokens' | 'forms' | 'assets' | 'checklist';
1
+ type Section = 'full' | 'first_steps' | 'analysis' | 'structure' | 'seo' | 'manifest' | 'templates' | 'tokens' | 'forms' | 'assets' | 'checklist';
2
2
  /**
3
3
  * Returns the conversion guide, either full or a specific section
4
4
  */
@@ -1 +1 @@
1
- {"version":3,"file":"get-conversion-guide.d.ts","sourceRoot":"","sources":["../../src/tools/get-conversion-guide.ts"],"names":[],"mappings":"AAAA,KAAK,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,WAAW,GAAG,KAAK,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;AAyuBlI;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAuD1E"}
1
+ {"version":3,"file":"get-conversion-guide.d.ts","sourceRoot":"","sources":["../../src/tools/get-conversion-guide.ts"],"names":[],"mappings":"AAAA,KAAK,OAAO,GAAG,MAAM,GAAG,aAAa,GAAG,UAAU,GAAG,WAAW,GAAG,KAAK,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;AA+2BlJ;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CA0D1E"}
@@ -2,6 +2,52 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getConversionGuide = getConversionGuide;
4
4
  const SECTIONS = {
5
+ first_steps: `# REQUIRED FIRST STEPS (Do These Before Building the Package)
6
+
7
+ **IMPORTANT:** Before converting any website, you MUST establish the target project first. This determines the schema and ensures you're deploying to the correct location.
8
+
9
+ ## Step 1: Check for Existing Projects
10
+
11
+ Call \`list_projects\` to see the user's existing Fast Mode projects.
12
+
13
+ ## Step 2: Ask the User
14
+
15
+ Based on what \`list_projects\` returns:
16
+
17
+ **If NO projects exist:**
18
+ - This is a new user - ask: "What would you like to name your new project?"
19
+ - Proceed to Step 3b (New Project)
20
+
21
+ **If projects exist:**
22
+ - Present the list and ask: "Is this website for one of your existing projects, or should I create a new one?"
23
+ - Let the user choose which project, or confirm they want a new one
24
+
25
+ ## Step 3a: For EXISTING Projects
26
+
27
+ 1. User selects the project from the list
28
+ 2. Call \`get_tenant_schema(projectId)\` to get the current collections and fields
29
+ 3. **Use this schema to build templates with the correct field names**
30
+ 4. When deploying, this will UPDATE the existing project
31
+ 5. Proceed to Phase 1 (Website Analysis)
32
+
33
+ ## Step 3b: For NEW Projects
34
+
35
+ 1. Ask for the project name if you don't have it
36
+ 2. Call \`create_site(name)\` to create the project
37
+ 3. You'll receive a projectId to use for deployment
38
+ 4. After building the package, call \`sync_schema\` to create collections/fields
39
+ 5. Proceed to Phase 1 (Website Analysis)
40
+
41
+ ---
42
+
43
+ **WHY THIS MATTERS:**
44
+ - For existing projects: The schema determines which fields to use in templates
45
+ - For new projects: You need the projectId before you can deploy
46
+ - Always: The user must confirm which project to use - never assume
47
+
48
+ ---
49
+
50
+ `,
5
51
  analysis: `# Phase 1: Website Analysis (MANDATORY)
6
52
 
7
53
  Before writing ANY code, complete this analysis:
@@ -415,6 +461,93 @@ Use for rich text content:
415
461
  - \`featured=true\` - Only featured (if collection has boolean "featured" field)
416
462
  - \`sort="field"\` - Sort by field
417
463
  - \`order="asc|desc"\` - Sort direction
464
+ - \`where="field:value"\` - Filter by field value
465
+
466
+ ## Nested Loops (Hierarchical Content)
467
+
468
+ For content hierarchies like categories with items, docs sections with pages, or any parent-child relationships.
469
+
470
+ **Works on ALL page types:** static pages, index pages, AND detail pages.
471
+
472
+ \`\`\`html
473
+ {{#each doc_categories}}
474
+ <div class="category-section">
475
+ <h3>{{name}}</h3>
476
+ <ul>
477
+ {{#each doc_pages where="category.slug:{{slug}}"}}
478
+ <li><a href="{{url}}">{{name}}</a></li>
479
+ {{/each}}
480
+ </ul>
481
+ </div>
482
+ {{/each}}
483
+ \`\`\`
484
+
485
+ **How it works:**
486
+ - The outer loop iterates through categories
487
+ - \`{{slug}}\` in the where clause references the current category's slug
488
+ - The inner loop filters doc_pages to only those matching that category
489
+
490
+ ---
491
+
492
+ ### Using @root. Prefix
493
+
494
+ Use \`@root.\` to access collections from the root context when you need to be explicit:
495
+
496
+ \`\`\`html
497
+ {{#each doc_categories}}
498
+ <div class="category-section">
499
+ <h3>{{name}}</h3>
500
+ <ul>
501
+ {{#each @root.doc_pages where="category.slug:{{slug}}"}}
502
+ <li><a href="{{url}}">{{name}}</a></li>
503
+ {{/each}}
504
+ </ul>
505
+ </div>
506
+ {{/each}}
507
+ \`\`\`
508
+
509
+ **When to use @root.:**
510
+ - When the inner collection name might be ambiguous
511
+ - When you want to be explicit about accessing root-level data
512
+ - Both \`{{#each collection}}\` and \`{{#each @root.collection}}\` work the same
513
+
514
+ ---
515
+
516
+ ### Common Patterns
517
+
518
+ \`\`\`html
519
+ <!-- Categories with posts -->
520
+ {{#each categories}}
521
+ <section>
522
+ <h2>{{name}}</h2>
523
+ {{#each posts where="category.slug:{{slug}}"}}
524
+ <article>{{name}}</article>
525
+ {{/each}}
526
+ </section>
527
+ {{/each}}
528
+
529
+ <!-- Authors with their articles -->
530
+ {{#each authors}}
531
+ <div class="author-section">
532
+ <h3>{{name}}</h3>
533
+ {{#each posts where="author.id:{{id}}"}}
534
+ <a href="{{url}}">{{name}}</a>
535
+ {{/each}}
536
+ </div>
537
+ {{/each}}
538
+
539
+ <!-- Documentation sidebar (on any page type) -->
540
+ {{#each doc_categories sort="order" order="asc"}}
541
+ <div class="sidebar-section">
542
+ <h4>{{name}}</h4>
543
+ {{#each @root.doc_pages where="category.slug:{{slug}}" sort="order" order="asc"}}
544
+ <a href="{{url}}">{{name}}</a>
545
+ {{/each}}
546
+ </div>
547
+ {{/each}}
548
+ \`\`\`
549
+
550
+ **Important:** The \`{{field}}\` in the where clause is resolved from the outer loop's current item.
418
551
 
419
552
  ## Conditionals
420
553
  \`\`\`html
@@ -741,6 +874,9 @@ async function getConversionGuide(section) {
741
874
  if (section === 'full') {
742
875
  return `# Complete Website Conversion Guide
743
876
 
877
+ ${SECTIONS.first_steps}
878
+ ---
879
+
744
880
  ${SECTIONS.analysis}
745
881
 
746
882
  ---
@@ -781,7 +917,7 @@ ${SECTIONS.checklist}
781
917
 
782
918
  Use these MCP tools during conversion:
783
919
 
784
- - \`get_schema\` - Get collection field reference and token syntax
920
+ - \`get_field_types\` - Get available field types for sync_schema
785
921
  - \`get_tenant_schema\` - Get exact collections and fields for a specific project
786
922
  - \`get_example\` - Get code examples for specific patterns
787
923
  - \`validate_manifest\` - Check your manifest.json
@@ -1,4 +1,4 @@
1
- type ExampleType = 'manifest_basic' | 'manifest_custom_paths' | 'manifest_minimal_with_ui' | 'blog_index_template' | 'blog_post_template' | 'team_template' | 'downloads_template' | 'authors_template' | 'author_detail_template' | 'custom_collection_template' | 'form_handling' | 'asset_paths' | 'image_handling' | 'relation_fields' | 'data_edit_keys' | 'each_loop' | 'conditional_if' | 'nested_fields' | 'featured_posts' | 'parent_context' | 'equality_comparison' | 'youtube_embed';
1
+ type ExampleType = 'manifest_basic' | 'manifest_custom_paths' | 'manifest_minimal_with_ui' | 'blog_index_template' | 'blog_post_template' | 'team_template' | 'downloads_template' | 'authors_template' | 'author_detail_template' | 'custom_collection_template' | 'form_handling' | 'asset_paths' | 'image_handling' | 'relation_fields' | 'data_edit_keys' | 'each_loop' | 'conditional_if' | 'nested_fields' | 'featured_posts' | 'parent_context' | 'equality_comparison' | 'youtube_embed' | 'nested_collection_loop';
2
2
  /**
3
3
  * Returns example code for a specific pattern
4
4
  */
@@ -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;AAiiCpB;;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,GACf,wBAAwB,CAAC;AAmqC7B;;GAEG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAE1E"}
@@ -1033,6 +1033,135 @@ https://www.youtube.com/watch?v=VIDEO_ID
1033
1033
  {{/if}}
1034
1034
  </div>
1035
1035
  \`\`\``,
1036
+ nested_collection_loop: `# Nested Collection Loops
1037
+
1038
+ For hierarchical content like documentation categories with pages, product categories with products, or authors with their posts.
1039
+
1040
+ **Works on ALL page types:** static pages, index pages, AND detail pages.
1041
+
1042
+ ## The Pattern
1043
+
1044
+ \`\`\`html
1045
+ {{#each outer_collection}}
1046
+ <div class="category-section">
1047
+ <h3>{{name}}</h3>
1048
+ <ul>
1049
+ {{#each inner_collection where="relation_field.slug:{{slug}}"}}
1050
+ <li><a href="{{url}}">{{name}}</a></li>
1051
+ {{/each}}
1052
+ </ul>
1053
+ </div>
1054
+ {{/each}}
1055
+ \`\`\`
1056
+
1057
+ **How it works:**
1058
+ - The outer loop iterates through parent items (e.g., categories)
1059
+ - \`{{slug}}\` in the \`where\` clause references the current outer item's slug
1060
+ - The inner loop filters to only items matching that parent
1061
+
1062
+ ---
1063
+
1064
+ ## Using @root. Prefix
1065
+
1066
+ Use \`@root.\` to explicitly access collections from the root context:
1067
+
1068
+ \`\`\`html
1069
+ {{#each doc_categories}}
1070
+ <h3>{{name}}</h3>
1071
+ {{#each @root.doc_pages where="category.slug:{{slug}}"}}
1072
+ <a href="{{url}}">{{name}}</a>
1073
+ {{/each}}
1074
+ {{/each}}
1075
+ \`\`\`
1076
+
1077
+ Both syntaxes work identically:
1078
+ - \`{{#each doc_pages}}\` - Standard syntax
1079
+ - \`{{#each @root.doc_pages}}\` - Explicit root reference
1080
+
1081
+ ---
1082
+
1083
+ ## Documentation Sidebar Example
1084
+
1085
+ **Collections needed:**
1086
+ - \`doc_categories\` (name, slug, order)
1087
+ - \`doc_pages\` (name, slug, content, category → relation to doc_categories)
1088
+
1089
+ \`\`\`html
1090
+ <nav class="docs-sidebar">
1091
+ {{#each doc_categories sort="order" order="asc"}}
1092
+ <div class="sidebar-section">
1093
+ <h4 class="section-title">{{name}}</h4>
1094
+ <ul class="section-links">
1095
+ {{#each @root.doc_pages where="category.slug:{{slug}}" sort="order" order="asc"}}
1096
+ <li>
1097
+ <a href="{{url}}" class="doc-link">{{name}}</a>
1098
+ </li>
1099
+ {{/each}}
1100
+ </ul>
1101
+ </div>
1102
+ {{/each}}
1103
+ </nav>
1104
+ \`\`\`
1105
+
1106
+ This sidebar pattern can be included on:
1107
+ - **Static pages** (about, contact, etc.)
1108
+ - **Index pages** (docs listing page)
1109
+ - **Detail pages** (individual doc pages)
1110
+
1111
+ ---
1112
+
1113
+ ## Blog Categories with Posts Example
1114
+
1115
+ \`\`\`html
1116
+ <section class="categorized-posts">
1117
+ {{#each categories}}
1118
+ <div class="category-group">
1119
+ <h2><a href="{{url}}">{{name}}</a></h2>
1120
+ <div class="posts-grid">
1121
+ {{#each posts where="category.id:{{id}}" limit=3 sort="publishedAt" order="desc"}}
1122
+ <article class="post-card">
1123
+ {{#if image}}
1124
+ <img src="{{image}}" alt="{{name}}">
1125
+ {{/if}}
1126
+ <h3><a href="{{url}}">{{name}}</a></h3>
1127
+ <p>{{summary}}</p>
1128
+ </article>
1129
+ {{/each}}
1130
+ </div>
1131
+ <a href="{{url}}" class="view-all">View all {{name}}</a>
1132
+ </div>
1133
+ {{/each}}
1134
+ </section>
1135
+ \`\`\`
1136
+
1137
+ ---
1138
+
1139
+ ## Important Notes
1140
+
1141
+ 1. **Works on ALL page types** - Static pages, index pages, AND detail pages
1142
+ 2. **The \`where\` clause uses parent context** - \`{{slug}}\` or \`{{id}}\` comes from the outer loop's current item
1143
+ 3. **@root. is optional** - Use it when you want to be explicit about root context
1144
+ 4. **Supports all modifiers** - \`limit\`, \`sort\`, \`order\` work on inner loops
1145
+ 5. **Relation field format** - Use \`relation_field.slug:{{slug}}\` or \`relation_field.id:{{id}}\`
1146
+
1147
+ ---
1148
+
1149
+ ## Manifest Configuration
1150
+
1151
+ \`\`\`json
1152
+ {
1153
+ "cmsTemplates": {
1154
+ "doc_categoriesIndex": "templates/docs_index.html",
1155
+ "doc_categoriesIndexPath": "/docs",
1156
+ "doc_pagesDetail": "templates/doc_page.html",
1157
+ "doc_pagesDetailPath": "/docs"
1158
+ }
1159
+ }
1160
+ \`\`\`
1161
+
1162
+ This gives you URLs like:
1163
+ - \`/docs\` - Documentation index showing all categories with their pages
1164
+ - \`/docs/quick-start-guide\` - Individual doc page`,
1036
1165
  };
1037
1166
  /**
1038
1167
  * Returns example code for a specific pattern
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Returns the list of field types that can be used when CREATING new fields
5
5
  * in collections. This is NOT the list of fields in your schema - use
6
- * get_schema or get_tenant_schema for that.
6
+ * get_tenant_schema for that.
7
7
  *
8
8
  * No authentication required.
9
9
  */
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Returns the list of field types that can be used when CREATING new fields
6
6
  * in collections. This is NOT the list of fields in your schema - use
7
- * get_schema or get_tenant_schema for that.
7
+ * get_tenant_schema for that.
8
8
  *
9
9
  * No authentication required.
10
10
  */
@@ -104,7 +104,7 @@ async function getFieldTypes() {
104
104
 
105
105
  These are the field types you can use when CREATING new fields with the \`sync_schema\` tool.
106
106
 
107
- **Note:** This is NOT the list of fields in your schema. Use \`get_schema\` for the generic schema or \`get_tenant_schema\` for a specific project's schema.
107
+ **Note:** This is NOT the list of fields in your schema. Use \`get_tenant_schema\` to see a specific project's schema.
108
108
 
109
109
  ## Field Types
110
110
 
@@ -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;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,CAobjB"}
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;AAmRrE;;;;;;;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,CA4djB"}
@@ -190,12 +190,15 @@ function validateForms(html) {
190
190
  }
191
191
  /**
192
192
  * Extract collection slugs referenced in {{#each}} loops on static pages
193
+ * Supports both {{#each collection}} and {{#each @root.collection}} syntax
193
194
  */
194
195
  function extractCollectionReferences(html) {
195
196
  const collections = [];
196
- const eachLoops = html.match(/\{\{#each\s+(\w+)[^}]*\}\}/g) || [];
197
+ // Match both {{#each collection}} and {{#each @root.collection}}
198
+ const eachLoops = html.match(/\{\{#each\s+(?:@root\.)?([\w]+)[^}]*\}\}/g) || [];
197
199
  for (const loop of eachLoops) {
198
- const match = loop.match(/\{\{#each\s+(\w+)/);
200
+ // Extract collection name, handling @root. prefix
201
+ const match = loop.match(/\{\{#each\s+(?:@root\.)?([\w]+)/);
199
202
  if (match) {
200
203
  collections.push(match[1]);
201
204
  }
@@ -218,7 +221,8 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
218
221
  // Extract all tokens from the HTML
219
222
  const doubleTokens = html.match(/\{\{([^{}#/]+)\}\}/g) || [];
220
223
  const tripleTokens = html.match(/\{\{\{([^{}]+)\}\}\}/g) || [];
221
- const eachLoops = html.match(/\{\{#each\s+(\w+)[^}]*\}\}/g) || [];
224
+ // Match both {{#each collection}} and {{#each @root.collection}}
225
+ const eachLoops = html.match(/\{\{#each\s+(?:@root\.)?([\w]+)[^}]*\}\}/g) || [];
222
226
  const conditionals = html.match(/\{\{#if\s+([^}]+)\}\}/g) || [];
223
227
  // Check for tokens with spaces (like {{ name }} instead of {{name}})
224
228
  const tokensWithSpaces = html.match(/\{\{\s+[^{}]+\s*\}\}/g) || [];
@@ -245,9 +249,9 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
245
249
  warnings.push(`- No {{#each}} loop found - index templates usually need a loop to display items`);
246
250
  }
247
251
  else {
248
- // Check if loop uses correct collection name
252
+ // Check if loop uses correct collection name (handles @root. prefix)
249
253
  for (const loop of eachLoops) {
250
- const match = loop.match(/\{\{#each\s+(\w+)/);
254
+ const match = loop.match(/\{\{#each\s+(?:@root\.)?([\w]+)/);
251
255
  if (match) {
252
256
  const usedCollection = match[1];
253
257
  if (collectionSlug && usedCollection !== collectionSlug) {
@@ -339,6 +343,37 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
339
343
  suggestions.push(`- Found ${parentRefs.length} parent context reference(s) ({{../fieldName}}) - accesses parent scope in loops`);
340
344
  }
341
345
  }
346
+ // Validate nested loops with parent context in where clause
347
+ // Supports both {{#each collection}} and {{#each @root.collection}}
348
+ const nestedLoopPattern = /\{\{#each\s+(?:@root\.)?(\w+)\s+[^}]*where="[^:]+:\{\{(\w+)\}\}"[^}]*\}\}/g;
349
+ const nestedLoops = html.match(nestedLoopPattern) || [];
350
+ if (nestedLoops.length > 0) {
351
+ suggestions.push(`- Found ${nestedLoops.length} nested loop(s) with parent context filtering (e.g., where="field:{{parentField}}")`);
352
+ // Check if there's an outer loop to provide context
353
+ // Match both {{#each collection}} and {{#each @root.collection}}
354
+ const allLoopMatches = html.match(/\{\{#each\s+(?:@root\.)?\w+/g) || [];
355
+ if (allLoopMatches.length < 2 && nestedLoops.length > 0) {
356
+ warnings.push(`- Nested loop with where="...{{field}}" found but no outer loop detected. The {{field}} needs to come from an outer loop's item.`);
357
+ }
358
+ }
359
+ // Detect common nested loop patterns
360
+ const hierarchicalPatterns = [
361
+ { outer: 'categories', inner: 'posts', relation: 'category' },
362
+ { outer: 'doc_categories', inner: 'doc_pages', relation: 'category' },
363
+ { outer: 'authors', inner: 'posts', relation: 'author' },
364
+ { outer: 'tags', inner: 'posts', relation: 'tags' },
365
+ ];
366
+ for (const pattern of hierarchicalPatterns) {
367
+ const outerMatch = new RegExp(`\\{\\{#each\\s+${pattern.outer}`, 'g').test(html);
368
+ const innerMatch = new RegExp(`\\{\\{#each\\s+${pattern.inner}`, 'g').test(html);
369
+ if (outerMatch && innerMatch) {
370
+ // Check if inner loop has proper where clause
371
+ const innerWithWhere = new RegExp(`\\{\\{#each\\s+${pattern.inner}\\s+[^}]*where=`, 'g').test(html);
372
+ if (!innerWithWhere) {
373
+ warnings.push(`- Nested loops: ${pattern.outer} → ${pattern.inner} detected. Consider adding where="${pattern.relation}.slug:{{slug}}" to filter ${pattern.inner} by parent ${pattern.outer}.`);
374
+ }
375
+ }
376
+ }
342
377
  // Validate equality helper syntax
343
378
  const eqHelpers = html.match(/\{\{#if\s+\(eq\s+[^)]+\)\s*\}\}/g) || [];
344
379
  if (eqHelpers.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multisite-cms-mcp",
3
- "version": "1.5.5",
3
+ "version": "1.5.7",
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": {