# Imlazy Wordpress

> WordPress + ACF (Advanced Custom Fields) development — themes, plugins, hooks, blocks, field groups, security, REST API, and the full ACF ecosystem

- **Type:** Skill
- **Install:** `agentstack add skill-hnikoloski-imlazy-imlazy-wordpress`
- **Verified:** Pending review
- **Seller:** [hnikoloski](https://agentstack.voostack.com/s/hnikoloski)
- **Installs:** 0
- **Category:** [Productivity](https://agentstack.voostack.com/c/productivity)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [hnikoloski](https://github.com/hnikoloski)
- **Source:** https://github.com/hnikoloski/imlazy/tree/main/skills/imlazy-wordpress
- **Website:** https://hnikoloski.github.io/imlazy/

## Install

```sh
agentstack add skill-hnikoloski-imlazy-imlazy-wordpress
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# WordPress + ACF Development Skill

Comprehensive guidance for WordPress theme/plugin development and ACF field management.

> This skill incorporates patterns from the official [WordPress Agent Skills](https://github.com/WordPress/agent-skills) — see their skills/ directory for deeper dives on specific topics.

Cross-reference: use `Skill(imlazy-php-review)` for PHP-level code review of any WordPress code encountered.

## When to Activate

- Building or modifying WordPress themes (classic or block)
- Creating or reviewing WordPress plugins
- Working with ACF field groups, blocks, or options pages
- Implementing custom REST API endpoints
- Enqueuing scripts/styles or registering blocks
- Performing WordPress security review
- Writing WP_Query loops or custom database queries

## WordPress Core

### 1. Theme Development

#### Template Hierarchy

WordPress resolves page templates in a strict order. Understand the file that will load:

```
single-{post-type}-{slug}.php  →  single-{post-type}.php  →  single.php  →  singular.php  →  index.php
page-{slug}.php               →  page-{id}.php            →  page.php    →  singular.php  →  index.php
category-{slug}.php           →  category-{id}.php        →  category.php →  archive.php   →  index.php
tag-{slug}.php                →  tag-{id}.php             →  tag.php     →  archive.php   →  index.php
taxonomy-{tax}-{term}.php     →  taxonomy-{tax}.php       →  taxonomy.php →  archive.php   →  index.php
archive-{post-type}.php       →  archive.php              →  index.php
```

Use `is_*()` conditionals in `index.php` as a fallback, not as a replacement for the hierarchy.

#### Block Themes (FSE)

```php
// theme.json — the heart of a block theme
{
  "version": 2,
  "settings": {
    "color": {
      "palette": [
        { "slug": "primary", "color": "#1d2327", "name": "Primary" },
        { "slug": "accent",  "color": "#007cba", "name": "Accent" }
      ]
    },
    "typography": {
      "fontSizes": [
        { "slug": "small", "size": "13px", "name": "Small" },
        { "slug": "medium", "size": "20px", "name": "Medium" }
      ]
    }
  },
  "templates": {
    "index": { "title": "Index", "postTypes": [ "page", "post" ] }
  }
}
```

#### Classic Theme Structure

```
themes/your-theme/
├── style.css              # Required: Theme header comment block
├── index.php              # Required: Main fallback template
├── functions.php          # Theme setup, enqueues, helpers
├── template-parts/        # Reusable partials (header, footer, loops)
├── page-templates/        # Custom page templates (full-width, landing, etc.)
└── assets/                # CSS, JS, images
```

Style.css header:

```css
/*
Theme Name: Your Theme
Theme URI:  https://example.com
Author:     You
Description: Description of your theme.
Version:    1.0.0
License:    GPL-2.0-or-later
Text Domain: your-theme
*/
```

### 2. Plugin Architecture

#### Main Plugin File

```php
 'book',
    'posts_per_page' => 10,
    'meta_key'       => 'rating',
    'orderby'        => 'meta_value_num',
    'order'          => 'DESC',
    'paged'          => get_query_var('paged', 1),
    'tax_query'      => [
        [
            'taxonomy' => 'genre',
            'field'    => 'slug',
            'terms'    => 'fiction',
        ],
    ],
]);

