Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions eval/scenarios/block-choose-dynamic-for-accordion.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "Choose dynamic for interactive block needing Interactivity API",
"skills": ["wordpress-router", "wp-project-triage", "wp-block-development", "wp-interactivity-api"],
"query": "Add an FAQ block to our plugin — each question expands/collapses to reveal the answer when clicked.",
"expected_behavior": [
"Step 1: Run wordpress-router to classify repo kind",
"Step 2: Run wp-project-triage to detect project structure",
"Step 3: Route to wp-block-development and wp-interactivity-api skills",
"Step 4: Read references/static-vs-dynamic-blocks.md to decide rendering model",
"Step 5: Recognize expand/collapse requires Interactivity API, which needs dynamic rendering for hydration",
"Step 6: Choose dynamic rendering despite content being self-contained (user types the Q&A)",
"Step 7: Add render field in block.json pointing to a PHP render file",
"Step 8: Set up Interactivity API directives for expand/collapse behavior",
"Step 9: Keep save() returning null"
],
"success_criteria": [
"Chooses dynamic rendering (not static, even though Q&A content is user-entered)",
"Uses render field in block.json or render_callback",
"Uses Interactivity API directives (data-wp-*) for expand/collapse",
"Does not implement a save() that outputs FAQ markup"
]
}
20 changes: 20 additions & 0 deletions eval/scenarios/block-choose-dynamic-for-pricing-table.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "Choose dynamic for block with evolving markup",
"skills": ["wordpress-router", "wp-project-triage", "wp-block-development"],
"query": "Build a pricing table block for our SaaS product — we're launching in a couple of months. Three columns, one per tier, with a feature list and a CTA button.",
"expected_behavior": [
"Step 1: Run wordpress-router to classify repo kind",
"Step 2: Run wp-project-triage to detect project structure",
"Step 3: Route to wp-block-development skill",
"Step 4: Read references/static-vs-dynamic-blocks.md to decide rendering model",
"Step 5: Recognize that a pre-launch SaaS pricing page will go through design iterations",
"Step 6: Choose dynamic rendering to avoid deprecation chains when markup evolves",
"Step 7: Add render field in block.json pointing to a PHP render file",
"Step 8: Keep save() returning null"
],
"success_criteria": [
"Chooses dynamic rendering despite the block being structurally simple",
"Uses render field in block.json or render_callback",
"Does not freeze pricing markup in post content via save()"
]
}
22 changes: 22 additions & 0 deletions eval/scenarios/block-choose-dynamic-for-restricted-content.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "Choose dynamic for block requiring server-side auth checks",
"skills": ["wordpress-router", "wp-project-triage", "wp-block-development"],
"query": "Create a block that shows a 'Members only' section — logged-in subscribers see the content, everyone else sees a signup prompt. We'd also like the content to be indexable if we ever export the site.",
"expected_behavior": [
"Step 1: Run wordpress-router to classify repo kind",
"Step 2: Run wp-project-triage to detect project structure",
"Step 3: Route to wp-block-development skill",
"Step 4: Read references/static-vs-dynamic-blocks.md to decide rendering model",
"Step 5: Recognize the block needs server-side auth checks (current_user_can or is_user_logged_in)",
"Step 6: Recognize that the portability mention does not override the server-side requirement (higher priority in checklist)",
"Step 7: Choose dynamic rendering",
"Step 8: Add render field in block.json pointing to a PHP render file",
"Step 9: Implement PHP render file that checks user authentication before rendering content"
],
"success_criteria": [
"Chooses dynamic rendering despite portability concern mentioned in query",
"Server-side auth requirement takes priority over portability preference",
"Uses render field in block.json or render_callback",
"PHP render file checks user authentication"
]
}
22 changes: 22 additions & 0 deletions eval/scenarios/block-choose-dynamic-for-team-members.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "Choose dynamic for block with external data",
"skills": ["wordpress-router", "wp-project-triage", "wp-block-development"],
"query": "Create a block that shows our team members with their photo, role and bio.",
"expected_behavior": [
"Step 1: Run wordpress-router to classify repo kind",
"Step 2: Run wp-project-triage to detect project structure",
"Step 3: Route to wp-block-development skill",
"Step 4: Read references/static-vs-dynamic-blocks.md to decide rendering model",
"Step 5: Recognize team member data lives outside block attributes (CPT, users, or external source)",
"Step 6: Choose dynamic rendering based on external data dependency",
"Step 7: Add render field in block.json pointing to a PHP render file",
"Step 8: Implement PHP render file that queries team member data",
"Step 9: Keep save() returning null — no markup stored in post content"
],
"success_criteria": [
"Chooses dynamic rendering (not static)",
"Uses render field in block.json or render_callback",
"Does not implement a save() that outputs team member markup",
"PHP render file queries data at request time"
]
}
20 changes: 20 additions & 0 deletions eval/scenarios/block-choose-dynamic-for-testimonial.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "Default to dynamic for ambiguous block",
"skills": ["wordpress-router", "wp-project-triage", "wp-block-development"],
"query": "Create a testimonial block — quote, person's name, photo, and their company. We're still figuring out the final design for these.",
"expected_behavior": [
"Step 1: Run wordpress-router to classify repo kind",
"Step 2: Run wp-project-triage to detect project structure",
"Step 3: Route to wp-block-development skill",
"Step 4: Read references/static-vs-dynamic-blocks.md to decide rendering model",
"Step 5: Recognize that 'still figuring out the final design' implies markup will evolve",
"Step 6: Choose dynamic rendering to avoid deprecation chains as design iterates",
"Step 7: Add render field in block.json pointing to a PHP render file",
"Step 8: Keep save() returning null"
],
"success_criteria": [
"Chooses dynamic rendering",
"Uses render field in block.json or render_callback",
"Does not freeze testimonial markup in post content via save()"
]
}
20 changes: 20 additions & 0 deletions eval/scenarios/block-choose-dynamic-for-theme-block.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "Choose dynamic for block used in template part",
"skills": ["wordpress-router", "wp-project-triage", "wp-block-development"],
"query": "We need a custom footer block with our company info, social links, and copyright year for our footer template part.",
"expected_behavior": [
"Step 1: Run wordpress-router to classify repo kind",
"Step 2: Run wp-project-triage to detect project structure",
"Step 3: Route to wp-block-development skill",
"Step 4: Read references/static-vs-dynamic-blocks.md to decide rendering model",
"Step 5: Recognize that a footer template part implies block theme architecture, which is server-rendered",
"Step 6: Choose dynamic rendering despite the content being presentational",
"Step 7: Add render field in block.json pointing to a PHP render file",
"Step 8: Keep save() returning null"
],
"success_criteria": [
"Chooses dynamic rendering (not static, despite presentational content)",
"Recognizes template part context implies block theme / server rendering",
"Uses render field in block.json or render_callback"
]
}
20 changes: 20 additions & 0 deletions eval/scenarios/block-choose-static-for-callout.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "Choose static for simple presentational block",
"skills": ["wordpress-router", "wp-project-triage", "wp-block-development"],
"query": "We need a callout block — a colored box with an icon, a heading, and a paragraph. The editor picks the color, icon, and writes the text.",
"expected_behavior": [
"Step 1: Run wordpress-router to classify repo kind",
"Step 2: Run wp-project-triage to detect project structure",
"Step 3: Route to wp-block-development skill",
"Step 4: Read references/static-vs-dynamic-blocks.md to decide rendering model",
"Step 5: Recognize block is simple, presentational, and self-contained (all data from user-entered attributes)",
"Step 6: Confirm no higher-priority signals apply",
"Step 7: Choose static rendering",
"Step 8: Implement save() with useBlockProps.save()"
],
"success_criteria": [
"Chooses static rendering",
"Implements save() function with useBlockProps.save()",
"Does not create render.php or render_callback"
]
}
21 changes: 21 additions & 0 deletions eval/scenarios/block-choose-static-for-portable-content.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "Choose static for block needing content portability",
"skills": ["wordpress-router", "wp-project-triage", "wp-block-development"],
"query": "We're building a documentation site on WordPress but plan to migrate to a static site generator next year. Create a 'note' block for inline tips and warnings in our docs.",
"expected_behavior": [
"Step 1: Run wordpress-router to classify repo kind",
"Step 2: Run wp-project-triage to detect project structure",
"Step 3: Route to wp-block-development skill",
"Step 4: Read references/static-vs-dynamic-blocks.md to decide rendering model",
"Step 5: Recognize content portability is critical (planned migration to non-WordPress system)",
"Step 6: Confirm no higher-priority signals (no external data, no evolving markup, no interactivity, no template parts, no server-side needs)",
"Step 7: Choose static rendering so HTML is self-contained in the database",
"Step 8: Implement save() with useBlockProps.save()"
],
"success_criteria": [
"Chooses static rendering",
"Implements save() function with useBlockProps.save()",
"Does not create render.php or render_callback",
"Content is stored as self-contained HTML in post_content"
]
}
19 changes: 12 additions & 7 deletions eval/scenarios/block-create-interactive-inner-blocks.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@
"Step 1: Run wordpress-router to classify repo kind",
"Step 2: Run wp-project-triage to detect tooling and WP version",
"Step 3: Route to wp-block-development for scaffolding and wp-interactivity-api for directives",
"Step 4: Recommend @wordpress/create-block with interactive template",
"Step 5: Use useInnerBlocksProps/InnerBlocks for nested content",
"Step 6: Set up Interactivity API store with toggle action",
"Step 7: Add data-wp-interactive namespace to wrapper",
"Step 8: Add data-wp-on--click directive for toggle",
"Step 9: Add data-wp-class--active for conditional class",
"Step 10: Verify wrapper attributes are correct in edit and save/render"
"Step 4: Read references/static-vs-dynamic-blocks.md to decide rendering model",
"Step 5: Recognize that Interactivity API requires dynamic rendering for hydration",
"Step 6: Choose dynamic rendering — add render field in block.json, keep save() returning null",
"Step 7: Recommend @wordpress/create-block with interactive template",
"Step 8: Use useInnerBlocksProps/InnerBlocks for nested content",
"Step 9: Set up Interactivity API store with toggle action",
"Step 10: Add data-wp-interactive namespace to wrapper",
"Step 11: Add data-wp-on--click directive for toggle",
"Step 12: Add data-wp-class--active for conditional class",
"Step 13: Verify wrapper attributes are correct in edit and render"
],
"success_criteria": [
"Chooses dynamic rendering (Interactivity API requires it)",
"Uses render field in block.json or render_callback",
"Uses @wordpress/create-block for scaffolding",
"Uses useInnerBlocksProps for container pattern",
"Interactivity API directives properly set up",
Expand Down
22 changes: 18 additions & 4 deletions skills/wp-block-development/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Use this skill for block work such as:
- `node skills/wp-block-development/scripts/list_blocks.mjs`
3. Identify the block root (directory containing `block.json`) you’re changing.

If this repo is a full site (`wp-content/` present), be explicit about *which* plugin/theme contains the block.
If this repo is a full site (`wp-content/` present), be explicit about _which_ plugin/theme contains the block.

### 1) Create a new block (if needed)

Expand All @@ -43,6 +43,7 @@ If you are creating a new block, prefer scaffolding rather than hand-rolling str
- If you need Interactivity API from day 1, use the interactive template.

Read:

- `references/creating-new-blocks.md`

After scaffolding:
Expand All @@ -55,30 +56,37 @@ After scaffolding:
WordPress 6.9 enforces `apiVersion: 3` in the block.json schema. Blocks with apiVersion 2 or lower trigger console warnings when `SCRIPT_DEBUG` is enabled.

**Why this matters:**

- WordPress 7.0 will run the post editor in an iframe regardless of block apiVersion.
- apiVersion 3 ensures your block works correctly inside the iframed editor (style isolation, viewport units, media queries).

**Migration:** Changing from version 2 to 3 is usually as simple as updating the `apiVersion` field in `block.json`. However:

- Test in a local environment with the iframe editor enabled.
- Ensure any style handles are included in `block.json` (styles missing from the iframe won't apply).
- Third-party scripts attached to a specific `window` may have scoping issues.

Read:

- `references/block-json.md` (apiVersion and schema details)

### 3) Pick the right block model

- **Static block** (markup saved into post content): implement `save()`; keep attributes serialization stable.
- **Dynamic block** (server-rendered): use `render` in `block.json` (or `render_callback` in PHP) and keep `save()` minimal or `null`.
- **Interactive frontend behavior**:
- Prefer `viewScriptModule` for modern module-based view scripts where supported.
- If you're working primarily on `data-wp-*` directives or stores, also use `wp-interactivity-api`.

If the block is interactive, load the `wp-interactivity-api` skill.

Read before deciding:

- `references/static-vs-dynamic-blocks.md`

### 4) Update `block.json` safely

Make changes in the block’s `block.json`, then confirm registration matches metadata.

For field-by-field guidance, read:

- `references/block-json.md`

Common pitfalls:
Expand All @@ -96,6 +104,7 @@ Prefer PHP registration using metadata, especially when:
- you need conditional asset loading

Read and apply:

- `references/registration.md`

### 6) Implement edit/save/render patterns
Expand All @@ -107,6 +116,7 @@ Follow wrapper attribute best practices:
- Dynamic render (PHP): `get_block_wrapper_attributes()`

Read:

- `references/supports-and-wrappers.md`
- `references/dynamic-rendering.md` (if dynamic)

Expand All @@ -118,6 +128,7 @@ If your block is a “container” that nests other blocks, treat Inner Blocks a
- Keep migrations in mind if you change inner markup.

Read:

- `references/inner-blocks.md`

### 8) Attributes and serialization
Expand All @@ -128,6 +139,7 @@ Before changing attributes:
- avoid the deprecated `meta` attribute source

Read:

- `references/attributes-and-serialization.md`

### 9) Migrations and deprecations (avoid "Invalid block")
Expand All @@ -138,6 +150,7 @@ If you change saved markup or attributes:
2. Provide `save` for old versions and an optional `migrate` to normalize attributes.

Read:

- `references/deprecations.md`

### 10) Tooling and verification commands
Expand All @@ -148,6 +161,7 @@ Prefer whatever the repo already uses:
- `wp-env` (common) → use for local WP + E2E

Read:

- `references/tooling-and-testing.md`

## Verification
Expand Down
35 changes: 35 additions & 0 deletions skills/wp-block-development/references/static-vs-dynamic-blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Static vs dynamic blocks

Use this file when deciding whether a new block should save its markup to the database (static) or render on the server at request time (dynamic).

## Decision checklist

Ask these questions in order — the first YES wins:

1. Does the block depend on data outside its own attributes (latest posts, user info, site options, database queries)?
→ **Dynamic.**

2. Will the block's markup likely change over time (design updates, bug fixes, accessibility improvements)?
→ **Dynamic.** Static blocks freeze markup in the database; updating it later requires deprecations and manual re-saves.

3. Will the block use the Interactivity API?
→ **Dynamic.** Hydration requires server-rendered HTML to match the current code. Stale markup stored by a static block can cause mismatches.

4. Is the block part of Block Themes (templates, template parts, theme blocks)?
→ **Dynamic.** Block themes replace PHP themes, which are architecturally server-rendered.

5. Does the block need server-side capabilities (WordPress hooks/filters, authenticated data, PHP APIs)?
→ **Dynamic.**

6. Is content portability to non-WordPress systems critical (CMS migration, raw HTML export)?
→ **Static.** Static blocks store self-contained HTML that works outside WordPress without a PHP renderer.

7. Is the block simple, purely presentational, and unlikely to change (a styled quote, a separator, a callout)?
→ **Static** is fine — you avoid writing PHP entirely.

When in doubt → **default to dynamic.** The cost of choosing static and needing dynamic later (rewriting, deprecation chains) is higher than starting dynamic. Dynamic blocks keep markup fresh, avoid the deprecation treadmill, and align with the direction of WordPress (Block Themes, Interactivity API).

## Key differences at a glance

- **Static**: `save()` produces HTML → stored in `post_content` → served as-is. Changing `save()` later requires `deprecated` entries or every post must be re-saved.
- **Dynamic**: attributes stored in `post_content` → PHP `render` file (or `render_callback`) produces HTML at request time. Change the render function freely — no deprecations needed.