@stellisoft/stellify-mcp 0.1.35 → 0.1.37

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/README.md CHANGED
@@ -374,12 +374,80 @@ Create a new UI element. Provide either `page` (route UUID) for root elements, o
374
374
  **Parameters:**
375
375
  - `type` (required): Element type - one of:
376
376
  - HTML5: `s-wrapper`, `s-input`, `s-form`, `s-svg`, `s-shape`, `s-media`, `s-iframe`
377
- - Components: `s-loop`, `s-transition`, `s-freestyle`, `s-motion`
377
+ - Components: `s-transition`, `s-freestyle`, `s-motion`
378
378
  - Blade: `s-directive`
379
379
  - Shadcn/ui: `s-chart`, `s-table`, `s-combobox`, `s-accordion`, `s-calendar`, `s-contiguous`
380
380
  - `page` (optional): UUID of the page/route (for root elements)
381
381
  - `parent` (optional): UUID of the parent element (for child elements)
382
382
 
383
+ **Using `s-directive` for Blade Conditionals:**
384
+
385
+ `s-directive` elements output Blade directives (like `@if`, `@foreach`, `@endif`). They are **sibling elements** — they don't wrap children. To conditionally render content:
386
+
387
+ 1. Create an `s-directive` element with a statement for the opening directive (e.g., `@if(...)`)
388
+ 2. Create the content element(s) as the **next sibling(s)**
389
+ 3. Create another `s-directive` element with a statement for the closing directive (e.g., `@endif`)
390
+
391
+ Example — conditionally showing an image:
392
+ ```
393
+ // 1. Create statement for @if
394
+ create_statement_with_code({
395
+ file: "<file-uuid>",
396
+ code: "@if($item->featured_image)"
397
+ })
398
+
399
+ // 2. Create opening directive element and set its statement
400
+ create_element({ type: "s-directive", page: "<route-uuid>" })
401
+ update_element({ uuid: "<if-directive-uuid>", data: { "statement": "<if-statement-uuid>" } })
402
+
403
+ // 3. Create the image as the next sibling
404
+ html_to_elements({ page: "<route-uuid>", elements: "<img class=\"w-full\" />" })
405
+ // Then update with dynamic src:
406
+ update_element({ uuid: "<img-uuid>", data: { "srcField": "featured_image" } })
407
+
408
+ // 4. Create statement for @endif
409
+ create_statement_with_code({ file: "<file-uuid>", code: "@endif" })
410
+
411
+ // 5. Create closing directive element
412
+ create_element({ type: "s-directive", page: "<route-uuid>" })
413
+ update_element({ uuid: "<endif-directive-uuid>", data: { "statement": "<endif-statement-uuid>" } })
414
+ ```
415
+
416
+ The three elements render in order as siblings:
417
+ ```blade
418
+ @if($item->featured_image)
419
+ <img class="w-full" src="{{ $item->featured_image }}" />
420
+ @endif
421
+ ```
422
+
423
+ **Using `s-directive` for Loops:**
424
+
425
+ ```
426
+ // 1. Create @foreach directive
427
+ create_statement_with_code({ file: "<file-uuid>", code: "@foreach($posts as $item)" })
428
+ create_element({ type: "s-directive", page: "<route-uuid>" })
429
+ update_element({ uuid: "<foreach-uuid>", data: { "statement": "<foreach-statement-uuid>" } })
430
+
431
+ // 2. Create loop content (article with dynamic fields)
432
+ html_to_elements({ page: "<route-uuid>", elements: "<article><h2></h2><p></p></article>" })
433
+ // Update elements to use loop item fields:
434
+ update_element({ uuid: "<h2-uuid>", data: { "textField": "title" } }) // → {{ $item->title }}
435
+ update_element({ uuid: "<p-uuid>", data: { "textField": "excerpt" } }) // → {{ $item->excerpt }}
436
+
437
+ // 3. Create @endforeach directive
438
+ create_statement_with_code({ file: "<file-uuid>", code: "@endforeach" })
439
+ create_element({ type: "s-directive", page: "<route-uuid>" })
440
+ update_element({ uuid: "<endforeach-uuid>", data: { "statement": "<endforeach-statement-uuid>" } })
441
+ ```
442
+
443
+ **Loop Item Attributes:**
444
+ Inside `@foreach` loops, use these attributes on elements to reference `$item`:
445
+ - `textField: "fieldName"` → outputs `{{ $item->fieldName }}`
446
+ - `hrefField: "fieldName"` → outputs `href="{{ $item->fieldName }}"`
447
+ - `srcField: "fieldName"` → outputs `src="{{ $item->fieldName }}"`
448
+ - `hrefExpression: "{{ route('posts.show', $item->slug) }}"` → for complex expressions
449
+ - `srcExpression`, `altExpression` → same pattern for other attributes
450
+
383
451
  ---
