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

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.php only — the blog index, category archives, tag archives, author archives, and search results all fall through this template in the current theme (no archive.php, category.php, tag.php, author.php, or home.php exist).
  • Does not change single.php (single-post view) or page.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:

  1. 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.
  2. styles/components/post-list.css — unfreeze the file (currently empty boilerplate) and add styles for the new .post-list__h1 heading. Use the project's existing H1 typography (Quincy serif, fluid --h1 size, default margin).
  3. 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 of tests/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() ) ); ?> &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( ... ); ?>
            </div>
        </div>
    </div>
<?php else : ?>
    ...
<?php endif; ?>

Changes:

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

  2. Remove the categories row (<div class="post-list__cats">...).

  3. Remove the 15-word excerpt (<div class="post-list__excerpt mb-6">...).

  4. Keep the byline (<div class="post-list__byline mt-auto">...).

  5. Tighten the card's details padding — the current px-4 py-8 leaves a lot of empty space when there's no excerpt. Change to px-4 py-6 (or whatever the design lands on after visual review; spec this as px-4 py-6).

  6. Empty state — when no posts are found, the "Nothing here yet..." heading still renders. This is the existing behavior; no change. (The else branch is unchanged.)

  7. customExcerpt() helper is now unused in index.php. The helper itself (in lib/extras.php or lib/helpers.php) is left as-is — other templates may use it. Just don't call it from index.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) to index.php. The template loops over the matching posts, renders each as a card, and renders pagination at the bottom. The sidebar is rendered when hasSidebar() 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.photos fallback is preserved. (The current code has get_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.php change 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 touch header.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.photos URL 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.php works 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.js covering:
    • 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.
  • Run npm run build to regenerate static/dist/.
  • Run the full Playwright suite (npx playwright test) including tests/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 new tests/blog-page.spec.js.
  • Run composer lint to catch PHPCS issues.
  • Capture a 1280px full-page screenshot of /blog/ for visual comparison with blog-mockup.png.

Acceptance Criteria

  1. /blog/ renders a "Blog" page title (Quincy serif, CWC Blue 01, the project's standard H1 style).
  2. Each post card on /blog/ shows: featured image, title, byline (author + date). No categories row. No excerpt.
  3. 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.
  4. Category, tag, author, and search views (which all use index.php) render with the same design — page title + post grid + sidebar.
  5. No inner-page hero is rendered on the blog (this was the previous turn's change; preserved here).
  6. No console errors, axe violations, or PHPCS errors.
  7. The new blog test passes; existing tests continue to pass with no regressions.