Files
CWC/docs/superpowers/specs/2026-07-01-inner-page-layout-design.md
T
Keith SolomonandClaude 455a005e58 Add inner page layout design spec
Defines the inner page layout: dark hero with background image and decorative
vector (driven by two new fields on the existing Page Heading ACF group), a
centered narrow intro section underneath (driven by the existing Intro field),
and the standard the_content body.

The mockup's bottom dark block is the existing site footer, not a new CTA. No
new ACF blocks are introduced; the layout reuses the existing Page Heading meta
box and the page-hero partial that header.php already loads.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-01 08:36:37 -05:00

9.2 KiB

Inner Page Layout

Goal

Build the inner page layout shown in notes/inner-page-mockup.png so that every WordPress page renders the same structure: a dark hero with a background image and a decorative vector on the right, a centered narrow intro section underneath, and the standard editor body below it. Different pages vary only by hero image, vector, intro text, and body content.

Scope

  • Applies to all page post-type views rendered by page.php (and through it, the page-hero partial that header.php already loads).
  • Does not change the front page, single posts, archives, search, or 404 templates.
  • Does not change the homepage-hero block.
  • Does not add a new contact block to the page body — the dark navy block with the orange rule in the mockup is the existing site footer, not a new CTA.

Architecture

The layout is a fixed three-part composition:

  1. Heroviews/partials/page-hero.php (existing), extended with two new ACF fields and two new visual slots.
  2. Intro — new views/partials/page-intro.php, rendered by page.php between the hero and the_content when the intro ACF field is non-empty.
  3. Bodythe_content, unchanged.

page.php becomes the orchestrator: it decides whether to render the intro partial based on the intro ACF value, then renders the body. Sidebar behavior (already gated by hasSidebar() and the Page Sidebar field group) is preserved.

hasPageHeader() in lib/extras.php continues to gate the hero. The function's current behavior (hero shows when hero_style !== 'none') is kept; no change to that filter is required.

ACF Field Group Changes

Add two fields to the existing Page Heading group (acf/group_60bfb84ae973c.json):

  • hero_image — type: image, return_format: array, library: all, preview_size: medium. Conditional: shown when hero_style !== 'none'. Optional.
  • hero_vector — type: image, return_format: array, library: all, preview_size: medium. Conditional: shown when hero_style !== 'none'. Optional.

No other ACF group changes. The existing heading, intro, and call_to_actions fields are reused as-is.

Hero Partial — views/partials/page-hero.php

Changes:

  • Read the two new fields (hero_image, hero_vector).
  • Render hero_image as an absolutely-positioned <img> inside a new .page-hero__media container, masked with a left-to-right fade so the text on the left remains readable.
  • Render hero_vector as an absolutely-positioned, right-anchored, bottom-anchored <img> inside a new .page-hero__vector container, with aria-hidden="true" and role="presentation".
  • Stop rendering the intro paragraph inside the hero. The intro field is now rendered in the new page-intro partial. (The heading still renders inside the hero, so the page title and the optional heading override work as before.)
  • Keep the heading, breadcrumbs, and call_to_actions rendering unchanged.
  • When no hero_image is set, the existing background_color (or the default dark gradient) is the only background.

Both new images are loading="lazy" and decorative.

Hero Partial CSS — views/partials/page-hero.css (new)

The partial needs a small CSS file for the absolute positioning and mask treatment that Tailwind utilities do not express cleanly. Inline styles in the partial are acceptable as a fallback if a CSS file is undesirable, but a dedicated file follows the project's existing pattern (contact-block.css, homepage-hero.css).

Rules:

  • .page-hero — the existing wrapper. Add position: relative and isolation: isolate so the new media layers stack predictably.
  • .page-hero__mediaposition: absolute; inset: 0; z-index: 0; mask-image: linear-gradient(to right, transparent 0%, black 30%); (mirrored -webkit-mask-image). The image inside it is width: 100%; height: 100%; object-fit: cover;.
  • .page-hero__vectorposition: absolute; right: 0; bottom: 0; width: clamp(18rem, 38vw, 32rem); height: auto; z-index: 1; pointer-events: none;. The mobile breakpoint (≤ 767px) repositions the vector to fill the lower portion of the hero: width: 100vw; right: auto; left: 0; bottom: 0; and the __media mask switches to a top-to-bottom gradient so the text remains readable.
  • .page-hero__content (or the existing .content-wrapper div inside the hero) — position: relative; z-index: 10; so text always sits above the media layers.

