Files
CWC/docs/superpowers/plans/2026-07-04-blog-layout.md

371 lines
15 KiB
Markdown

# 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).