Use when building or hardening WordPress sites or WooCommerce stores and treating WordPress as the product rather than just writing PHP — block themes with theme.json, plugins with block.json and proper hooks, wp-config security hardening, performance (object cache, asset loading, autoloaded options), and WP-CLI operations. NOT a Laravel app (that is `laravel`).
Scanned 9/2/2026
Install to Claude Code
npx -y skills add ericrisco/rsc-harness --skill wordpress --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Wordpress?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/ericrisco-wordpress)More formats (shields.io, HTML) on the badges page.
---
name: wordpress
description: "Use when building or hardening WordPress sites or WooCommerce stores and treating WordPress as the product rather than just writing PHP — block themes with theme.json, plugins with block.json and proper hooks, wp-config security hardening, performance (object cache, asset loading, autoloaded options), and WP-CLI operations. NOT a Laravel app (that is `laravel`)."
tags: [wordpress, woocommerce, block-themes, wp-cli, php, security-hardening, performance]
recommends: [php, secure-coding, performance, mysql, shopify]
origin: risco
---
# WordPress
You are building the WordPress application layer — themes, plugins, the dashboard/CLI surface, security, and stores — for someone treating WordPress as the product. Know the difference between a `functions.php` hack and a real plugin. Refuse to paste secrets into the dashboard file editor. Pick block themes over page builders by default. Reach for `wp` before clicking through wp-admin.
This is not generic PHP. If there is no WordPress API in sight (value objects, Composer/PSR-4, PHPStan), that is [php](../php/SKILL.md). A Laravel app is [laravel](../laravel/SKILL.md).
## Version & runtime targeting
Target current. Stale version assumptions are the most common way WP code rots.
- **WordPress 6.9** (released 2025-12-02) is current; it refines speculative loading and supports PHP 8.5. WP 6.8 ("Cecil", 2025-04-15) added bcrypt password hashing and speculative loading. *Why: features below assume 6.6+ APIs (`wp_enqueue_block_style`, theme.json v3).*
- **PHP 8.4 is the recommended runtime for 2026.** WP 6.9 fully supports PHP 8.5; WP 6.8+ fully supports 8.4; WP 6.4+ supports 8.3. **Never target PHP 7.x or 8.0–8.2 for new code — all EOL.** *Why: writing for a dead runtime ships deprecation bugs the host will eventually refuse to run.*
- **theme.json v3** is the schema for new block themes (WP 6.6+). Locally set `WP_DEVELOPMENT_MODE` to `all` so theme.json edits are not cached for 30+ seconds. *Why: without it you will edit theme.json, see nothing change, and waste an hour.*
```php
// wp-config.php — local dev only, never on production
define( 'WP_DEVELOPMENT_MODE', 'all' );
define( 'WP_DEBUG', true );
define( 'SCRIPT_DEBUG', true );
```
## Decision: how heavy is the change?
Pick the lightest artifact that survives an update. The failure mode is everyone reaching for `functions.php` or editing files that get overwritten.
| You need to… | Artifact | Do NOT |
| --- | --- | --- |
| One-line tweak, site-specific behavior | mu-plugin in `wp-content/mu-plugins/` | dump it in the active theme's `functions.php` |
| Change look of a third-party theme | **child theme** | edit the parent theme — updates wipe it |
| Reusable feature, hooks, blocks, CPTs | **standalone plugin** | hide app logic in `functions.php` |
| Full custom front end | **block theme** + theme.json v3 | reach for a page builder for structural layout |
| Sell products | block theme + **WooCommerce** | hand-roll a cart |
## Block themes
A block theme is `theme.json` + HTML templates in `templates/` and `parts/`. Start from a v3 skeleton:
```json
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"appearanceTools": true,
"color": {
"palette": [
{ "slug": "base", "color": "#ffffff", "name": "Base" },
{ "slug": "contrast", "color": "#111111", "name": "Contrast" }
]
},
"typography": {
"fluid": true,
"fontFamilies": [
{ "fontFamily": "system-ui, sans-serif", "slug": "system", "name": "System" }
]
},
"layout": { "contentSize": "640px", "wideSize": "1100px" }
},
"styles": {
"color": { "background": "var(--wp--preset--color--base)", "text": "var(--wp--preset--color--contrast)" }
}
}
```
Templates resolve by hierarchy: `templates/index.html` is the fallback; `single.html`, `archive.html`, `page.html`, `404.html` override it. Reusable chunks live in `parts/` (`header.html`, `footer.html`).
Register patterns by dropping PHP-headered HTML in `patterns/`; they appear in the inserter automatically. Enqueue CSS **per block** so it only loads when the block renders:
```php
add_action( 'init', function () {
wp_enqueue_block_style( 'core/group', array(
'handle' => 'mytheme-group',
'src' => get_theme_file_uri( 'assets/blocks/group.css' ),
'path' => get_theme_file_path( 'assets/blocks/group.css' ),
) );
} );
```
Rule: never hand-write `<link>`/`<script>` tags in templates — the browser cannot cache them and you lose dependency management. Always enqueue. And never edit a parent theme directly; make a child theme.
## Plugins
A plugin is a folder with a headered main file. That header is what makes it a plugin:
```php
<?php
/**
* Plugin Name: Acme Books
* Description: Registers the book CPT and a related block.
* Version: 1.0.0
* Requires PHP: 8.4
* Requires at least: 6.9
*/
defined( 'ABSPATH' ) || exit; // never expose a direct hit
```
Hooks split two ways: **actions** fire at a point in execution (`add_action('init', …)`); **filters** transform a value and must `return` it (`add_filter('the_content', …)`). Confusing them is a top-five WP bug.
Register a block from a `block.json` — never duplicate metadata in PHP:
```php
add_action( 'init', function () {
register_block_type( __DIR__ . '/build/related-books' ); // reads block.json
} );
```
Register a custom post type with explicit capabilities and labels:
```php
add_action( 'init', function () {
register_post_type( 'book', array(
'public' => true,
'show_in_rest' => true, // required for the block editor
'supports' => array( 'title', 'editor', 'thumbnail' ),
'labels' => array( 'name' => 'Books', 'singular_name' => 'Book' ),
) );
} );
```
Every write path needs a **capability check + nonce**, and every output/query needs escaping/sanitizing. This is the line between a plugin and a vulnerability.
```php
// Bad — SQL injection, no auth
$rows = $wpdb->get_results( "SELECT * FROM {$wpdb->posts} WHERE post_author = $id" );
// Good — capability, nonce, prepared statement
if ( ! current_user_can( 'edit_posts' ) ) { wp_die( 'nope' ); }
check_admin_referer( 'acme_save' );
$rows = $wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_author = %d", $id
) );
echo esc_html( $rows[0]->post_title );
```
Sanitize on input (`sanitize_text_field`, `absint`), escape on output (`esc_html`, `esc_attr`, `esc_url`, `wp_kses` for allowed HTML).
## Security hardening
The single highest-impact habit is keeping core, plugins, and themes updated — 2024 saw 7,966 new ecosystem vulnerabilities, +34% YoY, and the vast majority hit known-vulnerable extensions. Baseline `wp-config.php` constants:
```php
define( 'DISALLOW_FILE_EDIT', true ); // kills dashboard Theme/Plugin editor; SFTP/Git unaffected
define( 'WP_AUTO_UPDATE_CORE', 'minor' );
define( 'FORCE_SSL_ADMIN', true );
```
Use **Application Passwords** (WP 5.6+) for REST/API machine auth — individually revocable, cannot log into `wp-login.php`, and the right answer when a script or [nextjs](../nextjs/SKILL.md) front end needs to talk to WP. Never reuse the admin password in a script.
For the full block — file-permission matrix (dirs 755 / files 644 / wp-config 600), `DISALLOW_FILE_MODS`, nginx/.htaccess security headers, login lockdown, XML-RPC and user-enumeration disabling, version-disclosure removal — see [references/hardening.md](references/hardening.md). Generic OWASP/threat-modeling not tied to the WP attack surface is [secure-coding](../secure-coding/SKILL.md).
## Performance
Two caches, different jobs: an **object cache** (Redis/Memcached via a drop-in) memoizes DB query results across requests; a **page cache** stores whole rendered HTML pages. You usually want both. Then:
- **Audit autoloaded options** first when wp-admin crawls — a few MB of autoloaded junk loads on *every* request. Find offenders with the SQL in the reference.
- Enqueue assets per block (above); defer/async non-critical scripts.
- Speculative loading (prerender/prefetch on hover) ships in 6.8+; tune it rather than bolting on a plugin.
Full Redis wiring, the autoload audit query, transients, and a WP-tied Core Web Vitals checklist are in [references/performance.md](references/performance.md). Generic CWV/load-testing methodology divorced from WordPress is the [performance](../performance/SKILL.md) sibling.
## WooCommerce
**HPOS** (High-Performance Order Storage) is the default order datastore for new installs since WooCommerce 8.2 — orders live in dedicated tables, not `wp_posts`. Enable/migrate and verify with WP-CLI:
```bash
wp wc hpos enable # turn on custom order tables
wp wc cot sync # backfill/sync existing orders
wp wc cot verify_cot_data # confirm parity before flipping authority
```
Any plugin that touches orders **must declare HPOS compatibility**, or WooCommerce disables it in HPOS mode:
```php
add_action( 'before_woocommerce_init', function () {
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables', __FILE__, true
);
}
} );
```
Store/checkout/product setup checklist and migration caveats: [references/woocommerce.md](references/woocommerce.md). A Shopify store is the [shopify](../shopify/SKILL.md) sibling.
## WP-CLI & local dev
`wp` is the operational backbone — scriptable, faster than the dashboard, and safe on serialized data.
- **Local stack:** `wp-env start` (Dockerized WP). Scaffold a block/plugin with `npx @wordpress/create-block acme-block`.
- **Migrations:** `wp search-replace` is serialization-safe — never run a raw SQL `REPLACE` on the DB, it corrupts serialized arrays. Dry-run first:
```bash
wp search-replace 'https://staging.example.com' 'https://example.com' --all-tables --dry-run
wp db export backup.sql # always export before a real run
wp search-replace 'https://staging.example.com' 'https://example.com' --all-tables
```
- **Cron:** WP's pseudo-cron only fires on traffic. For reliable schedules, define `DISABLE_WP_CRON` true and run `wp cron event run --due-now` from real system cron.
## Anti-patterns
| Anti-pattern | Why it's wrong | Do instead |
| --- | --- | --- |
| App logic in `functions.php` | Dies with the theme; not portable | Standalone plugin or mu-plugin |
| Editing a parent theme / core files | Wiped on update | Child theme; hooks/filters |
| `query_posts()` | Breaks the main query, no pagination | `WP_Query` or `pre_get_posts` |
| `$wpdb->query("… $var …")` | SQL injection | `$wpdb->prepare()` with `%d`/`%s` |
| `<script src>`/`<link>` in templates | Not cached, no dependency graph | `wp_enqueue_script/style`, `wp_enqueue_block_style` |
| Secrets pasted in dashboard file editor | Logged, world-readable, version-control blind | `wp-config.php` constants + `DISALLOW_FILE_EDIT` |
| Page builder for structural layout | Bloat, lock-in, broken CWV | Block theme + patterns |
| Disabling auto-updates, never patching | 7,966 vulns/yr land on stale plugins | `WP_AUTO_UPDATE_CORE`; `wp plugin update --all` |
| Raw SQL `REPLACE` to change URLs | Corrupts serialized data | `wp search-replace` |
| Order-touching plugin without HPOS declaration | WooCommerce disables it | `FeaturesUtil::declare_compatibility` |
## verify.sh
`scripts/verify.sh <path>` statically scans theme/plugin/config code for these anti-patterns — committed DB creds or `AUTH_KEY`/salt literals, missing `DISALLOW_FILE_EDIT` when a `wp-config.php` is in scope, `eval(`/`base64_decode(`, `query_posts(`, unprepared `$wpdb` interpolation, and hardcoded `<script src=`/`<link rel="stylesheet">` in PHP templates. Read-only; exits 0 on a clean or empty target, non-zero on any FAIL so it can gate CI.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!