These rules are scoped to .page-hero and only affect the page-hero partial. They do not affect the homepage-hero block, which uses a different root class.

Intro Partial — views/partials/page-intro.php (new)

A small PHP partial that renders a single centered column:

  • Wrapper: <section class="page-intro py-12 lg:py-20 text-center">
  • Container: <div class="container mx-auto max-w-3xl content-wrapper">
  • Inner: a single <p> element with text-lg lg:text-xl leading-relaxed containing wp_kses_post( $intro ).

No new fields, no new CSS file — Tailwind utilities cover the layout.

page.php Wiring

page.php changes from rendering the body directly to:

  1. Output the article container (existing behavior).
  2. If get_field( 'intro' ) is non-empty, call get_template_part( 'views/partials/page-intro' ).
  3. Render the_content (existing behavior).
  4. Render the sidebar if hasSidebar() (existing behavior).

The hero is still rendered by header.php before this template runs. No other behavior changes.

Data Flow

  • Editor experience: on every page (and post) edit screen, the existing Page Heading meta box shows the existing fields plus two new ones (Hero Image, Vector). The editor uploads the assets, sets a background color if desired, writes the optional heading override, intro, and CTAs, then writes the body in the standard block editor.
  • Render: header.php loads the page-hero partial → page.php loads the page-intro partial (if intro is set) → the_content renders the body blocks.
  • Per-page variation: hero image, vector, intro text, and body content. Everything else is shared.

Error Handling and Edge Cases

  • hero_image empty → hero uses the existing dark background color (or the default dark text-only style).
  • hero_vector empty → no vector renders; the right side of the hero is empty space.
  • intro empty → page-intro partial is not loaded; the page goes straight from hero to body.
  • heading empty → falls back to getTheTitle() (existing behavior, unchanged).
  • Background color set without a hero image → existing behavior; no change.
  • No sidebar field set → existing hasSidebar() filter returns false → no sidebar (existing behavior).
  • Lazy-loaded images must await load/complete before Playwright reads dimensions (existing project convention; see tests/mobile-homepage.spec.js:641).

Out of Scope

  • New ACF blocks. This layout uses the existing Page Heading meta box, not the block editor.
  • Front page, single posts, archives, search, 404 changes.
  • Homepage-hero block changes.
  • The contact-block, pull-quote, our-work, or other existing blocks.
  • A new contact CTA block in the page body. The mockup's bottom dark block is the existing site footer.
  • Migration of existing pages' content.

Verification

  • Add tests/inner-page.spec.js covering:
    • Desktop (1280px): hero, intro, body render in that order. When hero image is set, it is visible behind the hero. When vector is set, it sits on the right of the hero.
    • Mobile (402px): same order. Vector fills the lower portion of the hero. Intro is centered and within the 48rem max-width. No clipped text.
    • 320px and 767px: nothing clips, vector scales proportionally, intro remains readable.
    • A page with no intro field renders hero + body with no empty intro wrapper.
    • A page with no hero_image and no hero_vector renders the existing dark text-only hero unchanged.
  • Add a test page fixture (or use an existing page) to drive the test. If no fixture exists, the test seeds a page in a beforeAll and tears it down in afterAll.
  • Run npm run build to regenerate static/dist/.
  • Run the full Playwright suite (npx playwright test) — including the existing tests/mobile-homepage.spec.js and tests/site-a11y.spec.js — to confirm no regressions.
  • Run composer lint to catch PHPCS issues.
  • Capture a 1280px and a 402px full-page screenshot of the test fixture for visual comparison against notes/inner-page-mockup.png.

Acceptance Criteria

  1. Every WordPress page renders the new layout when the page-hero partial is active.
  2. The hero shows the page title (or heading override) and, when supplied, a background image and a decorative vector on the right.
  3. When the intro field is set, a centered narrow column appears under the hero. When empty, it is omitted entirely (no empty wrapper).
  4. The body renders the_content as before, with the standard block editor.
  5. The hero background image is masked so text on the left remains readable at all viewports.
  6. The vector scales proportionally from 320px through desktop and is repositioned below 768px to fill the lower portion of the hero.
  7. The page renders without console errors, axe violations, or PHPCS errors.
  8. Existing single posts, archives, search, and 404 pages render the page-hero partial unchanged.
  9. Existing tests (mobile-homepage, site-a11y) continue to pass.