384
452
 
385
453
  #### `update_element`
@@ -397,7 +465,18 @@ Update an existing UI element.
397
465
  - `locked`: Prevent editing (boolean)
398
466
  - `tag`: HTML tag (div, input, button, etc.)
399
467
  - `classes`: CSS classes array `["class1", "class2"]`
400
- - `text`: Element text content
468
+ - `text`: Static text content
469
+ - `statements`: Array of statement UUIDs for dynamic Blade content
470
+
471
+ **Loop item fields** (for elements inside `@foreach` loops, references `$item`):
472
+ - `textField`: Field name → outputs `{{ $item->fieldName }}`
473
+ - `hrefField`: Field name → outputs `href="{{ $item->fieldName }}"`
474
+ - `srcField`: Field name → outputs `src="{{ $item->fieldName }}"`
475
+
476
+ **Expression attributes** (for complex Blade expressions):
477
+ - `hrefExpression`: Full Blade expression for href (e.g., `"{{ route('posts.show', $item->slug) }}"`)
478
+ - `srcExpression`: Full Blade expression for src
479
+ - `altExpression`: Full Blade expression for alt
401
480
 
402
481
  **Event handlers** (set value to method UUID):
403
482
  - `click`: @click
@@ -480,11 +559,6 @@ html_to_elements(page: routeUUID, elements: "<main>...</main>")
480
559
  html_to_elements(page: routeUUID, elements: "<footer>...</footer>")
