# Services Content Blocks Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build the missing content-area components for the Mapping services page (and any other service sub-page), matching `Mapping-Services.png`. The hero, header, and footer are already in place and must not be touched. **Architecture:** One new ACF block (`services-list`) that renders a row of cards. Card count is editor-driven (2, 3, or 4); card size and grid layout adapt to the count. The orange horizontal line that bisects the images is rendered by the block itself as a full-bleed SVG background. The closing "At CWC, mapping is about more than data…" block reuses the existing `pull-quote` block — no new build needed. Editor composes the page in Gutenberg by adding `Services List` and `Pull Quote` blocks to any service sub-page; `page.php` already calls `the_content()`, so no template change is required. **Tech Stack:** WordPress 6.x, PHP 8.x, Tailwind CSS v4, Playwright, axe-core, PHPCS (WordPress standard). ## Global Constraints - Tabs for PHP indentation (project standard). - `static/dist/theme.css` is committed (not gitignored) per project convention. - PHPCS uses the WordPress coding standard; `composer lint` must pass for all touched files. - All design colors use the project's existing CSS custom properties from `styles/base/colors.css`. No hardcoded hex values for theme colors. - Reuse the existing post-title accent pattern (blue heading + orange `::after` underline from `.post-title h1` in `styles/base/misc.css`) for consistency. Do not introduce a new accent style. - Reuse the existing `pull-quote` block for the closing statement — no duplicate block. - Page template `page.php` is not modified. The page-children block is not used. - This work does **not** edit `header.php`, `footer.php`, `views/partials/page-hero.php`, or `views/partials/page-hero-services.php`. - Tailwind v4 picks up class names from source files at build time. The dynamic `lg:grid-cols-N` class needs to literally appear in the source PHP for Tailwind to emit the rule. - The `static/img/services-list-line.svg` is a fixed asset; do not make it editor-uploadable. - All commits follow the project's emoji-prefixed commit-message style (e.g. `✨ feat: …`). ## File Structure | File | Responsibility | Created/Modified | | --- | --- | --- | | `views/blocks/services-list/services-list.php` | Block template (PHP) — renders the row of cards. | Create | | `views/blocks/services-list/services-list.css` | Block styles (CSS) — layout, headings, bisecting line. | Create | | `views/blocks/services-list/block.json` | Block registration metadata (name, category, icon, render template). | Create | | `acf/group_services_list.json` | ACF field group for the `acf/services-list` block. | Create | | `static/img/services-list-line.svg` | Decorative orange horizontal line SVG that bisects the images. | Create | | `styles/blocks/index.css` | Import the new block's CSS. | Modify | | `tests/services-page.spec.js` | Playwright tests for the rendered block. | Create | | `static/dist/theme.css` | Rebuilt via `npm run build`. | Modified | --- ## Task 1: ACF Field Group **Files:** - Create: `acf/group_services_list.json` **Interfaces:** - Produces: ACF group with `key: "group_services_list"`, location scoped to `block == acf/services-list`, fields: `items` (repeater) → `image`, `heading`, `description`. - [ ] **Step 1: Create the ACF field group JSON file** Create the file `acf/group_services_list.json` with the following exact content: ```json { "key": "group_services_list", "title": "Services List", "fields": [ { "key": "field_services_items", "label": "Items", "name": "items", "aria-label": "", "type": "repeater", "instructions": "Add 2, 3, or 4 cards. The grid layout adapts to the count.", "required": 0, "conditional_logic": 0, "wrapper": { "width": "", "class": "", "id": "" }, "layout": "block", "pagination": 0, "min": 0, "max": 0, "collapsed": "", "button_label": "Add Service", "rows_per_page": 20, "sub_fields": [ { "key": "field_services_item_image", "label": "Image", "name": "image", "aria-label": "", "type": "image", "instructions": "", "required": 0, "conditional_logic": 0, "wrapper": { "width": "", "class": "", "id": "" }, "return_format": "array", "library": "all", "min_width": "", "min_height": "", "min_size": "", "max_width": "", "max_height": "", "max_size": "", "mime_types": "", "allow_in_bindings": 0, "preview_size": "medium" }, { "key": "field_services_item_heading", "label": "Heading", "name": "heading", "aria-label": "", "type": "text", "instructions": "", "required": 0, "conditional_logic": 0, "wrapper": { "width": "", "class": "", "id": "" }, "default_value": "", "maxlength": "", "allow_in_bindings": 0, "placeholder": "", "prepend": "", "append": "" }, { "key": "field_services_item_description", "label": "Description", "name": "description", "aria-label": "", "type": "textarea", "instructions": "", "required": 0, "conditional_logic": 0, "wrapper": { "width": "", "class": "", "id": "" }, "default_value": "", "placeholder": "", "maxlength": "", "rows": 4, "new_lines": "" } ] } ], "location": [ [ { "param": "block", "operator": "==", "value": "acf/services-list" } ] ], "menu_order": 0, "position": "normal", "style": "default", "label_placement": "top", "instruction_placement": "label", "hide_on_screen": "", "active": true, "description": "Service cards row. Card count (2-4) determines the layout.", "show_in_rest": 0, "display_title": "", "allow_ai_access": false, "ai_description": "", "modified": 1753305600 } ``` - [ ] **Step 2: Verify the JSON parses** Run: `node -e "JSON.parse(require('fs').readFileSync('acf/group_services_list.json','utf8')); console.log('OK')"` Expected: prints `OK` and exits 0. - [ ] **Step 3: Commit** ```bash cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative" git add acf/group_services_list.json git commit -m "✨ feat: Add ACF field group for services-list block" ``` --- ## Task 2: Decorative SVG Asset **Files:** - Create: `static/img/services-list-line.svg` **Interfaces:** - Produces: A 1600×8 viewBox SVG with a 2px-tall orange `#F26B53` rectangle, `preserveAspectRatio="none"`. Reference URL: `/wp-content/themes/community-works-collaborative/static/img/services-list-line.svg`. - [ ] **Step 1: Create the SVG asset** Create the file `static/img/services-list-line.svg` with the following exact content: ```svg ``` - [ ] **Step 2: Verify the SVG is well-formed** Run: `node -e "const fs=require('fs'); const s=fs.readFileSync('static/img/services-list-line.svg','utf8'); if(!s.includes('viewBox=\"0 0 1600 8\"')||!s.includes('fill=\"#F26B53\"')) { process.exit(1); } console.log('OK')"` Expected: prints `OK` and exits 0. - [ ] **Step 3: Commit** ```bash cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative" git add static/img/services-list-line.svg git commit -m "✨ feat: Add services-list decorative line SVG" ``` --- ## Task 3: Block Registration JSON **Files:** - Create: `views/blocks/services-list/block.json` **Interfaces:** - Produces: `block.json` declaring `acf/services-list` as the block name, `services-list.php` as the render template, `grid-view` icon, `common` category. `regACFBlocks()` in `functions.php` will pick this up automatically — no PHP registration code change required. - [ ] **Step 1: Create the block.json file** Create the file `views/blocks/services-list/block.json` with the following exact content: ```json { "name": "acf/services-list", "title": "Services List", "description": "A row of service cards. Card count (2, 3, or 4) determines the layout.", "category": "common", "icon": "grid-view", "keywords": ["services", "cards", "mapping"], "acf": { "mode": "preview", "renderTemplate": "services-list.php" }, "supports": { "anchor": true, "align": false } } ``` - [ ] **Step 2: Verify the JSON parses** Run: `node -e "JSON.parse(require('fs').readFileSync('views/blocks/services-list/block.json','utf8')); console.log('OK')"` Expected: prints `OK` and exits 0. - [ ] **Step 3: Commit** ```bash cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative" git add views/blocks/services-list/block.json git commit -m "✨ feat: Add services-list block.json registration" ``` --- ## Task 4: Block CSS **Files:** - Create: `views/blocks/services-list/services-list.css` **Interfaces:** - Produces: BEM-styled CSS for `.services-list`, `__line`, `__grid`, `__card`, `__media`, `__heading`, `__desc`. Reuses `text-cwc-blue-01` and `var(--color-secondary)` from existing tokens. Renders the bisecting line via `background-image: url('/wp-content/themes/community-works-collaborative/static/img/services-list-line.svg')`. - [ ] **Step 1: Create the block CSS file** Create the file `views/blocks/services-list/services-list.css` with the following exact content: ```css /* Services List block */ .services-list { --services-line-top: 25%; &__line { position: absolute; inset-inline: 0; top: var(--services-line-top); height: 8px; pointer-events: none; z-index: 1; background-image: url('/wp-content/themes/community-works-collaborative/static/img/services-list-line.svg'); background-repeat: no-repeat; background-position: center center; background-size: 100% 100%; } &__grid { position: relative; z-index: 2; } &__card { @apply flex flex-col; } &__media { @apply overflow-hidden rounded-md bg-gray-100; } &__img { @apply w-full h-full object-cover; } &__heading { @apply text-cwc-blue-01 text-25px font-bold leading-none mt-6 mb-4; &::after { background: var(--color-secondary); content: ""; display: block; height: 4px; margin-top: 0.45rem; width: 3rem; } } &__desc { @apply text-gray-600 text-16px font-light leading-snug; } } @media (max-width: 1023px) { .services-list__line { display: none; } } ``` - [ ] **Step 2: Verify the CSS contains the expected classes** The brief uses CSS-nesting shorthand (`&__line`, `&__grid`, etc.) for 6 of the 7 BEM children, and a literal selector (`.services-list__line`) for the one inside the `@media` block. A naive grep for the literal class names will undercount. Use: ```bash grep -cE "&__(line|grid|card|media|img|heading|desc)|services-list__(line|grid|card|media|img|heading|desc)" views/blocks/services-list/services-list.css ``` Expected: `8` (7 nested `&__` selectors + 1 literal `.services-list__line` inside the `@media` block). - [ ] **Step 3: Commit** ```bash cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative" git add views/blocks/services-list/services-list.css git commit -m "✨ feat: Add services-list block CSS" ``` --- ## Task 5: Block Template (PHP) **Files:** - Create: `views/blocks/services-list/services-list.php` **Interfaces:** - Consumes: `get_field( 'items' )` → array of items each with `image` (array: `url`, `alt`), `heading` (string), `description` (string). - Produces: A `
` element containing the bisecting `__line` div, a `container` with a grid div, and a `
` per item. Computes the dynamic `lg:grid-cols-N` class from item count. - [ ] **Step 1: Create the block PHP template** Create the file `views/blocks/services-list/services-list.php` with the following exact content (tabs for indentation): ```php
>
<?php echo esc_attr( $item['image']['alt'] ?? '' ); ?>

