10 KiB
Blog Index Layout
Goal
Build the blog index layout shown in blog-mockup.png (theme root) so /blog/ and any other view that uses the WordPress index.php template (category, tag, author, search) render with a clean design: a "Blog" page title at the top, a grid of post cards (each card shows image, title, and byline only), and the existing sidebar logic preserved on the right side.
Scope
- Applies to
index.phponly — the blog index, category archives, tag archives, author archives, and search results all fall through this template in the current theme (noarchive.php,category.php,tag.php,author.php, orhome.phpexist). - Does not change
single.php(single-post view) orpage.php(static pages). The Services hero gate is unaffected. - Does not change
front-page.php(the home page). - The existing post list component (
.post-list__posts,.post-list__post,.post-list__title,.post-list__byline) is preserved and used. - The existing sidebar logic (
hasSidebar()→ 4-col grid with sidebar) is preserved. - The existing pagination (
the_posts_pagination) is preserved.
Architecture
The change is a small refactor of index.php and an unfreeze of styles/components/post-list.css:
index.php— add a "Blog" page title at the top of the post list section, and trim the post card to image + title + byline (remove the categories row and the 15-word excerpt). The grid, sidebar, and pagination remain unchanged.styles/components/post-list.css— unfreeze the file (currently empty boilerplate) and add styles for the new.post-list__h1heading. Use the project's existing H1 typography (Quincy serif, fluid--h1size, default margin).- A new test in
tests/blog-page.spec.js— verifies the blog renders the page title, the post grid, and no inner-page hero. Mirrors the structure oftests/contact-page.spec.js.
The post list grid is already implemented with grid-cols-[repeat(auto-fit,minmax(20rem,1fr))] (a responsive auto-fit grid that gives 3-4 columns on desktop, fewer on smaller viewports). The mockup shows 3 columns at desktop, which is what the auto-fit grid gives at ~1280px. No change to the grid layout.
ACF Field Group Changes
None. The blog index uses the standard WordPress post data (the_title, the_permalink, the_author, the_date, get_the_post_thumbnail_url, get_the_category). No custom fields.
index.php Wiring
Find the post list section:
<?php if ( have_posts() ) : ?>
<div class="blog-posts <?php echo esc_attr( $clsEntry ); ?>">
<div class="post-list">
<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 ...">
<figure class="post-list__img ...">
... (featured image)
</figure>
<div class="post-list__details px-4 py-8 flex flex-col grow">
<div class="post-list__cats">
Posted in: ... (categories)
</div>
<a href="<?php the_permalink(); ?>" class="">
<h2 class="post-list__title ..."><?php the_title(); ?></h2>
</a>
<div class="post-list__excerpt mb-6">
<?php customExcerpt( get_the_content(), 15 ); ?>
</div>
<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( ... ); ?>
</div>
</div>
</div>
<?php else : ?>
...
<?php endif; ?>
Changes:
-
Add a page title at the top of the post list, after the
<div class="post-list">opening tag:<div class="post-list"> <h1 class="post-list__h1">Blog</h1> <div class="post-list__posts grid ..."> ... </div> ... </div>The title text is hard-coded "Blog" — this matches the spec's "Standard H1 (Quincy, project default)" answer. The title is the same on category, tag, author, and search views (the mockup only shows the blog index, but consistency is the simplest choice for now; a future task can add per-view titles if needed).
-
Remove the categories row (
<div class="post-list__cats">...). -
Remove the 15-word excerpt (
<div class="post-list__excerpt mb-6">...). -
Keep the byline (
<div class="post-list__byline mt-auto">...). -
Tighten the card's details padding — the current
px-4 py-8leaves a lot of empty space when there's no excerpt. Change topx-4 py-6(or whatever the design lands on after visual review; spec this aspx-4 py-6). -
Empty state — when no posts are found, the "Nothing here yet..." heading still renders. This is the existing behavior; no change. (The
elsebranch is unchanged.) -
customExcerpt()helper is now unused inindex.php. The helper itself (inlib/extras.phporlib/helpers.php) is left as-is — other templates may use it. Just don't call it fromindex.php.
post-list.css Wiring
The file is currently empty boilerplate. Add:
/* 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;
}
The color: var(--color-cwc-blue-01) matches the contact-info heading and the inner-page hero heading for visual consistency. The other values are the project's existing H1 typography tokens (Quincy serif, fluid --h1 size, default 700 weight).
No other changes to post-list.css. The card styling (border, shadow, padding) is currently inline via Tailwind utility classes in index.php; the spec leaves that as-is.
Data Flow
- Editor flow: the blog index uses standard WordPress post data. Editors write posts in the WP admin, set a featured image, assign categories, and publish. The blog renders whatever is published.
- Render flow: WordPress routes
/blog/,/category/...,/tag/...,/author/..., and/?s=...(search) toindex.php. The template loops over the matching posts, renders each as a card, and renders pagination at the bottom. The sidebar is rendered whenhasSidebar()returns true. - Per-page variation: none — the design is fixed.
Error Handling and Edge Cases
- No posts found → the "Nothing here yet..." empty state renders.
- Pagination → preserved; renders at the bottom of the post list.
- Featured image missing → the existing
picsum.photosfallback is preserved. (The current code hasget_the_post_thumbnail_url() ? $featImg : 'https://picsum.photos/600/400?random=' . get_the_ID();. This is a hack — a real fallback would be a project default image. Out of scope for this task.) - Page title when there are no posts → still renders "Blog" (the title is unconditional). This is fine — a user navigating to an empty category should still know what page they're on.
- Search results → the "Blog" title is misleading for search. A future task can add a per-view title (
is_search() ? 'Search Results' : 'Blog'). Out of scope here. - Hero → the
header.phpchange from the previous turn ($showHero = isServicesDescendant() && get_field( 'hero_style' ) === 'default') already ensures the blog renders without the inner-page hero. This task doesn't touchheader.php.
Out of Scope
- New ACF fields for posts.
- New archive templates (
archive.php,category.php,tag.php,author.php). - Single-post view (
single.php) changes. - A proper fallback image (the
picsum.photosURL is a known wart but not a blocker). - Pagination restyling (the existing
the_posts_pagination()is fine). - Changing
front-page.php(the home page). - Sidebar widget changes (the existing
sidebar.phpworks as-is). - A per-view page title (category-specific, tag-specific, etc.). The current spec hard-codes "Blog" everywhere.
Verification
- Add
tests/blog-page.spec.jscovering:- Desktop (1280px): the blog page renders with the "Blog" H1, the post grid, no inner-page hero, the sidebar (when
hasSidebar()is true). - Mobile (402px): the post grid stacks to a single column, no horizontal overflow, the page title is visible.
- 320px: nothing clips.
- Axe scan: no violations.
- No console errors.
- Each post card has: image, title link, byline (author + date). Does NOT have a categories row or an excerpt.
- Desktop (1280px): the blog page renders with the "Blog" H1, the post grid, no inner-page hero, the sidebar (when
- Run
npm run buildto regeneratestatic/dist/. - Run the full Playwright suite (
npx playwright test) includingtests/mobile-homepage.spec.js,tests/site-a11y.spec.js,tests/inner-page.spec.js,tests/contact-page.spec.js,tests/services-hero-scope.spec.js, and the newtests/blog-page.spec.js. - Run
composer lintto catch PHPCS issues. - Capture a 1280px full-page screenshot of
/blog/for visual comparison withblog-mockup.png.
Acceptance Criteria
/blog/renders a "Blog" page title (Quincy serif, CWC Blue 01, the project's standard H1 style).- Each post card on
/blog/shows: featured image, title, byline (author + date). No categories row. No excerpt. - The existing sidebar logic is preserved: when
hasSidebar()is true, the post grid is in the main column and the sidebar is in the 4th column of a 4-col grid. - Category, tag, author, and search views (which all use
index.php) render with the same design — page title + post grid + sidebar. - No inner-page hero is rendered on the blog (this was the previous turn's change; preserved here).
- No console errors, axe violations, or PHPCS errors.
- The new blog test passes; existing tests continue to pass with no regressions.