@stellisoft/stellify-mcp 0.1.34 → 0.1.36

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
@@ -352,6 +352,20 @@ Search for routes/pages in the project by name.
352
352
 
353
353
  ---
354
354
 
355
+ ### Views & Blade Templates
356
+
357
+ Stellify stores Blade views as elements instead of files. The root element's `name` field maps to the view name:
358
+
359
+ - Element with `name="notes.index"` → `view('notes.index', $data)`
360
+ - Element with `name="layouts.app"` → `@extends('layouts.app')`
361
+ - Element with `name="components.card"` → `<x-card>`
362
+
363
+ Use `update_element` to set the `name` on a root element after creating it with `html_to_elements`.
364
+
365
+ **Convention for reusable templates:** Attach layouts, components, and partials to a template route (e.g., `/template/app-layout`, `/template/card`) to keep them organized and editable.
366
+
367
+ ---
368
+
355
369
  ### Element Tools (UI Components)
356
370
 
357
371
  #### `create_element`
@@ -360,12 +374,80 @@ Create a new UI element. Provide either `page` (route UUID) for root elements, o
360
374
  **Parameters:**
361
375
  - `type` (required): Element type - one of:
362
376
  - HTML5: `s-wrapper`, `s-input`, `s-form`, `s-svg`, `s-shape`, `s-media`, `s-iframe`
363
- - Components: `s-loop`, `s-transition`, `s-freestyle`, `s-motion`
377
+ - Components: `s-transition`, `s-freestyle`, `s-motion`
364
378
  - Blade: `s-directive`
365
379
  - Shadcn/ui: `s-chart`, `s-table`, `s-combobox`, `s-accordion`, `s-calendar`, `s-contiguous`
366
380
  - `page` (optional): UUID of the page/route (for root elements)
367
381
  - `parent` (optional): UUID of the parent element (for child elements)
368
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
+
369
451
  ---
370
452
 
371
453
  #### `update_element`
@@ -383,7 +465,18 @@ Update an existing UI element.
383
465
  - `locked`: Prevent editing (boolean)
384
466
  - `tag`: HTML tag (div, input, button, etc.)
385
467
  - `classes`: CSS classes array `["class1", "class2"]`