``` - [ ] **Step 2: Lint the PHP file with PHPCS** Run: `composer exec phpcs -- views/blocks/services-list/services-list.php` Expected: exits 0 with no errors. If there are pre-existing project-wide issues unrelated to this file, fix only the ones in this file. - [ ] **Step 3: Verify the dynamic grid class is in the source** Run: `grep -E "lg:grid-cols-\$grid_cols|lg:grid-cols-' \. \$grid_cols" views/blocks/services-list/services-list.php` Expected: at least one match (Tailwind must see the literal class name in the source to emit the rule). - [ ] **Step 4: Commit** ```bash cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative" git add views/blocks/services-list/services-list.php git commit -m "✨ feat: Add services-list block PHP template" ``` --- ## Task 6: CSS Import Wiring **Files:** - Modify: `styles/blocks/index.css` **Interfaces:** - Consumes: `views/blocks/services-list/services-list.css` (from Task 4). - Produces: A new `@import` line at the end of `styles/blocks/index.css` so Tailwind's build picks up the block's CSS. - [ ] **Step 1: Add the import to the block index CSS** Append the following line at the end of `styles/blocks/index.css`: ```css @import '../../views/blocks/services-list/services-list.css'; ``` After the edit, the file should end with: ``` @import '../../views/blocks/contact-info/contact-info.css'; @import '../../views/blocks/services-list/services-list.css'; ``` - [ ] **Step 2: Verify the import is present** Run: `grep -c "services-list/services-list.css" styles/blocks/index.css` Expected: `1`. - [ ] **Step 3: Commit** ```bash cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative" git add styles/blocks/index.css git commit -m "🔧 chore: Import services-list block CSS into theme" ``` --- ## Task 7: Build the CSS **Files:** - Modify: `static/dist/theme.css` **Interfaces:** - Consumes: The new import in `styles/blocks/index.css` (from Task 6) and the block CSS (from Task 4). - Produces: A rebuilt `static/dist/theme.css` that includes the `.services-list*` rules and the dynamic `lg:grid-cols-2`, `lg:grid-cols-3`, `lg:grid-cols-4` utilities. - [ ] **Step 1: Run the production Tailwind build** Run: `npm run build` Expected: completes with no errors. The output file `static/dist/theme.css` is updated in place. - [ ] **Step 2: Verify the services-list CSS classes are in the built output** Run: `grep -c "services-list__heading" static/dist/theme.css` Expected: `1` or more (the class should appear in the built CSS). - [ ] **Step 3: Verify the dynamic grid classes are in the built output** Run: `grep -E "lg\\\\:grid-cols-(2|3|4) " static/dist/theme.css | wc -l` Expected: `3` (all three column counts should be present). - [ ] **Step 4: Commit the rebuilt dist** ```bash cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative" git add -f static/dist/theme.css git commit -m "🎨 build: Rebuild dist with services-list block styles" ``` --- ## Task 8: Playwright Tests **Files:** - Create: `tests/services-page.spec.js` **Interfaces:** - Consumes: The rendered `views/blocks/services-list/services-list.php` template (Tasks 4 + 5) on the WordPress page `/services/mapping/`. - Produces: Three tests: 1. Card count, image, and heading visibility per block (expects 2 + 4 cards across two block instances). 2. Bisecting `__line` is present and `aria-hidden="true"`. 3. No axe-core a11y violations scoped to `.services-list`. > **Prerequisite:** the test depends on the mapping page being composed in Gutenberg with two `Services List` blocks (2 items + 4 items) and a `Pull Quote` block. That composition is the editor's responsibility and is not part of this plan. The tests should be authored against the *intended* state, not a placeholder state. - [ ] **Step 1: Create the test spec file** Create the file `tests/services-page.spec.js` with the following exact content: ```js import { test, expect } from "@playwright/test"; import AxeBuilder from "@axe-core/playwright"; test.describe("Services List block on /services/mapping/", () => { test("renders the heading and image for each card", async ({ page }) => { await page.goto("/services/mapping/"); const blocks = page.locator(".services-list"); await expect(blocks).toHaveCount(2); await expect(blocks.nth(0).locator(".services-list__card")).toHaveCount(2); await expect(blocks.nth(1).locator(".services-list__card")).toHaveCount(4); const firstCard = blocks.nth(0).locator(".services-list__card").first(); await expect(firstCard.locator(".services-list__media img")).toBeVisible(); await expect(firstCard.locator(".services-list__heading")).toBeVisible(); }); test("bisecting line is present and aria-hidden", async ({ page }) => { await page.goto("/services/mapping/"); const line = page.locator(".services-list__line").first(); await expect(line).toBeAttached(); await expect(line).toHaveAttribute("aria-hidden", "true"); }); test("no axe violations on the services-list block", async ({ page }) => { await page.goto("/services/mapping/"); const results = await new AxeBuilder({ page }) .include(".services-list") .analyze(); expect(results.violations).toEqual([]); }); }); ``` - [ ] **Step 2: Run the tests** Run: `npx playwright test tests/services-page.spec.js` Expected: all 3 tests pass. If tests fail because the mapping page has not yet been composed in the editor, that's an expected prerequisite — note the prerequisite in the final report and skip the test run. - [ ] **Step 3: Commit** ```bash cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative" git add tests/services-page.spec.js git commit -m "✅ test: Add Playwright tests for services-list block" ``` --- ## Self-Review Notes - **Spec coverage:** - One new ACF block (`services-list`) — covered by Tasks 1, 3, 4, 5. - Repeater of `items` with `image` / `heading` / `description` — covered by Task 1. - Card count drives grid columns (2/3/4) — covered by Task 5 (PHP computes the class) and Task 7 (Tailwind emits the rule). - Orange bisecting line, full-bleed, behind the cards, hidden on mobile — covered by Tasks 2 (SVG), 4 (CSS with `inset-inline: 0`, `z-index: 1` on line, `z-index: 2` on grid, media query to hide on `