From 354de174dffe0985d034f5b975c599d1c5c0884a Mon Sep 17 00:00:00 2001 From: luisherranz Date: Wed, 25 Feb 2026 10:46:14 +0100 Subject: [PATCH 1/3] Update block-development skill for clarity and add static vs dynamic --- skills/wp-block-development/SKILL.md | 22 +++++++++--- .../references/static-vs-dynamic-blocks.md | 35 +++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 skills/wp-block-development/references/static-vs-dynamic-blocks.md diff --git a/skills/wp-block-development/SKILL.md b/skills/wp-block-development/SKILL.md index 110e9d2..1314179 100644 --- a/skills/wp-block-development/SKILL.md +++ b/skills/wp-block-development/SKILL.md @@ -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) @@ -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: @@ -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: @@ -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 @@ -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) @@ -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 @@ -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") @@ -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 @@ -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 diff --git a/skills/wp-block-development/references/static-vs-dynamic-blocks.md b/skills/wp-block-development/references/static-vs-dynamic-blocks.md new file mode 100644 index 0000000..a514615 --- /dev/null +++ b/skills/wp-block-development/references/static-vs-dynamic-blocks.md @@ -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. From c1129341a2b078e1335914099d99962f5c2632a1 Mon Sep 17 00:00:00 2001 From: luisherranz Date: Wed, 25 Feb 2026 11:13:37 +0100 Subject: [PATCH 2/3] Add eval scenarios for choosing dynamic and static blocks in various contexts --- .../block-choose-dynamic-for-accordion.json | 22 +++++++++++++++++++ ...lock-choose-dynamic-for-pricing-table.json | 20 +++++++++++++++++ ...choose-dynamic-for-restricted-content.json | 22 +++++++++++++++++++ ...block-choose-dynamic-for-team-members.json | 22 +++++++++++++++++++ .../block-choose-dynamic-for-testimonial.json | 20 +++++++++++++++++ .../block-choose-dynamic-for-theme-block.json | 20 +++++++++++++++++ .../block-choose-static-for-callout.json | 20 +++++++++++++++++ ...ck-choose-static-for-portable-content.json | 21 ++++++++++++++++++ 8 files changed, 167 insertions(+) create mode 100644 eval/scenarios/block-choose-dynamic-for-accordion.json create mode 100644 eval/scenarios/block-choose-dynamic-for-pricing-table.json create mode 100644 eval/scenarios/block-choose-dynamic-for-restricted-content.json create mode 100644 eval/scenarios/block-choose-dynamic-for-team-members.json create mode 100644 eval/scenarios/block-choose-dynamic-for-testimonial.json create mode 100644 eval/scenarios/block-choose-dynamic-for-theme-block.json create mode 100644 eval/scenarios/block-choose-static-for-callout.json create mode 100644 eval/scenarios/block-choose-static-for-portable-content.json diff --git a/eval/scenarios/block-choose-dynamic-for-accordion.json b/eval/scenarios/block-choose-dynamic-for-accordion.json new file mode 100644 index 0000000..70488ba --- /dev/null +++ b/eval/scenarios/block-choose-dynamic-for-accordion.json @@ -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" + ] +} diff --git a/eval/scenarios/block-choose-dynamic-for-pricing-table.json b/eval/scenarios/block-choose-dynamic-for-pricing-table.json new file mode 100644 index 0000000..eef09e0 --- /dev/null +++ b/eval/scenarios/block-choose-dynamic-for-pricing-table.json @@ -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()" + ] +} diff --git a/eval/scenarios/block-choose-dynamic-for-restricted-content.json b/eval/scenarios/block-choose-dynamic-for-restricted-content.json new file mode 100644 index 0000000..83d1384 --- /dev/null +++ b/eval/scenarios/block-choose-dynamic-for-restricted-content.json @@ -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" + ] +} diff --git a/eval/scenarios/block-choose-dynamic-for-team-members.json b/eval/scenarios/block-choose-dynamic-for-team-members.json new file mode 100644 index 0000000..89a924c --- /dev/null +++ b/eval/scenarios/block-choose-dynamic-for-team-members.json @@ -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" + ] +} diff --git a/eval/scenarios/block-choose-dynamic-for-testimonial.json b/eval/scenarios/block-choose-dynamic-for-testimonial.json new file mode 100644 index 0000000..07cc702 --- /dev/null +++ b/eval/scenarios/block-choose-dynamic-for-testimonial.json @@ -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()" + ] +} diff --git a/eval/scenarios/block-choose-dynamic-for-theme-block.json b/eval/scenarios/block-choose-dynamic-for-theme-block.json new file mode 100644 index 0000000..bd1c8c6 --- /dev/null +++ b/eval/scenarios/block-choose-dynamic-for-theme-block.json @@ -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" + ] +} diff --git a/eval/scenarios/block-choose-static-for-callout.json b/eval/scenarios/block-choose-static-for-callout.json new file mode 100644 index 0000000..a4d8dc2 --- /dev/null +++ b/eval/scenarios/block-choose-static-for-callout.json @@ -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" + ] +} diff --git a/eval/scenarios/block-choose-static-for-portable-content.json b/eval/scenarios/block-choose-static-for-portable-content.json new file mode 100644 index 0000000..92855c4 --- /dev/null +++ b/eval/scenarios/block-choose-static-for-portable-content.json @@ -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" + ] +} From 27e9e99ebea7a7168a010d746874e5f69cc30303 Mon Sep 17 00:00:00 2001 From: luisherranz Date: Wed, 25 Feb 2026 11:20:15 +0100 Subject: [PATCH 3/3] Update existing eval scenarios --- ...block-create-interactive-inner-blocks.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/eval/scenarios/block-create-interactive-inner-blocks.json b/eval/scenarios/block-create-interactive-inner-blocks.json index 66af5e8..d90526f 100644 --- a/eval/scenarios/block-create-interactive-inner-blocks.json +++ b/eval/scenarios/block-create-interactive-inner-blocks.json @@ -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",