if ($query->have_posts()) :
    while ($query->have_posts()) : $query->the_post();
        the_title();
    endwhile;
    wp_reset_postdata();
endif;
```

#### WP_User_Query

```php
$user_query = new WP_User_Query([
    'role'    => 'subscriber',
    'number'  => 50,
    'orderby' => 'registered',
    'fields'  => ['ID', 'display_name', 'user_email'],
]);
$users = $user_query->get_results();
```

#### get_posts vs query_posts

```php
// PASS: get_posts() — returns array, no global state pollution
$posts = get_posts([
    'post_type'      => 'post',
    'posts_per_page' => 5,
    'meta_key'       => 'featured',
    'meta_value'     => '1',
]);

// FAIL: query_posts() — modifies main query, breaks pagination, pollutes global state
query_posts(['posts_per_page' => 1]);
// NEVER use query_posts(). Use pre_get_posts to alter the main query.
```

#### Pre-get_posts to Modify Main Queries

```php
// PASS: Modify main query via pre_get_posts
add_action('pre_get_posts', function (WP_Query $query) {
    if (!is_admin() && $query->is_main_query() && is_home()) {
        $query->set('posts_per_page', 20);
        $query->set('meta_key', 'featured');
        $query->set('meta_value', '1');
    }
});
```

### 5. REST API

#### Custom Endpoints

```php
// PASS: Register custom REST route
add_action('rest_api_init', function () {
    register_rest_route('my-plugin/v1', '/books/(?P\d+)', [
        'methods'             => 'GET',
        'callback'            => 'my_plugin_get_book',
        'permission_callback' => function (WP_REST_Request $request) {
            return current_user_can('edit_posts');
        },
        'args' => [
            'id' => [
                'required'          => true,
                'validate_callback' => function ($param) {
                    return is_numeric($param);
                },
                'sanitize_callback' => 'absint',
            ],
        ],
    ]);
});

function my_plugin_get_book(WP_REST_Request $request) {
    $post = get_post($request['id']);

    if (!$post || $post->post_type !== 'book') {
        return new WP_Error('not_found', 'Book not found', ['status' => 404]);
    }

    return new WP_REST_Response([
        'id'    => $post->ID,
        'title' => $post->post_title,
        'meta'  => get_post_meta($post->ID),
    ], 200);
}
```

#### Authentication

```php
// Cookie authentication (logged-in users) — automatic for same-origin
// Application password authentication — use for external clients
add_filter('rest_authentication_errors', function ($errors) {
    if (!is_user_logged_in()) {
        return new WP_Error('rest_not_logged_in', 'Not authenticated', ['status' => 401]);
    }
    return $errors;
});

// OAuth / JWT — use a dedicated plugin (e.g., WP OAuth Server, JWT Auth)
```

#### Permissions Best Practices

```php
// Minimum capability mapping:
// GET    items  →  read / publish_posts
// POST   items  →  edit_posts
// PUT    items  →  edit_others_posts
// DELETE items  →  delete_posts

register_rest_route('my-plugin/v1', '/settings', [
    'methods'             => 'GET',
    'callback'            => 'my_plugin_get_settings',
    'permission_callback' => function () {
        return current_user_can('manage_options');  // Admin only
    },
]);
```

#### Controller Classes (WP_REST_Controller)

For non-trivial endpoints, subclass `WP_REST_Controller` for CRUD consistency:

```php
class My_Plugin_Books_Controller extends WP_REST_Controller {
    protected $namespace = 'my-plugin/v1';
    protected $rest_base = 'books';

