# Blog Index Layout 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 blog index layout shown in `blog-mockup.png` so `/blog/` and any other view using `index.php` render a "Blog" page title, a grid of post cards (image + title + byline), and the existing sidebar.

**Architecture:** A small refactor of `index.php` (add a page title, trim the post card) and an unfreeze of `styles/components/post-list.css` (add `.post-list__h1` styles). A new Playwright test verifies the result. The existing sidebar logic, pagination, and post grid layout are preserved.

**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, see `composer.json` and existing files). The brief's canonical PHP for the index.php changes should use tabs.
- The theme's Tailwind build outputs `static/dist/theme.css`; that file is gitignored but committed with `git add -f` per project convention.
- PHPCS uses the WordPress coding standard; `composer lint` must pass. Note: the previous contact-page work surfaced 5 pre-existing PHPCS CRLF errors on untouched files — they are out of scope and not introduced by this plan.
- All Playwright tests in `tests/*.spec.js` must pass except the 4 pre-existing `mobile-homepage.spec.js` failures (out of scope) and the still-broken services-hero-scope ACF data state (separate user follow-up).
- The new `index.php` change applies to all views that route through it: `/blog/`, category archives, tag archives, author archives, and search results. No new template files are created.
- The post card content is simplified to image + title + byline (no categories, no excerpt). The card's `px-4 py-8` padding is tightened to `px-4 py-6`.
- The page title is hard-coded "Blog" (project's standard H1 styling: Quincy serif, CWC Blue 01, fluid `--h1` size, 700 weight). The title is the same on every view that uses `index.php`.
- The hero gate (set in the previous turn: `$showHero = isServicesDescendant() && get_field( 'hero_style' ) === 'default'`) is unaffected.

---

## File Structure

| File | Responsibility | Created/Modified |
| --- | --- | --- |
| `index.php` | Renders the blog index, category/tag/author archives, and search results. Add a "Blog" H1 and trim the post card. | Modify |
| `styles/components/post-list.css` | Unfreeze the file and add `.post-list__h1` styles. | Modify |
| `tests/blog-page.spec.js` | New Playwright spec for the blog layout. | Create |
| `static/dist/theme.css` | Rebuilt via `npm run build`. | Modified (committed with `git add -f`). |

---

### Task 1: Update `index.php` — add page title, trim the post card

**Files:**
- Modify: `index.php`

**Interfaces:**
- Consumes: the standard WordPress loop (`have_posts()`, `the_post()`).
- Produces: a `<section>` with the existing sidebar-aware grid; a `<h1 class="post-list__h1">Blog</h1>` at the top of the post list; a card per post with image, title link, and byline (no categories, no excerpt).

- [ ] **Step 1: Read `index.php` to confirm the current structure**

The file has been read in this session (it currently renders `.post-list__posts` with cards containing categories, title, excerpt, and byline). Confirm the line numbers and class names match the spec.

- [ ] **Step 2: Add the H1 and trim the card**

Replace the post-list opening + card inner content with the new structure:

```php
<?php if ( have_posts() ) : ?>
    <div class="blog-posts <?php echo esc_attr( $clsEntry ); ?>">
        <div class="post-list">
            <h1 class="post-list__h1">Blog</h1>

            <div class="post-list__posts grid grid-cols-[repeat(auto-fit,minmax(20rem,1fr))] gap-6">
                <?php
                while ( have_posts() ) :
                    the_post();
                    ?>
                    <div class="post-list__post flex flex-col border border-secondary rounded-md shadow-lg hover:prose-img:scale-110 hover:prose-img:origin-center hover:prose-img:duration-500">
                        <figure class="post-list__img aspect-video border-b border-secondary rounded-t-md block h-auto w-full overflow-hidden m-0 p-0">
                            <?php if ( has_post_thumbnail() ) : ?>
                                <?php
                                    $featImg = get_the_post_thumbnail_url();
                                    $postImg = $featImg ? $featImg : 'https://picsum.photos/600/400?random=' . get_the_ID();
                                    $imgAlt  = get_post_meta( get_post_thumbnail_id(), '_wp_attachment_image_alt', true ) ?? get_the_title();
                                ?>

                                <img class="block h-full object-cover transition-transform duration-300 ease-linear w-full will-change-transform" src="<?php echo esc_url( $postImg ); ?>" alt="<?php echo esc_attr( $imgAlt ); ?>">
                            <?php endif; ?>
                        </figure>

                        <div class="post-list__details px-4 py-6 flex flex-col grow">
                            <a href="<?php the_permalink(); ?>" class="">
                                <h2 class="post-list__title font-normal text-25px text-balance line-clamp-4 truncate mt-0 mb-5 mx-0"><?php the_title(); ?></h2>
                            </a>

                            <div class="post-list__byline mt-auto">
                                <span class="post-list__author"><?php echo esc_html( ucfirst( get_the_author() ) ); ?> &mdash;</span>
                                <span class="post-list__date"><?php echo get_the_date(); ?></span>
                            </div>
                        </div>
                    </div>
                <?php endwhile; ?>
            </div>

            <div class="post-list__pagination">
                <?php
                the_posts_pagination(
                    array(
                        'mid_size'  => 2,
                        'prev_text' => '&laquo; Previous',
                        'next_text' => 'Next &raquo;',
                    )
                );
                ?>
            </div>
        </div>
    </div>
<?php else : ?>
    <div class="blog-posts <?php echo esc_attr( $clsEntry ); ?>">
        <div class="no-posts">
            <h2 class="no-posts__title">Nothing here yet&hellip;</h2>
            <p class="no-posts__desc">No published posts found.</p>
        </div>
    </div>
<?php endif; ?>
```

Specifically:
- Add `<h1 class="post-list__h1">Blog</h1>` after the `<div class="post-list">` opening tag.
- Remove the `<div class="post-list__cats">` block.
- Remove the `<div class="post-list__excerpt mb-6">` block.
- Change `post-list__details px-4 py-8` to `post-list__details px-4 py-6`.
- Do NOT touch the `<div class="post-list__byline mt-auto">` block.
- Do NOT touch the empty state (`else` branch).
- Do NOT touch the pagination block.

**Indentation:** tabs. The current `index.php` uses tabs.

- [ ] **Step 3: Verify the file parses**

Run: `php -l index.php`
Expected: `No syntax errors detected in index.php`.

- [ ] **Step 4: Commit**

```bash
git add index.php
git commit -m "feat(blog): add page title and trim post card to image + title + byline"
```

---

### Task 2: Unfreeze `styles/components/post-list.css` and add `.post-list__h1`

**Files:**
- Modify: `styles/components/post-list.css`

**Interfaces:**
- Consumes: the new `<h1 class="post-list__h1">` from Task 1.
- Produces: a styled H1 that matches the project's H1 typography (Quincy serif, CWC Blue 01, fluid `--h1` size, 700 weight, default margin).

- [ ] **Step 1: Verify the import in `styles/components/index.css` is already present**

Run: `grep -n "post-list" styles/components/index.css`
Expected: a matching line. If not, add:
```css
@import './post-list.css';
```

- [ ] **Step 2: Replace `styles/components/post-list.css` with the new content**

```css
/* Blog/post index listing styles */

.post-list__h1 {
    color: var(--color-cwc-blue-01);
    font-family: var(--font-quincy, 'Quincy', serif);
    font-size: var(--h1);
    font-weight: 700;
    line-height: 1.2;
    margin: 0 0 2rem;
    text-align: left;
}
```

- [ ] **Step 3: Commit**

```bash
git add styles/components/post-list.css styles/components/index.css
git commit -m "feat(blog): style the post-list page title"
```

---

### Task 3: Build the dist CSS

**Files:**
- Modify: `static/dist/theme.css` (committed with `git add -f`).

- [ ] **Step 1: Run the build**

Run: `npm run build`
Expected: build completes without errors.

- [ ] **Step 2: Verify the new class is in the dist**

Run: `grep -c "post-list__h1" static/dist/theme.css`
Expected: at least 1 match.

- [ ] **Step 3: Commit the rebuilt dist**

```bash
git add -f static/dist/theme.css
git commit -m "build: regenerate dist with blog post-list styles"
```

---

### Task 4: Add `tests/blog-page.spec.js`

**Files:**
- Create: `tests/blog-page.spec.js`

**Interfaces:**
- Consumes: the rendered `/blog/` page.
- Produces: a Playwright spec that verifies the page title, the post grid, the absence of categories/excerpt, the sidebar, the absence of an inner-page hero, and no console errors or axe violations.

- [ ] **Step 1: Create `tests/blog-page.spec.js`**

```js
import { expect, test } from "@playwright/test";
const AxeBuilder = require("@axe-core/playwright").default;

const blogUrl =
    process.env.TEST_BLOG_URL ||
    "http://community-works-collaborative.test/blog/";

test.describe("blog page", () => {
    test("desktop: Blog title, post grid, no inner-page hero, sidebar", async ({
        page,
    }) => {
        const pageErrors = [];
        page.on("pageerror", (err) => pageErrors.push(err.message));

        await page.setViewportSize({ width: 1280, height: 900 });
        await page.goto(blogUrl);

        // No inner-page hero.
        expect(await page.locator(".page-hero").count()).toBe(0);

        // Page title is "Blog".
        const heading = page.locator(".post-list__h1").first();
        await expect(heading).toBeVisible();
        await expect(heading).toHaveText("Blog");

        // Post grid renders at least one card.
        const posts = page.locator(".post-list__post");
        const postCount = await posts.count();
        expect(postCount).toBeGreaterThanOrEqual(1);

        // First card: image, title link, byline.
        const firstCard = posts.first();
        await expect(firstCard.locator(".post-list__img img")).toBeVisible();
        await expect(firstCard.locator(".post-list__title")).toBeVisible();
        await expect(firstCard.locator(".post-list__byline")).toBeVisible();

        // First card does NOT have a categories row or an excerpt.
        expect(await firstCard.locator(".post-list__cats").count()).toBe(0);
        expect(await firstCard.locator(".post-list__excerpt").count()).toBe(0);

        // Sidebar renders.
        await expect(page.locator("#secondary, .sidebar, aside").first()).toBeAttached();

        // No JS errors.
        expect(pageErrors).toEqual([]);
    });

    test("mobile 402: single-column grid, no horizontal overflow", async ({
        page,
    }) => {
        await page.setViewportSize({ width: 402, height: 874 });
        await page.goto(blogUrl);

        const heading = page.locator(".post-list__h1").first();
        await expect(heading).toBeVisible();

        const headingBox = await heading.boundingBox();
        const block = page.locator(".post-list").first();
        const blockBox = await block.boundingBox();

        expect(headingBox).not.toBeNull();
        expect(blockBox).not.toBeNull();
        expect(headingBox.x + headingBox.width).toBeLessThanOrEqual(402 + 3);
        expect(blockBox.x + blockBox.width).toBeLessThanOrEqual(402 + 3);
    });

    test("mobile 320: nothing clips horizontally", async ({ page }) => {
        await page.setViewportSize({ width: 320, height: 800 });
        await page.goto(blogUrl);

        const block = page.locator(".post-list").first();
        await expect(block).toBeVisible();

        const blockBox = await block.boundingBox();
        expect(blockBox.x + blockBox.width).toBeLessThanOrEqual(320 + 3);
    });

    test("axe: no violations", async ({ page }, testInfo) => {
        await page.setViewportSize({ width: 1280, height: 900 });
        await page.goto(blogUrl);

        const results = await new AxeBuilder({ page })
            .withTags([
                "wcag2a",
                "wcag2aa",
                "wcag21a",
                "wcag21aa",
                "wcag22a",
                "wcag22aa",
            ])
            .analyze();

        await testInfo.attach("accessibility-scan-results", {
            body: JSON.stringify(results, null, 2),
            contentType: "application/json",
        });

        expect(results.violations).toEqual([]);
    });
});
```

- [ ] **Step 2: Run the new test**

Run: `npx playwright test tests/blog-page.spec.js`
Expected: all 4 tests pass. The blog URL is `http://community-works-collaborative.test/blog/`; if the live site has no published posts, the test may fail with `postCount === 0`. In that case, the user needs to seed a published post (or set `TEST_BLOG_URL` to a URL with posts).

- [ ] **Step 3: Commit**

```bash
git add tests/blog-page.spec.js
git commit -m "test(blog): add blog-page Playwright spec"
```

---

### Task 5: Run the full Playwright suite + PHPCS

- [ ] **Step 1: Run the full Playwright suite**

Run: `npx playwright test`
Expected: all tests pass except:
- 4 pre-existing `mobile-homepage.spec.js` failures (out of scope).
- The 2 services-hero-scope ACF-data failures (separate user follow-up; this plan does not introduce them).
- The 1 services-hero-scope `test.fail()` grandchild (already handled).
- The 6 inner-page regressions (user follow-up: set `CWC_TEST_BYPASS_HERO_GATE=true` in `wp-config.php`).

The new `tests/blog-page.spec.js` should pass.

- [ ] **Step 2: Run PHPCS**

Run: `composer lint`
Expected: no NEW errors. The 5 pre-existing CRLF errors on untouched files remain (out of scope).

- [ ] **Step 3: Verify the dist is up to date**

Run: `git status static/dist/theme.css`
Expected: `nothing to commit, working tree clean`. If uncommitted, run `git add -f static/dist/theme.css` and commit with `chore: sync dist after lint/fix`.

- [ ] **Step 4: Final commit (if anything changed in step 1-3)**

```bash
git add -A
git commit -m "chore: final pass after Playwright and PHPCS"
```

---

## Self-Review Notes

- **Spec coverage:** Task 1 (index.php H1 + card trim), Task 2 (post-list.css), Task 3 (build), Task 4 (test), Task 5 (final suite + PHPCS). Every spec section is covered.
- **Placeholder scan:** No "TBD", "TODO", or "implement later" in any step. The `picsum.photos` fallback is a known wart but it's in the existing code (out of scope).
- **Type consistency:** Class names match the BEM convention in the spec.
- **Spec section "Out of Scope" (per-view titles, new templates, single-post view, sidebar widgets, fallback image, pagination restyling, front-page.php, archive/category/tag/author templates):** all left as-is.
- **Indentation:** tabs for PHP (per the previous contact-page work, the project standard).
