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

179 lines
10 KiB
Markdown

# 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
<?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:
```php
<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:
```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;
}
```
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.