    public function register_routes() {
        register_rest_route($this->namespace, "/{$this->rest_base}", [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [$this, 'get_items'],
                'permission_callback' => [$this, 'get_items_permissions_check'],
            ],
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [$this, 'create_item'],
                'permission_callback' => [$this, 'create_item_permissions_check'],
            ],
        ]);
        register_rest_route($this->namespace, "/{$this->rest_base}/(?P\d+)", [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [$this, 'get_item'],
                'permission_callback' => [$this, 'get_item_permissions_check'],
                'args'                => $this->get_endpoint_args_for_item_schema(false),
            ],
        ]);
    }

    public function get_items_permissions_check($request) {
        return current_user_can('edit_posts');
    }

    public function get_items($request) {
        $posts = get_posts([
            'post_type'      => 'book',
            'posts_per_page' => $request['per_page'] ?? 10,
            'paged'          => $request['page'] ?? 1,
        ]);

        $data = [];
        foreach ($posts as $post) {
            $data[] = $this->prepare_response_for_collection(
                $this->prepare_item_for_response($post, $request)
            );
        }

        return rest_ensure_response($data);
    }

    public function prepare_item_for_response($item, $request) {
        $post = $item instanceof WP_Post ? $item : get_post($item);
        $data = [
            'id'      => $post->ID,
            'title'   => $post->post_title,
            'content' => $post->post_content,
            'meta'    => get_post_meta($post->ID),
        ];
        return rest_ensure_response($data);
    }

    public function get_item_schema() {
        return [
            '$schema'    => 'http://json-schema.org/draft-04/schema#',
            'title'      => 'book',
            'type'       => 'object',
            'properties' => [
                'id'      => ['type' => 'integer'],
                'title'   => ['type' => 'string'],
                'content' => ['type' => 'string'],
            ],
        ];
    }
}

add_action('rest_api_init', function () {
    $controller = new My_Plugin_Books_Controller();
    $controller->register_routes();
});
```

#### Schema Validation & Discovery

```php
// Validate args against schema automatically
register_rest_route('my-plugin/v1', '/books', [
    'methods'             => 'GET',
    'callback'            => 'my_plugin_get_books',
    'permission_callback' => '__return_true',
    'args'                => [
        'category' => [
            'type'              => 'string',
            'validate_callback'  => function ($value) {
                return in_array($value, ['fiction', 'non-fiction', 'sci-fi']);
            },
        ],
        'per_page' => [
            'type'    => 'integer',
            'default' => 10,
            'minimum' => 1,
            'maximum' => 100,
        ],
        'page' => [
            'type'    => 'integer',
            'default' => 1,
            'minimum' => 1,
        ],
    ],
]);

// Pagination headers (set automatically when using WP_REST_Controller)
// Response headers: X-WP-Total, X-WP-TotalPages

// Discovery: ensure your namespace appears in the /wp-json/ index
// Use rest_get_route_for_post_type_items() for core-compatible route URLs
$route = rest_get_route_for_post_type_items('book');
// Returns: /wp/v2/books (when rest_base is set properly)

// Expose meta in REST responses
register_post_meta('book', 'isbn', [
    'show_in_rest'  => true,
    'type'          => 'string',
    'single'        => true,
    'auth_callback' => function () {
        return current_user_can('edit_posts');
    },
]);

// Support _fields for sparse responses
// GET /wp-json/my-plugin/v1/books?_fields=id,title
```

### 6. Block Editor / Gutenberg

#### block.json

```json
{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "my-plugin/book-card",
  "title": "Book Card",
  "category": "widgets",
  "icon": "book",
  "description": "Display a book with cover, title, and author.",
  "keywords": ["book", "card", "library"],
  "version": "1.0.0",
  "textdomain": "my-plugin",
  "editorScript": "file:./index.js",
  "style": "file:./style-index.css",
  "render": "file:./render.php",
  "attributes": {
    "bookId": {
      "type": "number",
      "default": 0
    },
    "showRating": {
      "type": "boolean",
      "default": false
    }
  },
  "supports": {
    "align": ["wide", "full"],
    "color": {
      "background": true,
      "text": true
    }
  }
}
```

#### register_block_type

```php
// PASS: Register block from block.json metadata
add_action('init', function () {
    register_block_type(__DIR__ . '/build/book-card');
});

// Render callback in render.php (automatically loaded from block.json)
// >
//   
//     post_title); ?>
//     ID, 'author', true)); ?>
//   
// 

