migrate-component
Migrate WordPress components from v1 (page-parts/) or v2 (layouts/) to v3 (vendi-theme-parts/components/) structure. Use when migrating components, moving layouts to v3, or converting component files.
$ Instalar
git clone https://github.com/majiayu000/claude-skill-registry /tmp/claude-skill-registry && cp -r /tmp/claude-skill-registry/skills/development/migrate-component ~/.claude/skills/claude-skill-registry// tip: Run this command in your terminal to install the skill
name: migrate-component description: Migrate WordPress components from v1 (page-parts/) or v2 (layouts/) to v3 (vendi-theme-parts/components/) structure. Use when migrating components, moving layouts to v3, or converting component files.
Component Migration Skill: v1/v2 to v3
You are helping migrate WordPress theme components to the v3 structure (vendi-theme-parts/components/). There are two migration paths:
- v2 → v3: ACF Flexible Layout components from
layouts/ - v1 → v3: Utility/site components from
page-parts/site/orpage-parts/page/
Your Role
Guide the user through migrating WordPress theme components one at a time. Each component migration includes:
- Moving files with git mv
- Creating a component class
- Refactoring the main template
- Migrating sub-layouts (if applicable)
- Creating documentation
The user will handle git add and git commit commands - you should NOT run these.
Cleanup After Migration
After successfully migrating a component, clean up the old v2 folders:
- Check for leftover files: Look for any files in
layouts/[component_name]/that weren't migrated - Handle dead/unused files:
- If you find old sub-layouts or files that aren't referenced in the current code, move them to
_LEGACY/ - Only create the
_LEGACY/folder if there are actually dead files to preserve - Example:
git mv layouts/[component_name]/layouts/old_file.php vendi-theme-parts/components/[component_name]/_LEGACY/old_file.php
- If you find old sub-layouts or files that aren't referenced in the current code, move them to
- Remove empty directories: After all files are moved, remove the empty v2 directories
- Use
rmdirto safely remove only empty directories - Example:
rmdir layouts/[component_name]/layouts && rmdir layouts/[component_name]
- Use
This ensures the codebase stays clean and old unused code is preserved in _LEGACY folders if needed.
Reference Components
v2 Components (Content/Layout):
- Simple:
basic_copy_block,form - With custom root setup:
accordion,content_callout_block - With sub-layouts:
accordion,media_block,events,carousel_resources - Without region/wrap pattern:
media_block - Using VendiComponent:
ad_row
v1 Components (Utility/Site):
- Utility component:
ad-slot
v2 to v3 Migration (Content Components)
File Structure Transformation
v2 Structure (layouts/)
layouts/[component_name]/
├── component.php
├── render.css
├── admin.css (optional, kept as-is)
└── thumbnail.png
v3 Structure (vendi-theme-parts/components/)
vendi-theme-parts/components/[component_name]/
├── [component_name].php (was component.php)
├── [component_name].class.php (new)
├── [component_name].css (was render.css)
├── [component_name].docs.yaml (new)
├── [component_name].thumbnail.png (was thumbnail.png)
├── admin.css (kept as-is, not used in v3)
└── docs/
├── overview.md
└── accessibility.md
Migration Steps
Step 1: Examine the v2 Component
First, explore the existing component:
find layouts/[component_name] -type f
Read the main files:
layouts/[component_name]/component.php(or[component_name].php)layouts/[component_name]/render.css- Any sub-layout files in
layouts/[component_name]/layouts/
Step 2: Move Files with git mv
Create the v3 directory and move files:
mkdir -p vendi-theme-parts/components/[component_name]
git mv layouts/[component_name]/component.php vendi-theme-parts/components/[component_name]/[component_name].php
git mv layouts/[component_name]/render.css vendi-theme-parts/components/[component_name]/[component_name].css
git mv layouts/[component_name]/thumbnail.png vendi-theme-parts/components/[component_name]/[component_name].thumbnail.png
If admin.css exists:
git mv layouts/[component_name]/admin.css vendi-theme-parts/components/[component_name]/admin.css
Step 3: Create Component Class
Create [component_name].class.php in the component directory.
Component Class Hierarchy:
BaseComponent- Full component with auto-generated wrappers (<section>,<div class="region">,<div class="content-wrap">)VendiComponent- Simplified version with only root tag, no extra HTML wrappers
Conventions:
- Top-level components typically extend
BaseComponent - Sub-layouts commonly extend
VendiComponent - Choice depends on needed HTML structure, not strict rules
Purpose: Enable automatic asset loading (CSS/JS only loaded when component is used) and provide helper methods for cleaner templates.
Minimal Class Template:
<?php
namespace Vendi\Theme\Component;
use Vendi\Theme\BaseComponent;
class [component_name] extends BaseComponent
{
public function __construct()
{
parent::__construct('[css-class-name]');
}
protected function initComponent(): void
{
parent::initComponent();
// Add dynamic classes or attributes to root element
// Example: $this->addRootClass('variant-' . get_sub_field('variant'));
// Example: $this->addRootAttribute('data-role', 'carousel');
}
// Override wrapper class names if v2 used different names
protected function getContentWrapClassName(): string
{
return 'wrap'; // Default is 'content-wrap'
}
protected function getRegionWrapClassName(): string
{
return 'region'; // Default is 'region' (rarely needs override)
}
}
With Helper Methods (for complex templates):
<?php
namespace Vendi\Theme\Component;
use Vendi\Theme\BaseComponent;
class [component_name] extends BaseComponent
{
public function __construct()
{
parent::__construct('[css-class-name]');
}
protected function abortRender(): bool
{
if (!$this->getRequiredField()) {
return true;
}
return parent::abortRender();
}
public function getRequiredField(): ?array
{
$field = $this->getSubField('field_name');
if (!is_array($field)) {
return null;
}
return $field;
}
// Add getter methods to simplify template
public function getHeadline(): ?string
{
return $this->getSubField('headline');
}
}
Important Notes:
- Add helper methods ONLY when templates have complex logic
- Don't create simple pass-through methods
- Keep templates simple and readable
Step 4: Refactor Main PHP Template
Understanding Auto-Generated Wrappers:
v3 automatically wraps content with:
<section class="[component-class]">
<div class="region">
<div class="content-wrap">
<!-- your content -->
Remove these wrappers from the template - they're handled by renderComponentWrapperStart().
Standard v2 to v3 Conversion:
Before (v2):
<section class="section-[name] <?php esc_attr_e('dynamic-class-'.get_sub_field('field')); ?>">
<div class="region">
<div class="wrap">
<!-- content -->
</div>
</div>
</section>
After (v3):
<?php
use Vendi\Theme\Component\[component_name];
use Vendi\Theme\ComponentUtility;
/** @var [component_name] $component */
$component = ComponentUtility::get_new_component_instance([component_name]::class);
if (!$component->renderComponentWrapperStart()) {
return;
}
?>
<!-- content only (all wrappers removed) -->
<?php
$component->renderComponentWrapperEnd();
Important Cases:
-
If v2 used
<div class="wrap">instead of<div class="content-wrap">: OverridegetContentWrapClassName()in the class to return'wrap'to preserve existing CSS. -
If component doesn't follow region/wrap pattern: Some components have custom structures like
<section><div class="custom-class">...</div></section>. In these cases, ONLY remove the outer<section>tag. Keep all other markup as-is.Example:
// v2: <section class="media-block-container"><div class='narrow-left-aligned'>...</div></section> // v3: Only remove <section>, keep <div class='narrow-left-aligned'>...</div>
Step 5: Handle Sub-Layouts (if applicable)
If the component has sub-layouts in layouts/[component_name]/layouts/:
Move Sub-Layout Files:
# For each sub-layout, create its own directory
mkdir -p vendi-theme-parts/components/[component_name]/[field_name]/[layout_name]
git mv layouts/[component_name]/layouts/[layout].php vendi-theme-parts/components/[component_name]/[field_name]/[layout_name]/[layout_name].php
Create Loader File:
Create [field_name]/[field_name].php:
<?php
while (have_rows("[field_name]")) {
the_row();
$layout = get_row_layout();
if (!in_array($layout, ['layout1', 'layout2'], true)) {
continue;
}
vendi_load_component_v3(['component_name', 'field_name', $layout]);
}
Update Sub-Layout Files:
Critical: The global variable for state has been renamed in v3.
// v2 (OLD - don't use)
global $vendi_layout_component_object_state;
$data = $vendi_layout_component_object_state['key'] ?? null;
// v3 (NEW - use this)
global $vendi_component_object_state;
$data = $vendi_component_object_state['key'] ?? null;
When sub-layouts need to pass state to other sub-layouts:
vendi_load_component_v3(['component_name', 'field_name', 'layout'], ['key' => $value]);
Note: There is NO vendi_load_component_v3_with_state function. State is passed as the second parameter to vendi_load_component_v3.
Sub-Layout Classes (Optional):
Sub-layouts can have their own class files for complex logic. They commonly extend VendiComponent:
<?php
namespace Vendi\Theme\Component;
use Vendi\Theme\VendiComponent;
class [sub_layout_name] extends VendiComponent
{
public function __construct()
{
parent::__construct('[css-class-name]');
}
public function getEvents(): array
{
return tribe_get_events(['posts_per_page' => 4, 'start_date' => 'now']);
}
protected function abortRender(): bool
{
if (!$this->getEvents()) {
return true;
}
return parent::abortRender();
}
}
Reference events/events/recent_events for a complete example.
Step 6: Create Documentation
Create documentation structure:
mkdir -p vendi-theme-parts/components/[component_name]/docs
Create [component_name].docs.yaml:
component-name: [Human Readable Name]
role: component
example-prefix: [component-name]
parent-field: content_components
pages:
- Overview
- Accessibility
Create docs/overview.md:
Describe:
- Component purpose and functionality
- When to use this component
- When to choose a different component
- Available component options and fields
- Visual elements and features
- Any special behaviors
Infer content from the component template, class, and CSS.
Create docs/accessibility.md:
Document accessibility considerations:
- Heading hierarchy
- Color and contrast requirements
- Semantic HTML structure
- Image alt text requirements
- Interactive element accessibility
- Keyboard navigation
- Screen reader announcements
- Focus management
- Motion and animation considerations
Reference basic_copy_block, content_callout_block, media_block, or carousel_resources for examples.
Code Style Guidelines
Template Structure
Templates follow a three-part structure:
<?php
// === TOP: Setup ===
use Vendi\Theme\Component\example;
use Vendi\Theme\ComponentUtility;
/** @var example $component */
$component = ComponentUtility::get_new_component_instance(example::class);
if (!$component->renderComponentWrapperStart()) {
return;
}
// Pre-calculate any needed variables
$items = $component->getItems();
?>
<!-- === MIDDLE: Template with alternative syntax === -->
<?php if ($condition): ?>
<div class="example">Content</div>
<?php endif; ?>
<?php foreach ($items as $item): ?>
<p><?php echo $item; ?></p>
<?php endforeach; ?>
<?php
// === BOTTOM: Cleanup ===
$component->renderComponentWrapperEnd();
Alternative PHP Syntax
ALWAYS use alternative PHP syntax in the middle (template) section:
✅ Correct:
<?php if ($condition): ?>
<div>Content</div>
<?php endif; ?>
<?php foreach ($items as $item): ?>
<p><?php echo $item; ?></p>
<?php endforeach; ?>
<?php while (have_rows('field')): ?>
<?php the_row(); ?>
<?php endwhile; ?>
❌ Incorrect (don't use curly braces in template section):
<?php if ($condition) { ?>
<div>Content</div>
<?php } ?>
<?php foreach ($items as $item) { ?>
<p><?php echo $item; ?></p>
<?php } ?>
HTML Attributes
Always use double quotes for HTML attributes:
✅ <div class="example" data-role="carousel">
❌ <div class='example' data-role='carousel'>
SVG Loading
Use the vendi_get_svg() helper function:
✅ <?php vendi_get_svg('/svgs/icon.svg'); ?>
❌ <?php echo file_get_contents(VENDI_CUSTOM_THEME_PATH . '/svgs/icon.svg'); ?>
Variable Usage
Echo variables directly - don't create intermediate variables:
✅ <?php echo vendi_fly_get_attachment_picture($id, 'size'); ?>
❌ <?php $image = vendi_fly_get_attachment_picture($id, 'size'); echo $image; ?>
Class Methods
Avoid rendering from class methods. Classes should not output to the HTTP stream.
Use getter methods that return data, then render in templates:
✅ Correct:
// Class
public function getItems(): array
{
return ['item1', 'item2'];
}
// Template
<?php foreach ($component->getItems() as $item): ?>
<p><?php echo $item; ?></p>
<?php endforeach; ?>
❌ Incorrect:
// Class - DON'T DO THIS
public function renderItems(): void
{
foreach ($this->getItems() as $item) {
echo "<p>{$item}</p>";
}
}
Exception: Very rare cases where rendering from class is absolutely necessary.
Key Principles
- One file per commit - User handles git add/commit, NOT you
- Preserve template logic - Don't refactor working code unless necessary
- Dynamic classes go in
initComponent()usingaddRootClass()oraddRootAttribute() - Admin.css is kept but not used in v3
- Helper methods only for complex template needs, not simple pass-throughs
- Alternative PHP syntax in template sections (if:/endif, foreach:/endforeach)
- Double quotes for all HTML attributes
- No rendering from class methods - return data, render in templates
Common Patterns
Loading Function Calls
v2 to v3 conversions:
| v2 Function | v3 Function | Notes |
|---|---|---|
vendi_load_layout_based_sub_component(basename(__DIR__), 'layout') | vendi_load_component_v3(['component_name', 'field_name', 'layout']) | Array syntax in v3 |
vendi_load_layout_based_sub_component_with_state(...) | vendi_load_component_v3(['...'], ['state' => $value]) | State is second parameter |
$vendi_layout_component_object_state | $vendi_component_object_state | Renamed global variable |
Dynamic Root Classes
Add dynamic classes in initComponent():
protected function initComponent(): void
{
parent::initComponent();
// Add class based on field value
$this->addRootClass('variant-' . get_sub_field('variant'));
// Conditional class
if ($this->shouldShowAd()) {
$this->addRootClass('show-ad');
}
// Add data attributes
$this->addRootAttribute('data-role', 'carousel-container');
}
v2 Migration Workflow
When the user asks to migrate a v2 component:
- Explore: Read the v2 component files to understand structure
- Move Files: Use git mv to move files to v3 structure
- Create Class: Build component class with appropriate methods
- Refactor Template: Update main PHP file to v3 pattern
- Handle Sub-Layouts: Migrate any sub-layouts if present
- Document: Create docs.yaml and markdown documentation
- Clean Up: Remove empty v2 directories with rmdir
- Remind: Tell user the component is ready to commit (they handle git add/commit)
Use the TodoWrite tool to track progress through these steps for complex migrations.
Always reference the existing migrated components for patterns and examples.
v1 to v3 Migration (Utility/Site Components)
Overview
v1 components are utility components stored in page-parts/site/ or page-parts/page/ directories. These are not flexible content components - they're called from other components using loader functions.
Key Differences from v2:
- Single files, not directories (e.g.,
page-parts/site/ad-slot.php) - No CSS/JS files to migrate (assets stored elsewhere or inline)
- No sub-layouts (simple utility components)
- Documentation: Only
overview.md(no accessibility.md for utility components) - Loader functions: Called via
vendi_load_site_component()orvendi_load_page_component() - State via global: Use
$vendi_component_object_stateglobal for passed parameters
File Structure Transformation
v1 Structure (page-parts/)
page-parts/site/ad-slot.php (single file)
v3 Structure (vendi-theme-parts/components/)
vendi-theme-parts/components/ad-slot/
├── ad-slot.php (migrated from page-parts/site/)
├── ad-slot.class.php (new)
├── ad-slot.docs.yaml (new)
└── docs/
└── overview.md (only overview, no accessibility)
Migration Steps
Step 1: Identify Component Type
v1 components are loaded via:
vendi_load_site_component('component-name')vendi_load_site_component_with_state('component-name', ['key' => $value])vendi_load_page_component('component-name')
These map to v3 as:
vendi_load_component_v3(['component-name'])vendi_load_component_v3(['component-name'], ['key' => $value])
Step 2: Move File
mkdir -p vendi-theme-parts/components/[component-name]
git mv page-parts/site/[component-name].php vendi-theme-parts/components/[component-name]/[component-name].php
Step 3: Create Class File
Create [component-name].class.php extending VendiComponent (utility components don't need full wrappers):
<?php
namespace Vendi\Theme\Component;
use Vendi\Theme\VendiComponent;
class component_name extends VendiComponent
{
public function __construct()
{
parent::__construct('component-class-name');
}
protected function abortRender(): bool
{
if (!$this->getRequiredData()) {
return true;
}
return parent::abortRender();
}
// Getter methods for state data
public function getStateValue(): mixed
{
global $vendi_component_object_state;
return $vendi_component_object_state['key'] ?? null;
}
// Helper methods for data from state objects
public function getSomething(): ?string
{
return get_field('field_name', $this->getStateValue());
}
}
Important: Utility components still use $vendi_component_object_state global to access passed state.
Step 4: Refactor Template
Update the template to use component class:
<?php
use Vendi\Theme\Component\component_name;
use Vendi\Theme\ComponentUtility;
/** @var component_name $component */
$component = ComponentUtility::get_new_component_instance(component_name::class);
if (!$component->renderComponentWrapperStart()) {
return;
}
// Extract data using component methods
$value = $component->getSomething();
?>
<div class="content">
<?php if ($value): ?>
<?php echo esc_html($value); ?>
<?php endif; ?>
</div>
<?php
$component->renderComponentWrapperEnd();
Code Style:
- Use alternative PHP syntax (if:/endif, foreach:/endforeach)
- Double quotes for HTML attributes
- Inline variable assignment for single-use variables
Step 5: Update All Loader References
Find all instances of the old loader functions and update them:
Find references:
grep -r "vendi_load_site_component.*'component-name'" --include="*.php"
Update pattern:
// Old v1
vendi_load_site_component_with_state('component-name', ['key' => $value]);
// New v3
vendi_load_component_v3(['component-name'], ['key' => $value]);
Update ALL occurrences across:
- Other v3 components
- v2 components (layouts/)
- v1 components (page-parts/)
Step 6: Create Documentation
docs.yaml (minimal for utility components):
component-name: [Human Readable Name]
role: component
example-prefix: [component-name]
parent-field: site # or 'page' depending on origin
pages:
- Overview
docs/overview.md:
- Describe component purpose and function
- Mark as "Utility Component" or "Site Component"
- Document that it's called from other components, not flexible content
- List required state parameters with types
- Show usage example with
vendi_load_component_v3() - Document any special behaviors or integrations
No accessibility.md - Utility components don't need accessibility documentation.
Step 7: No CSS/JS Migration
v1 components typically don't have dedicated CSS/JS files in their directories. Assets are:
- Inline in the template (keep as-is)
- Loaded from external sources (keep as-is)
- In global theme files (leave alone)
Do not search for or migrate CSS/JS files - they're optional and stored elsewhere.
Step 8: Clean Up
The original file was moved with git mv, so no cleanup needed. The empty page-parts/site/ or page-parts/page/ directory will remain (it may contain other components).
v1 Migration Workflow
When the user asks to migrate a v1 component:
- Identify: Confirm it's from page-parts/ directory
- Move File: Use git mv to move single file to v3 structure
- Create Class: Build utility component class (VendiComponent)
- Refactor Template: Update to use component class and state from global
- Update References: Find and replace ALL loader function calls across codebase
- Document: Create docs.yaml and overview.md (no accessibility.md)
- Remind: Tell user the component is ready to commit (they handle git add/commit)
Use the TodoWrite tool to track progress through these steps.
Key Reminders for v1 Migrations
- Use VendiComponent - Utility components don't need full BaseComponent wrappers
- Access state via global -
$vendi_component_object_statestill used in class methods - Update ALL references - Search entire codebase for loader function calls
- No CSS/JS - Don't search for or migrate asset files
- Only overview.md - No accessibility documentation for utility components
- Single file - No sub-layouts or complex directory structures
Repository
