15 KiB
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.jsonand 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 withgit add -fper project convention. - PHPCS uses the WordPress coding standard;
composer lintmust 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.jsmust pass except the 4 pre-existingmobile-homepage.spec.jsfailures (out of scope) and the still-broken services-hero-scope ACF data state (separate user follow-up). - The new
index.phpchange 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-8padding is tightened topx-4 py-6. - The page title is hard-coded "Blog" (project's standard H1 styling: Quincy serif, CWC Blue 01, fluid
--h1size, 700 weight). The title is the same on every view that usesindex.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.phpto 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 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() ) ); ?> —</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' => '« Previous',
'next_text' => 'Next »',
)
);
?>
</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…</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-8topost-list__details px-4 py-6. - Do NOT touch the
<div class="post-list__byline mt-auto">block. - Do NOT touch the empty state (
elsebranch). - 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
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
--h1size, 700 weight, default margin). -
Step 1: Verify the import in
styles/components/index.cssis already present
Run: grep -n "post-list" styles/components/index.css
Expected: a matching line. If not, add:
@import './post-list.css';
- Step 2: Replace
styles/components/post-list.csswith the new content
/* 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
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 withgit 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
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
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
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.jsfailures (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=trueinwp-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)
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.photosfallback 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).