// Dynamic block with PHP render callback (alternative)
register_block_type('my-plugin/book-card', [
    'render_callback' => function ($attributes, $content, $block) {
        $post = get_post($attributes['bookId']);
        if (!$post) return '';

        ob_start();
        ?>
        >
            post_title); ?>
        
        
            {props.attributes.title}
          
        );
      },
      migrate(attributes) {
        // Transform old attributes to new format
        return {
          ...attributes,
          layout: attributes.layout || 'card',
          version: 2,
        };
      },
    },
    {
      // Even older version
      attributes: { bookId: { type: 'number' } },
      save: function () {
        return ...;
      },
    },
  ],
});
```

#### PHP-Side Deprecations (Dynamic Blocks)

Dynamic blocks (`render.php`) don't save markup to post content, so they rarely need `deprecated`. However, if you change attributes, still add a `deprecated` entry with `save: () => null` and a `migrate` callback:

```javascript
deprecated: [
  {
    attributes: { oldAttr: { type: 'string' } },
    save: () => null,
    migrate(attributes) {
      return { newAttr: attributes.oldAttr };
    },
  },
],
```

#### Rules of Deprecation

- Never remove a deprecated entry that still exists in stored post content
- Each deprecation must include the exact old `save()` output and attribute shape
- Use `migrate()` to normalize old attribute values to new schema
- Test by creating a post with the old block, switching to the new code, and reloading
- List entries newest-first (WordPress tries them in order)

### 8. Interactivity API (data-wp-*)

For interactive frontend behavior without full React, use `viewScriptModule` with the `@wordpress/interactivity` API.

#### block.json Setup

```json
{
  "apiVersion": 3,
  "name": "my-plugin/like-button",
  "title": "Like Button",
  "viewScriptModule": "file:./view.js",
  "render": "file:./render.php",
  "attributes": {
    "postId": { "type": "number" },
    "likes":  { "type": "number", "default": 0 }
  }
}
```

#### view.js (Module Script)

```javascript
import { store, getContext, getElement } from '@wordpress/interactivity';

const { state } = store('my-plugin/like-button', {
  state: {
    get likes() {
      const context = getContext();
      return context.likes;
    },
    get isLiked() {
      const context = getContext();
      return context.isLiked;
    },
  },
  actions: {
    toggleLike() {
      const context = getContext();
      context.isLiked = !context.isLiked;
      context.likes += context.isLiked ? 1 : -1;
    },
  },
  callbacks: {
    logView() {
      console.log('Block viewed:', getContext().postId);
    },
  },
});
```

#### render.php

```php

  data-wp-interactive="my-plugin/like-button"
   $attributes['postId'],
    'likes'   => $attributes['likes'],
    'isLiked' => false,
  ]); ?>
>
  
     Likes
  
  ❤️ Liked!

```

#### viewScript vs viewScriptModule

| Feature          | `viewScript`                    | `viewScriptModule`                    |
|------------------|----------------------------------|----------------------------------------|
| Module type      | Regular ES5 JS (IIFE)            | ES module (`type="module"`)            |
| Interactivity API| Manual store setup               | Built-in `@wordpress/interactivity`    |
| Dependencies     | `wp-interactivity` as dependency | No dependency array needed             |
| Browser support  | All browsers                     | Modern browsers (ES modules)           |
| Prefer when      | Simple scripts, legacy compat    | New development, Interactivity API     |

Use `viewScriptModule` for new development targeting WordPress 6.5+.

### 9. WP-CLI & Operations

WP-CLI is the command-line tool for WordPress management. Always confirm the environment (dev/staging/prod) before running write commands.

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [hnikoloski](https://github.com/hnikoloski)
- **Source:** [hnikoloski/imlazy](https://github.com/hnikoloski/imlazy)
- **License:** MIT
- **Homepage:** https://hnikoloski.github.io/imlazy/

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** yes
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** yes

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: flagged — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-hnikoloski-imlazy-imlazy-wordpress
- Seller: https://agentstack.voostack.com/s/hnikoloski
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