386
- - `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
387
480
 
388
481
  **Event handlers** (set value to method UUID):
389
482
  - `click`: @click
@@ -441,8 +534,31 @@ Convert HTML to Stellify elements in ONE operation. This is the fastest way to b
441
534
  - `elements` (required): HTML string to convert
442
535
  - `page` (optional): Route UUID to attach elements to. Omit for Vue components.
443
536
  - `selection` (optional): Parent element UUID to attach to (alternative to page)
537
+ - `file` (optional): Vue component file UUID. Pass this to auto-wire @click handlers to method UUIDs.
444
538
  - `test` (optional): If true, returns structure without creating elements
445
539
 
540
+ **⚠️ CRITICAL: Multiple Root Elements**
541
+
542
+ When passing HTML with **multiple root-level elements** (e.g., `<header>`, `<main>`, `<footer>`), only the **FIRST root element** gets attached to the route via `routeParent`. Other elements are created but become **orphaned** (not attached to the route).
543
+
544
+ **Wrong approach (causes orphaned elements):**
545
+ ```
546
+ html_to_elements(page: routeUUID, elements: "<header>...</header><main>...</main><footer>...</footer>")
547
+ // Result: Only <header> is attached to the route. <main> and <footer> are orphaned!
548
+ ```
549
+
550
+ **Correct approach (make separate calls for each root element):**
551
+ ```
552
+ // Call 1: Header
553
+ html_to_elements(page: routeUUID, elements: "<header>...</header>")
554
+
555
+ // Call 2: Main content
556
+ html_to_elements(page: routeUUID, elements: "<main>...</main>")
557
+
558
+ // Call 3: Footer
559
+ html_to_elements(page: routeUUID, elements: "<footer>...</footer>")
560
+ ```
561
+
446
562
  **Features:**
447
563
  - Parses HTML structure
448
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,24 @@ 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. Include context for significant UI components.`,
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 }}"\`
604
+ - \`srcField\`: Field name for src → outputs \`src="{{ $item->fieldName }}"\`
605
+
606
+ **For complex Blade expressions in attributes:**
607
+ Use expression attributes when you need more than simple field access (e.g., route helpers, method calls):
608
+ - \`hrefExpression\`: Blade expression for href → outputs \`href="..."\` with the expression
609
+ - \`srcExpression\`: Blade expression for src → outputs \`src="..."\` with the expression
610
+ - \`altExpression\`: Blade expression for alt → outputs \`alt="..."\` with the expression
611
+
612
+ Example: \`hrefExpression: "{{ route('posts.show', $item->slug) }}"\`
613
+
614
+ **For Blade text content:**
615
+ Use the \`statements\` array with statement UUIDs containing Blade code. The statement's \`code\` property will be output directly for Blade to evaluate.`,
600
616
  inputSchema: {
601
617
  type: 'object',
602
618
  properties: {
@@ -685,12 +701,50 @@ Note: To reorder elements, use update_element to modify the parent element's 'da
685
701
  },
686
702
  {
687
703
  name: 'html_to_elements',
688
- description: `Convert HTML to Stellify elements. Returns element UUIDs to use in save_file template array.
704
+ description: `Convert HTML to Stellify elements.
705
+
706
+ **IMPORTANT - Choose the right approach:**
707
+
708
+ **For SSR/Blade Pages (WordPress imports, static content, layouts):**
709
+ - MUST pass 'page' (route UUID) - elements attach to the route for server-side rendering
710
+ - This is the most common use case
711
+
712
+ **For Vue Components (client-side interactivity):**
713
+ - Omit 'page' - elements are standalone, referenced by file's template array
714
+ - Returns UUIDs to use in save_file's template array
715
+
716
+ **Where elements go:**
717
+ - Pass 'page' (route UUID): Elements attached to the route for SSR rendering
718
+ - Pass 'selection' (element UUID): Elements attached as children of existing element
719
+ - Omit both: Elements are standalone (Vue components only) - use returned UUIDs in save_file's template array
720
+
721
+ **⚠️ CRITICAL: Multiple Root Elements Limitation**
722
+ When HTML contains multiple root-level elements (e.g., <header>, <main>, <footer>), only the FIRST root element gets attached to the route. Other elements become orphaned!
723
+
724
+ **WRONG:** \`html_to_elements(page: routeUUID, elements: "<header>...</header><main>...</main><footer>...</footer>")\`
725
+ → Only <header> attaches to route. <main> and <footer> are orphaned!
689
726
 
690
- Auto-detects Vue bindings ({{ var }}). For Vue components, omit 'page'. For pages, provide route UUID.
727
+ **CORRECT:** Make separate calls for each root element:
728
+ 1. \`html_to_elements(page: routeUUID, elements: "<header>...</header>")\`
729
+ 2. \`html_to_elements(page: routeUUID, elements: "<main>...</main>")\`
730
+ 3. \`html_to_elements(page: routeUUID, elements: "<footer>...</footer>")\`
731
+
732
+ **OR** wrap all elements in a single container div.
691
733
 
692
734
  **@click auto-wiring:** Pass 'file' UUID to auto-resolve @click="methodName" handlers. Methods must exist in the file first.
693
735
 
736
+ **Blade Syntax Handling:**
737
+ 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:
738
+
739
+ 1. **For static HTML:** Pass clean HTML without Blade syntax, then use \`update_element\` to add dynamic behavior
740
+ 2. **For loop content:** After creating elements, use \`update_element\` with:
741
+ - \`textField\`, \`hrefField\`, \`srcField\` for simple field access (outputs \`{{ $item->field }}\`)
742
+ - \`hrefExpression\`, \`srcExpression\`, \`altExpression\` for complex expressions
743
+ - \`statements\` array with statement UUIDs for text content with Blade code
744
+ 3. **For conditionals:** Use \`s-directive\` elements as siblings (see update_element docs)
745
+
746
+ **Loop variable:** Inside \`@foreach\` loops created with \`s-directive\`, the default loop variable is \`$item\`. Use \`textField: "title"\` to output \`{{ $item->title }}\`.
747
+
694
748
  Prefer SVG icons over emoji (encoding issues).`,
695
749
  inputSchema: {
696
750
  type: 'object',
@@ -701,11 +755,11 @@ Prefer SVG icons over emoji (encoding issues).`,
701
755
  },
702
756
  page: {
703
757
  type: 'string',
704
- description: 'Route UUID to attach elements to. Optional for Vue components.',
758
+ description: 'Route UUID to attach elements to. REQUIRED for SSR/Blade pages (WordPress imports, static content, layouts). Only omit for Vue component templates.',
705
759
  },
706
760
  selection: {
707
761
  type: 'string',
708
- description: 'Parent element UUID to attach to (alternative to page)',
762
+ description: 'Parent element UUID to attach to (alternative to page). Use when adding children to existing elements.',
709
763
  },
710
764
  file: {
711
765
  type: 'string',
@@ -1154,7 +1208,7 @@ Changes are EPHEMERAL (not saved). For persistent changes, use update_element or
1154
1208
  },
1155
1209
  api: {
1156
1210
  type: 'boolean',
1157
- description: 'Generate API-style responses (default: true)',
1211
+ description: 'Generate API-style responses (default: true). Set to FALSE for SSR/Blade pages (controllers return views). Set to TRUE for API endpoints (controllers return JSON).',
1158
1212
  },
1159
1213
  },
1160
1214
  required: ['name'],
@@ -1405,22 +1459,45 @@ const SERVER_INSTRUCTIONS = `Stellify is a coding platform where code is stored
1405
1459
 
1406
1460
  - Files are stored as json. The json has a data key that references its methods (using uuids), and each method has a data key that references statements, and each statement has a data key that references clauses.
1407
1461
 
1408
- ## Workflow (follow every step)
1409
- 1. Research: Call get_project to understand current structure.
1410
- 2. **If JS/Vue task**: Call \`get_stellify_framework_api\` to check for composables before writing custom code.
1411
- 3. Plan: Map out the solution before calling tools.
1412
- 4. Create: create_file, create_method (with body), create_statement_with_code
1413
- 5. **Handle mount file**: Check \`appJs\` in create_file response. If \`action_required\` exists, ask user about mount file before proceeding.
1414
- 6. Wire: html_to_elements (pass file UUID to auto-wire @click handlers)
1415
- 7. Finalize: save_file with template/data/statements arrays (include frameworkImports for composables)
1416
- 8. Verify: Call \`get_assembled_code\` to check the output
1417
- 9. Test: Use run_code or broadcast_element_command to verify behavior
1462
+ ## Choosing Between SSR Pages and Vue Components
1463
+
1464
+ **Use SSR/Blade Pages (routes + elements) when:**
1465
+ - Building static pages, layouts, or content pages
1466
+ - Importing from WordPress, HTML templates, or static sites
1467
+ - No client-side interactivity needed beyond links/forms
1468
+ - SEO is important
1469
+
1470
+ **Use Vue Components (files with type='js', extension='vue') when:**
1471
+ - Client-side interactivity is required (counters, toggles, dynamic forms)
1472
+ - Real-time updates needed
1473
+ - Complex state management
1474
+
1475
+ **CRITICAL DIFFERENCE:**
1476
+ - SSR Pages: Elements attach directly to routes. Use \`html_to_elements\` with \`page\` parameter (route UUID). Elements render via Blade.
1477
+ - Vue Components: Elements are standalone, referenced by file's \`template\` array. Use \`html_to_elements\` WITHOUT \`page\` parameter.
1478
+
1479
+ ## Workflow for SSR Pages (most common)
1480
+ 1. Research: Call get_project to understand current structure
1481
+ 2. Create route: \`create_route\` for each page
1482
+ 3. Add elements: \`html_to_elements\` with \`page\` parameter set to route UUID
1483
+ 4. Wire controller (if needed): \`save_route\` with controller and controller_method UUIDs
1484
+
1485
+ ## Workflow for Vue Components (client-side interactivity)
1486
+ 1. Research: Call get_project to understand current structure
1487
+ 2. **Call \`get_stellify_framework_api\`** to check for composables before writing custom code
1488
+ 3. Create file: \`create_file\` with type='js', extension='vue'
1489
+ 4. Create methods: \`create_method\` (with body parameter)
1490
+ 5. Create state: \`create_statement_with_code\` for refs/reactive
1491
+ 6. Create template: \`html_to_elements\` WITHOUT \`page\` parameter (returns UUIDs)
1492
+ 7. **Handle mount file**: Check \`appJs\` in create_file response. If \`action_required\` exists, ask user about mount file
1493
+ 8. Finalize: \`save_file\` with template/data/statements arrays (include frameworkImports for composables)
1494
+ 9. Verify: Call \`get_assembled_code\` to check the output
1418
1495
  `;
1419
1496
  // Legacy detailed instructions preserved as comments for reference if needed
1420
1497
  // Create MCP server
1421
1498
  const server = new Server({
1422
1499
  name: 'stellify-mcp',
1423
- version: '0.1.34',
1500
+ version: '0.1.36',
1424
1501
  }, {
1425
1502
  capabilities: {
1426
1503
  tools: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stellisoft/stellify-mcp",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
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.34",
9
+ "version": "0.1.36",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@stellisoft/stellify-mcp",
14
- "version": "0.1.34",
14
+ "version": "0.1.36",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -0,0 +1,238 @@
1
+ ---
2
+ name: wordpress-import
3
+ description: "Use this skill when importing WordPress sites into Stellify. Invoke with /wordpress-import to start the guided workflow. Covers: analyzing WordPress exports, converting post types to Eloquent models, transforming PHP templates to Vue components, migrating forms and plugins, and creating routes and API endpoints."
4
+ license: MIT
5
+ metadata:
6
+ author: stellify
7
+ ---
8
+
9
+ # WordPress Import Skill
10
+
11
+ You are importing a WordPress site into Stellify by reading its PHP theme files and recreating the site as a structured Laravel application using the Stellify MCP tools.
12
+
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
+
15
+ ## Step 1 — Identify the Active Theme
16
+
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.
18
+
19
+ Before proceeding, ask the user:
20
+
21
+ > **How do you want to handle the database?**
22
+ >
23
+ > **A) Connect to existing WordPress database** — Models map directly to the WordPress tables (`wp_posts`, `wp_users`, etc.) using the existing column names. No migrations, no data copying. Your Laravel app reads and writes to the same database WordPress uses. This is the fastest path.
24
+ >
25
+ > **B) Fresh database** — Create new Laravel migrations with clean column names (`title` instead of `post_title`, etc.). You start with an empty database and can optionally migrate content from WordPress later.
26
+
27
+ These are the only questions you may ask. Do not ask anything else.
28
+
29
+ ## Step 2 — Inventory the Theme
30
+
31
+ Read the full theme directory. Categorise every file:
32
+
33
+ - **Core templates**: index.php, single.php, page.php, archive.php, search.php, 404.php, front-page.php, home.php
34
+ - **Structural partials**: header.php, footer.php, sidebar.php
35
+ - **Template parts**: anything in template-parts/ or patterns/
36
+ - **Functions**: functions.php (and any files it includes/requires)
37
+ - **Styles**: style.css, any CSS/SCSS files
38
+ - **Config**: theme.json if present (block theme configuration)
39
+
40
+ Print the inventory before proceeding.
41
+
42
+ ## Step 3 — Analyse functions.php
43
+
44
+ Read functions.php thoroughly. Extract:
45
+
46
+ - **Custom post types** registered via `register_post_type()` → these become Laravel models
47
+ - **Taxonomies** registered via `register_taxonomy()` → these become models or enums
48
+ - **Navigation menus** registered via `register_nav_menus()` → these define nav structure
49
+ - **Widget areas** registered via `register_sidebar()` → note for layout
50
+ - **Enqueued scripts/styles** via `wp_enqueue_script/style()` → note any JS dependencies
51
+ - **Custom image sizes** via `add_image_size()` → note for media handling
52
+ - **Theme supports** via `add_theme_support()` → note post formats, thumbnails, etc.
53
+ - **Shortcodes** via `add_shortcode()` → these become Blade includes or components
54
+ - **AJAX handlers** via `wp_ajax_*` → these become API routes
55
+ - **Any included files** → read those too
56
+
57
+ ## Step 4 — Plan the Laravel Structure
58
+
59
+ Before calling any Stellify tools, plan:
60
+
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
66
+
67
+ ## Step 5 — Build in Stellify (Order of Operations)
68
+
69
+ Execute the plan using Stellify MCP tools in this order. This order matters — parent records must exist before children.
70
+
71
+ ### 5a. Create Models, Migrations & Controllers
72
+
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.
74
+
75
+ **If Mode A (connect to existing WordPress database):**
76
+
77
+ Create resources that map to the existing WordPress tables. No migrations. Set the model's table and primary key to match WordPress.
78
+
79
+ ```php
80
+ // Example Post model
81
+ class Post extends Model {
82
+ protected $table = 'wp_posts';
83
+ protected $primaryKey = 'ID';
84
+
85
+ // Scope to only published posts (not revisions, drafts, etc.)
86
+ public function scopePublished($query) {
87
+ return $query->where('post_status', 'publish')
88
+ ->where('post_type', 'post');
89
+ }
90
+ }
91
+ ```
92
+
93
+ Key WordPress table mappings:
94
+ - Posts & Pages → `wp_posts` (distinguished by `post_type` column: 'post', 'page', or custom types)
95
+ - Categories & Tags → `wp_terms` + `wp_term_taxonomy` + `wp_term_relationships`
96
+ - Users → `wp_users`
97
+ - Post meta / custom fields → `wp_postmeta`
98
+ - Comments → `wp_comments`
99
+ - Navigation menus → `wp_posts` with `post_type = 'nav_menu_item'` + `wp_terms` with taxonomy `nav_menu`
100
+
101
+ In Blade templates, use WordPress column names:
102
+ - `$post->post_title` (not `$post->title`)
103
+ - `$post->post_content` (not `$post->content`)
104
+ - `$post->post_excerpt` (not `$post->excerpt`)
105
+ - `$post->post_name` (this is the slug)
106
+ - `$post->post_date` (not `$post->published_at`)
107
+ - `$post->post_status` (not `$post->status`)
108
+
109
+ **If Mode B (fresh database):**
110
+
111
+ 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.
112
+
113
+ ### 5b. Refine Controller Methods
114
+
115
+ `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:
116
+
117
+ ```php
118
+ // CORRECT for Stellify — returns a data array, Stellify handles the view binding
119
+ public function index(): array
120
+ {
121
+ return ['posts' => Post::where('status', 'published')->latest('published_at')->paginate(10)];
122
+ }
123
+
124
+ public function show(Post $post): array
125
+ {
126
+ return ['post' => $post];
127
+ }
128
+
129
+ // WRONG — do not return JSON
130
+ public function index()
131
+ {
132
+ return Post::where('status', 'published')->get();
133
+ }
134
+
135
+ // WRONG — do not call view() directly, Stellify handles this
136
+ public function index()
137
+ {
138
+ return view('posts.index', compact('posts'));
139
+ }
140
+ ```
141
+
142
+ **Mode A note:** WordPress stores posts, pages, and custom post types all in `wp_posts`, distinguished by the `post_type` column. Controllers must scope queries accordingly — e.g. `Post::where('post_type', 'post')->where('post_status', 'publish')`. It also stores revisions and auto-drafts in the same table, so always filter by `post_status`.
143
+
144
+ ### 5c. Create Routes
145
+ Create route entries mapping URLs to controller methods.
146
+
147
+ ### 5d. Create the Layout (layouts/app.blade.php)
148
+ Read header.php and footer.php. Create a single Blade layout that combines:
149
+ - The HTML structure from header.php (doctype, head, opening body, nav)
150
+ - `@yield('content')` for page content
151
+ - The structure from footer.php (closing elements, footer content)
152
+
153
+ Translate WordPress functions to Blade:
154
+ - `wp_head()` → `@vite(['resources/css/app.css', 'resources/js/app.js'])` and `<meta>` tags
155
+ - `wp_footer()` → nothing needed (Vite handles it)
156
+ - `wp_nav_menu()` → `@include('partials.nav')`
157
+ - `bloginfo('name')` → `{{ config('app.name') }}`
158
+ - `bloginfo('description')` → `{{ config('app.description', '') }}`
159
+ - `body_class()` → appropriate Tailwind classes
160
+ - `language_attributes()` → `lang="{{ str_replace('_', '-', app()->getLocale()) }}"`
161
+
162
+ ### 5e. Create Blade Views
163
+ 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')`.**
164
+
165
+ **⚠️ 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.
166
+
167
+ **WordPress → Blade translation guide:**
168
+
169
+ Mode A uses WordPress column names; Mode B uses clean Laravel names.
170
+
171
+ **IMPORTANT:** Inside `@foreach` loops, use `$item` as the loop variable. This matches the Stellify assembler's expectations for `textField`, `hrefField`, `srcField` attributes.
172
+
173
+ | WordPress | Blade (Mode A) | Blade (Mode B) | Stellify Attribute |
174
+ |-----------|----------------|----------------|-------------------|
175
+ | `the_title()` | `{{ $item->post_title }}` | `{{ $item->title }}` | `textField: "title"` |
176
+ | `the_content()` | `{!! $item->post_content !!}` | `{!! $item->content !!}` | statement with code |
177
+ | `the_excerpt()` | `{{ $item->post_excerpt }}` | `{{ $item->excerpt }}` | `textField: "excerpt"` |
178
+ | `the_permalink()` | `{{ route('posts.show', $item->post_name) }}` | `{{ route('posts.show', $item->slug) }}` | `hrefExpression: "..."` |
179
+ | `the_date()` | `{{ $item->post_date->format('d M Y') }}` | `{{ $item->published_at->format('d M Y') }}` | statement with code |
180
+ | `the_author()` | `{{ $item->author->display_name }}` | `{{ $item->author->name }}` | statement with code |
181
+ | `get_template_part()` | `@include('partials.post-card', ['post' => $item])` | same | s-directive |
182
+ | `have_posts()` | `@foreach($posts as $item)` | same | s-directive pair |
183
+
184
+ **Conditional Rendering with `s-directive`:**
185
+
186
+ 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.
187
+
188
+ Common WordPress conditionals to convert (use `$item` inside loops):
189
+ - `<!-- wp:post-featured-image -->` → `@if($item->featured_image)` ... `@endif`
190
+ - `<!-- wp:post-excerpt -->` → `@if($item->post_excerpt)` ... `@endif`
191
+ - `<!-- wp:post-comments -->` → `@if($item->comments->count() > 0)` ... `@endif`
192
+ - `<!-- wp:query-no-results -->` → `@if($posts->isEmpty())` ... `@endif` (outside loop)
193
+
194
+ **Iteration (The Loop):**
195
+
196
+ WordPress's "The Loop" (`have_posts() / the_post()`) maps to `@foreach` directives. Use `s-directive` elements for the opening `@foreach` and closing `@endforeach`.
197
+
198
+ **IMPORTANT - Loop Variable:** The default loop variable is `$item`. When creating elements inside a loop:
199
+ 1. Do NOT pass raw Blade syntax to `html_to_elements` — it will be stored literally
200
+ 2. Create clean HTML first, then use `update_element` to add dynamic attributes:
201
+ - `textField: "title"` → outputs `{{ $item->title }}`
202
+ - `hrefField: "slug"` → outputs `href="{{ $item->slug }}"`
203
+ - `srcField: "featured_image"` → outputs `src="{{ $item->featured_image }}"`
204
+ 3. For complex expressions (route helpers, method calls), use:
205
+ - `hrefExpression: "{{ route('posts.show', $item->slug) }}"`
206
+ - `srcExpression: "{{ $item->featured_image }}"`
207
+ 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`
208
+
209
+ Example loop structure using `s-directive` siblings:
210
+ ```
211
+ 1. s-directive with statement: "@foreach($posts as $item)"
212
+ 2. article element (content to repeat)
213
+ 3. s-directive with statement: "@endforeach"
214
+ ```
215
+
216
+ ### 5f. Create Partials & Components
217
+ 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`.
218
+
219
+ ### 5g. Style with Tailwind
220
+ Do not try to port WordPress CSS. Read the visual intent from the theme's CSS/theme.json and apply Tailwind utility classes directly in the Blade templates. For example:
221
+ - WordPress container → `<div class="max-w-4xl mx-auto px-4">`
222
+ - WordPress navigation → `<nav class="flex items-center gap-6">`
223
+ - WordPress post grid → `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">`
224
+
225
+ ## Step 6 — Review (Optional)
226
+
227
+ If requested, summarize what was created and note any WordPress features that couldn't be mapped.
228
+
229
+ ## Important Rules
230
+
231
+ - **Blade for SSR, Vue for interactivity.** If the WordPress source is server-rendered PHP (posts, pages, archives, menus, layouts), create Blade templates using `@extends`, `@section`, `@include`, `@foreach`, `{{ }}` and `{!! !!}`. If the WordPress source uses JavaScript for interactivity (AJAX, dynamic filtering, sliders, modals, live search, infinite scroll), create a Vue component. When creating Vue components, ensure they are registered and mounted in `app.js` via `createApp` — do not just import them.
232
+ - **Do not ask questions** except the two permitted in Step 1 (which theme, and which database mode). Make reasonable decisions and document them.
233
+ - **Work file by file** — read a WordPress file, create the Stellify equivalent, move to the next.
234
+ - **Show progress** — print what you're reading and what you're creating as you go.
235
+ - **Handle block themes** — if theme.json exists and templates are in HTML with block markup (`<!-- wp:xxx -->`), parse the block structure rather than traditional PHP templates. The output is still Blade (or Vue where the block is interactive).
236
+ - **Style with Tailwind** — do not port WordPress CSS. Interpret the visual intent and use Tailwind utilities.
237
+ - **Be pragmatic** — focus on the core templates that define the site's main pages. Skip hyper-specific template variations unless they're clearly important.
238
+ - **Content handling depends on database mode** — In Mode A (existing DB), the content is already there via the WordPress tables. In Mode B (fresh DB), content is separate and can be migrated later.