481
560
  ```
482
561
 
483
- **Alternative approach (wrap in a single container):**
484
- ```
485
- html_to_elements(page: routeUUID, elements: "<div class='min-h-screen flex flex-col'><header>...</header><main>...</main><footer>...</footer></div>")
486
- ```
487
-
488
562
  **Features:**
489
563
  - Parses HTML structure
490
564
  - Creates all elements with proper nesting
package/dist/index.js CHANGED
@@ -569,16 +569,15 @@ Use the returned UUID with html_to_elements (page parameter) or get_route for fu
569
569
  },
570
570
  {
571
571
  name: 'create_element',
572
- description: `Create a UI element. Provide page (route UUID) for root elements, or parent (element UUID) for children. Use s-loop for v-for elements.`,
572
+ description: `Create a UI element. Provide page (route UUID) for root elements, or parent (element UUID) for children.`,
573
573
  inputSchema: {
574
574
  type: 'object',
575
575
  properties: {
576
576
  type: {
577
577
  type: 'string',
578
578
  enum: [
579
- 's-wrapper', 's-input', 's-form', 's-svg', 's-shape', 's-media', 's-iframe',
580
- 's-loop', 's-transition', 's-freestyle', 's-motion',
581
- 's-directive'
579
+ 's-wrapper', 's-input', 's-form', 's-svg', 's-shape', 's-media',
580
+ 's-freestyle', 's-directive'
582
581
  ],
583
582
  description: 'Element type - must be one of the valid Stellify element types',
584
583
  },
@@ -596,7 +595,32 @@ Use the returned UUID with html_to_elements (page parameter) or get_route for fu
596
595
  },
597
596
  {
598
597
  name: 'update_element',
599
- description: `Update a UI element. Data object: tag, classes, text, event handlers (method UUIDs), classBindings. Set 'name' on root elements to create Blade views (e.g., name="notes.index" for view('notes.index')).`,
598
+ description: `Update a UI element. Data object: tag, classes, text, event handlers (method UUIDs), classBindings. Set 'name' on root elements to create Blade views (e.g., name="notes.index" for view('notes.index')).
599
+
600
+ **For elements inside @foreach loops (SSR/Blade):**
601
+ Use these attributes to reference the loop variable (defaults to \`$item\`):
602
+ - \`textField\`: Field name for text content → outputs \`{{ $item->fieldName }}\`
603
+ - \`hrefField\`: Field name for href → outputs \`href="{{ $item->fieldName }}"\` (field value ONLY, no prefix)
604
+ - \`srcField\`: Field name for src → outputs \`src="{{ $item->fieldName }}"\`
605
+
606
+ **For hrefs with path prefixes (IMPORTANT):**
607
+ \`hrefField\` outputs ONLY the field value. There is NO \`hrefPrefix\` attribute.
608
+ For links like \`/post/slug-here\`, you MUST use \`hrefExpression\`:
609
+ - \`hrefExpression: "/post/{{ $item->slug }}"\` → outputs \`href="/post/{{ $item->slug }}"\`
610
+ - \`hrefExpression: "/category/{{ $item->slug }}"\` → outputs \`href="/category/{{ $item->slug }}"\`
611
+
612
+ **For complex Blade expressions in attributes:**
613
+ Use expression attributes when you need more than simple field access:
614
+ - \`hrefExpression\`: Blade expression for href → outputs \`href="..."\` with the expression
615
+ - \`srcExpression\`: Blade expression for src → outputs \`src="..."\` with the expression
616
+ - \`altExpression\`: Blade expression for alt → outputs \`alt="..."\` with the expression
617
+
618
+ Examples:
619
+ - Path prefix: \`hrefExpression: "/post/{{ $item->slug }}"\`
620
+ - Route helper: \`hrefExpression: "{{ route('posts.show', $item->slug) }}"\`
621
+
622
+ **For Blade text content:**
623
+ Use the \`statements\` array with statement UUIDs containing Blade code. The statement's \`code\` property will be output directly for Blade to evaluate.`,
600
624
  inputSchema: {
601
625
  type: 'object',
602
626
  properties: {
@@ -718,12 +742,20 @@ When HTML contains multiple root-level elements (e.g., <header>, <main>, <footer
718
742
  **@click auto-wiring:** Pass 'file' UUID to auto-resolve @click="methodName" handlers. Methods must exist in the file first.
719
743
 
720
744
  **Blade Syntax Handling:**
721
- When element text or attributes contain Blade syntax like \`{{ $post->title }}\` or \`{!! $post->content !!}\`, do NOT store them as literal text. Instead:
722
- - Parse the Blade expression and create a statement/clause for it
723
- - Store the statement UUID in the element's \`statements\` array
724
- - The SSR assembler will evaluate these at render time
745
+ For SSR/Blade pages, do NOT pass raw Blade expressions in text or attributes. The HTML parser stores them literally which causes rendering issues. Instead:
746
+
747
+ 1. **For static HTML:** Pass clean HTML without Blade syntax, then use \`update_element\` to add dynamic behavior
748
+ 2. **For loop content:** After creating elements, use \`update_element\` with:
749
+ - \`textField\`, \`hrefField\`, \`srcField\` for simple field access (outputs \`{{ $item->field }}\`)
750
+ - \`hrefExpression\`, \`srcExpression\`, \`altExpression\` for paths with prefixes or complex expressions
751
+ - \`statements\` array with statement UUIDs for text content with Blade code
752
+ 3. **For conditionals:** Use \`s-directive\` elements as siblings (see update_element docs)
753
+
754
+ **IMPORTANT - Links with path prefixes:**
755
+ \`hrefField\` outputs ONLY the field value with no prefix. There is NO \`hrefPrefix\` attribute.
756
+ For links like \`/post/my-slug\`, use \`hrefExpression: "/post/{{ $item->slug }}"\` instead.
725
757
 
726
- For example, \`{{ $post->title }}\` should become a statement that references the \`$post\` variable and accesses its \`title\` property.
758
+ **Loop variable:** Inside \`@foreach\` loops created with \`s-directive\`, the default loop variable is \`$item\`. Use \`textField: "title"\` to output \`{{ $item->title }}\`.
727
759
 
728
760
  Prefer SVG icons over emoji (encoding issues).`,
729
761
  inputSchema: {
@@ -1477,7 +1509,7 @@ const SERVER_INSTRUCTIONS = `Stellify is a coding platform where code is stored
1477
1509
  // Create MCP server
1478
1510
  const server = new Server({
1479
1511
  name: 'stellify-mcp',
1480
- version: '0.1.35',
1512
+ version: '0.1.37',
1481
1513
  }, {
1482
1514
  capabilities: {
1483
1515
  tools: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stellisoft/stellify-mcp",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "mcpName": "io.github.MattStellisoft/stellify-mcp",
5
5
  "description": "MCP server for Stellify - AI-native code generation platform",
6
6
  "main": "dist/index.js",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/Stellify-Software-Ltd/stellify-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.1.35",
9
+ "version": "0.1.37",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@stellisoft/stellify-mcp",
14
- "version": "0.1.35",
14
+ "version": "0.1.37",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -12,10 +12,6 @@ You are importing a WordPress site into Stellify by reading its PHP theme files
12
12
 
13
13
  **Match the rendering approach of the WordPress site.** If content is server-rendered in WordPress (which most of it will be — posts, pages, archives, navigation), create Blade templates. If a feature uses JavaScript for client-side interactivity (AJAX forms, dynamic filtering, live search, infinite scroll, modals, sliders), create a Vue component for that specific piece. The default is Blade — only reach for Vue when the WordPress source is doing something that genuinely requires JS.
14
14
 
15
- ## Step 0 — Discover Tools
16
-
17
- List all available Stellify MCP tools. Understand what each tool does, what parameters it requires, and any dependencies between them (e.g. you need a file before you can add methods to it, you need a method before you can add statements). Print a summary of the tools and their creation order.
18
-
19
15
  ## Step 1 — Identify the Active Theme
20
16
 
21
17
  Look in `./wp-content/themes/` to find the active theme. If there are multiple themes, check for the most recently modified one, or the one with the most template files. If unsure, ask which theme to import.
@@ -60,60 +56,13 @@ Read functions.php thoroughly. Extract:
60
56
 
61
57
  ## Step 4 — Plan the Laravel Structure
62
58
 
63
- Before calling any Stellify tools, write out the full plan:
64
-
65
- ### Models
66
- - Post (always — WordPress core)
67
- - Page (always — WordPress core)
68
- - Category, Tag (if used)
69
- - Any custom post types found in Step 3
70
- - User (if the theme has author pages)
71
-
72
- ### Controllers
73
- - PostController (index, show) — handles blog listing and single posts
74
- - PageController (show) — handles static pages
75
- - One controller per custom post type
76
- - HomeController — if front-page.php or a static homepage exists
77
- - SearchController — if search.php exists
78
-
79
- ### Routes (web.php)
80
- Map the WordPress URL structure:
81
- - `/` → HomeController or PostController@index
82
- - `/blog` or `/posts` → PostController@index (if static homepage)
83
- - `/{post-slug}` → PostController@show
84
- - `/{page-slug}` → PageController@show
85
- - `/category/{slug}` → PostController@index (filtered)
86
- - `/tag/{slug}` → PostController@index (filtered)
87
- - `/search` → SearchController@index
88
- - Custom post type archives and singles
89
-
90
- ### Blade Views
91
- Everything is Blade. The directory structure should be:
92
- ```
93
- layouts/
94
- app.blade.php ← from header.php + footer.php
95
- posts/
96
- index.blade.php ← from archive.php or index.php
97
- show.blade.php ← from single.php
98
- pages/
99
- show.blade.php ← from page.php
100
- partials/
101
- nav.blade.php ← from wp_nav_menu output
102
- sidebar.blade.php ← from sidebar.php
103
- post-card.blade.php ← from template-parts/content.php
104
- comments.blade.php ← from comments.php
105
- search-form.blade.php ← from searchform.php
106
- errors/
107
- 404.blade.php ← from 404.php
108
- ```
109
-
110
- ### Migrations (Mode B only)
111
- If the user chose Mode B (fresh database), plan one migration per model with fields derived from:
112
- - WordPress core fields: title, slug, content, excerpt, featured_image, status, published_at
113
- - Custom fields from `get_post_meta()` calls found in templates
114
- - Any ACF or custom meta boxes registered in functions.php
59
+ Before calling any Stellify tools, plan:
115
60
 
116
- If the user chose Mode A (existing database), skip migrations entirely the tables already exist.
61
+ - **Models:** One per post type (Post, Page, plus any custom types from Step 3)
62
+ - **Controllers:** One per model with index/show actions
63
+ - **Routes:** Match WordPress URL structure (archive at `/`, singles at `/{slug}`, taxonomies at `/category/{slug}`)
64
+ - **Views:** Mirror WordPress template hierarchy — layout from header+footer, index/show views per post type, partials from template-parts
65
+ - **Migrations (Mode B only):** One per model with fields from WordPress core + any custom fields found in templates
117
66
 
118
67
  ## Step 5 — Build in Stellify (Order of Operations)
119
68
 
@@ -123,6 +72,17 @@ Execute the plan using Stellify MCP tools in this order. This order matters —
123
72
 
124
73
  Use the `create_resources` tool with `api: false` to generate models and controllers together. The `api: false` flag ensures controller methods return data arrays (for Blade views) rather than JSON responses.
125
74
 
75
+ **Route Model Binding with Slugs:** WordPress URLs use slugs (e.g., `/post/my-article-title`). After creating each model with a `slug` field, add a `getRouteKeyName()` method so Laravel's route model binding works correctly:
76
+
77
+ ```php
78
+ public function getRouteKeyName(): string
79
+ {
80
+ return 'slug';
81
+ }
82
+ ```
83
+
84
+ Add this method to **Post**, **Category**, **Tag**, and any custom post type models that use slugs in their URLs. Without this, routes like `/post/{post}` will try to look up by ID instead of slug, causing 404 errors.
85
+
126
86
  **If Mode A (connect to existing WordPress database):**
127
87
 
128
88
  Create resources that map to the existing WordPress tables. No migrations. Set the model's table and primary key to match WordPress.
@@ -132,7 +92,13 @@ Create resources that map to the existing WordPress tables. No migrations. Set t
132
92
  class Post extends Model {
133
93
  protected $table = 'wp_posts';
134
94
  protected $primaryKey = 'ID';
135
-
95
+
96
+ // Enable slug-based route model binding (WordPress uses post_name for slugs)
97
+ public function getRouteKeyName(): string
98
+ {
99
+ return 'post_name';
100
+ }
101
+
136
102
  // Scope to only published posts (not revisions, drafts, etc.)
137
103
  public function scopePublished($query) {
138
104
  return $query->where('post_status', 'publish')
@@ -161,6 +127,15 @@ In Blade templates, use WordPress column names:
161
127
 
162
128
  Create new resources with clean Laravel migrations for each model identified in Step 4. Use clean column names (`title`, `slug`, `content`, `excerpt`, `status`, `published_at`, etc.). After the import is complete, suggest a data migration SQL query or artisan command that maps data from the WordPress tables to the new schema.
163
129
 
130
+ For each model with a `slug` field, add `getRouteKeyName()`:
131
+
132
+ ```php
133
+ public function getRouteKeyName(): string
134
+ {
135
+ return 'slug';
136
+ }
137
+ ```
138
+
164
139
  ### 5b. Refine Controller Methods
165
140
 
166
141
  `create_resources` (Step 5a) scaffolds controllers automatically. Review and refine the generated methods to ensure they query the right data. Controller methods in Stellify return data arrays — Stellify automatically merges these into the Blade view context. For example:
@@ -211,52 +186,58 @@ Translate WordPress functions to Blade:
211
186
  - `language_attributes()` → `lang="{{ str_replace('_', '-', app()->getLocale()) }}"`
212
187
 
213
188
  ### 5e. Create Blade Views
214
- For each WordPress template file, read the PHP and create the equivalent Blade view. **All views use `@extends('layouts.app')` and `@section('content')`.**
189
+ For each WordPress template file, read the PHP and create the equivalent Blade view using `html_to_elements`. **All views use `@extends('layouts.app')` and `@section('content')`.**
190
+
191
+ **⚠️ Multiple Root Elements:** When converting HTML with multiple root-level elements (e.g., `<header>`, `<main>`, `<footer>`), only the first root element gets attached to the route. Make separate `html_to_elements` calls for each root element.
215
192
 
216
193
  **WordPress → Blade translation guide:**
217
194
 
218
- The Blade column depends on which database mode was chosen. Mode A uses WordPress column names directly; Mode B uses clean Laravel names.
219
-
220
- | WordPress | Blade (Mode A existing DB) | Blade (Mode B fresh DB) |
221
- |-----------|------------------------------|---------------------------|
222
- | `the_title()` | `{{ $post->post_title }}` | `{{ $post->title }}` |
223
- | `the_content()` | `{!! $post->post_content !!}` | `{!! $post->content !!}` |
224
- | `the_excerpt()` | `{{ $post->post_excerpt }}` | `{{ $post->excerpt }}` |
225
- | `the_permalink()` | `{{ route('posts.show', $post->post_name) }}` | `{{ route('posts.show', $post->slug) }}` |
226
- | `the_post_thumbnail()` | via `wp_postmeta` lookup | `<img src="{{ asset('storage/' . $post->featured_image) }}" />` |
227
- | `the_date()` / `the_time()` | `{{ \Carbon\Carbon::parse($post->post_date)->format('d M Y') }}` | `{{ $post->published_at->format('d M Y') }}` |
228
- | `the_author()` | `{{ $post->author->display_name }}` | `{{ $post->author->name }}` |
229
- | `comments_template()` | `@include('partials.comments', ['post' => $post])` | same |
230
- | `get_template_part('content')` | `@include('partials.post-card', ['post' => $post])` | same |
231
- | `have_posts() / the_post()` | Use an `s-loop` element (see below) | same |
232
- | `wp_link_pages()` | `{{ $posts->links() }}` | same |
233
- | `get_search_form()` | `@include('partials.search-form')` | same |
234
- | `wp_nav_menu()` | `@include('partials.nav')` | same |
235
- | `get_header()` / `get_footer()` | handled by `@extends('layouts.app')` | same |
236
- | `esc_html()` / `esc_attr()` | `{{ }}` (Blade auto-escapes) | same |
237
- | `wp_kses_post()` | `{!! !!}` (for trusted HTML content) | same |
238
-
239
- **Stellify `s-loop` elements for iteration:**
240
-
241
- WordPress's "The Loop" (`have_posts() / the_post()`) requires two things in Stellify:
242
-
243
- 1. **Set the element type to `s-loop`** using `update_element` with `type: "s-loop"` and `variable` set to the collection name from the controller's return array. This tells Stellify to iterate over that variable and pass each item as `$item` to child elements.
244
-
245
- 2. **Write the `@foreach` in the Blade template as well.** The Blade content inside the element still needs the `@foreach` loop.
246
-
247
- Example: If the controller returns `['posts' => Post::paginate(10)]`, the element that lists posts should be updated to:
248
- ```json
249
- {
250
- "uuid": "<element-uuid>",
251
- "data": {
252
- "type": "s-loop",
253
- "variable": "posts"
254
- }
255
- }
256
- ```
257
- And the Blade content inside uses `@foreach($posts as $post)` ... `@endforeach` as normal.
195
+ Mode A uses WordPress column names; Mode B uses clean Laravel names.
196
+
197
+ **IMPORTANT:** Inside `@foreach` loops, use `$item` as the loop variable. This matches the Stellify assembler's expectations for `textField`, `hrefField`, `srcField` attributes.
198
+
199
+ | WordPress | Blade (Mode A) | Blade (Mode B) | Stellify Attribute |
200
+ |-----------|----------------|----------------|-------------------|
201
+ | `the_title()` | `{{ $item->post_title }}` | `{{ $item->title }}` | `textField: "title"` |
202
+ | `the_content()` | `{!! $item->post_content !!}` | `{!! $item->content !!}` | statement with code |
203
+ | `the_excerpt()` | `{{ $item->post_excerpt }}` | `{{ $item->excerpt }}` | `textField: "excerpt"` |
204
+ | `the_permalink()` | `{{ route('posts.show', $item->post_name) }}` | `{{ route('posts.show', $item->slug) }}` | `hrefExpression: "..."` |
205
+ | `the_date()` | `{{ $item->post_date->format('d M Y') }}` | `{{ $item->published_at->format('d M Y') }}` | statement with code |
206
+ | `the_author()` | `{{ $item->author->display_name }}` | `{{ $item->author->name }}` | statement with code |
207
+ | `get_template_part()` | `@include('partials.post-card', ['post' => $item])` | same | s-directive |
208
+ | `have_posts()` | `@foreach($posts as $item)` | same | s-directive pair |
258
209
 
259
- This applies to any listing — post archives, category pages, search results, related posts, comment lists, navigation menu items, etc.
210
+ **Conditional Rendering with `s-directive`:**
211
+
212
+ WordPress blocks that render conditionally (like `<!-- wp:post-featured-image -->`) use `s-directive` elements. See the MCP tool documentation for the sibling pattern — create an opening directive, content elements, then a closing directive as siblings.
213
+
214
+ Common WordPress conditionals to convert (use `$item` inside loops):
215
+ - `<!-- wp:post-featured-image -->` → `@if($item->featured_image)` ... `@endif`
216
+ - `<!-- wp:post-excerpt -->` → `@if($item->post_excerpt)` ... `@endif`
217
+ - `<!-- wp:post-comments -->` → `@if($item->comments->count() > 0)` ... `@endif`
218
+ - `<!-- wp:query-no-results -->` → `@if($posts->isEmpty())` ... `@endif` (outside loop)
219
+
220
+ **Iteration (The Loop):**
221
+
222
+ WordPress's "The Loop" (`have_posts() / the_post()`) maps to `@foreach` directives. Use `s-directive` elements for the opening `@foreach` and closing `@endforeach`.
223
+
224
+ **IMPORTANT - Loop Variable:** The default loop variable is `$item`. When creating elements inside a loop:
225
+ 1. Do NOT pass raw Blade syntax to `html_to_elements` — it will be stored literally
226
+ 2. Create clean HTML first, then use `update_element` to add dynamic attributes:
227
+ - `textField: "title"` → outputs `{{ $item->title }}`
228
+ - `hrefField: "slug"` → outputs `href="{{ $item->slug }}"`
229
+ - `srcField: "featured_image"` → outputs `src="{{ $item->featured_image }}"`
230
+ 3. For complex expressions (route helpers, method calls), use:
231
+ - `hrefExpression: "{{ route('posts.show', $item->slug) }}"`
232
+ - `srcExpression: "{{ $item->featured_image }}"`
233
+ 4. For text with Blade code, create a statement with `create_statement_with_code`, then add its UUID to the element's `statements` array via `update_element`
234
+
235
+ Example loop structure using `s-directive` siblings:
236
+ ```
237
+ 1. s-directive with statement: "@foreach($posts as $item)"
238
+ 2. article element (content to repeat)
239
+ 3. s-directive with statement: "@endforeach"
240
+ ```
260
241
 
261
242
  ### 5f. Create Partials & Components
262
243
  Convert template-parts/ files into Blade partials using `@include`. Static partials receive data via the second argument: `@include('partials.post-card', ['post' => $post])`. If a template part relies on JavaScript for interactivity (e.g. a slider, a filterable gallery, a live search form), create a Vue component instead and ensure it is registered and mounted in `app.js`.
@@ -267,18 +248,9 @@ Do not try to port WordPress CSS. Read the visual intent from the theme's CSS/th
267
248
  - WordPress navigation → `<nav class="flex items-center gap-6">`
268
249
  - WordPress post grid → `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">`
269
250
 
270
- ## Step 6 — Review
271
-
272
- After building everything, print a summary:
273
- - Database mode chosen (A or B)
274
- - Total models created (and which WordPress tables they map to, if Mode A)
275
- - Total controllers and methods
276
- - Total routes
277
- - Total Blade views and partials
278
- - Total Vue components (if any) and why each was needed
279
- - Any WordPress features that could NOT be mapped (e.g. specific plugins, shortcodes with no equivalent)
280
- - If Mode B: provide a SQL migration query or artisan command to copy content from the WordPress database to the new schema
281
- - Suggestions for manual follow-up
251
+ ## Step 6 — Review (Optional)
252
+
253
+ If requested, summarize what was created and note any WordPress features that couldn't be mapped.
282
254
 
283
255
  ## Important Rules
284
256