9.1 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
pagepost-type views rendered bypage.php(and through it, thepage-heropartial thatheader.phpalready 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:
- Hero —
views/partials/page-hero.php(existing), extended with two new ACF fields and two new visual slots. - Intro — new
views/partials/page-intro.php, rendered bypage.phpbetween the hero andthe_contentwhen theintroACF field is non-empty. - Body —
the_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 whenhero_style !== 'none'. Optional.hero_vector— type:image, return_format:array, library:all, preview_size:medium. Conditional: shown whenhero_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_imageas an absolutely-positioned<img>inside a new.page-hero__mediacontainer, masked with a left-to-right fade so the text on the left remains readable. - Render
hero_vectoras an absolutely-positioned, right-anchored, bottom-anchored<img>inside a new.page-hero__vectorcontainer, witharia-hidden="true"androle="presentation". - Stop rendering the
introparagraph inside the hero. Theintrofield 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, andcall_to_actionsrendering unchanged. - When no
hero_imageis set, the existingbackground_colorstyle (or the defaultbg-darktoken) 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. A dedicated file follows the project's existing pattern (contact-block.css, homepage-hero.css).
Rules:
.page-hero— the existing wrapper. Addposition: relativeandisolation: isolateso the new media layers stack predictably..page-hero__media—position: absolute; inset: 0; z-index: 0; mask-image: linear-gradient(to right, transparent 0%, black 30%);(mirrored-webkit-mask-image). The image inside it iswidth: 100%; height: 100%; object-fit: cover;..page-hero__vector—position: 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__mediamask switches to a top-to-bottom gradient so the text remains readable..page-hero__content(or the existing.content-wrapperdiv 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 withtext-lg lg:text-xl leading-relaxedcontainingwp_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:
- Output the article container (existing behavior).
- If
get_field( 'intro' )is non-empty, callget_template_part( 'views/partials/page-intro' ). - Render
the_content(existing behavior). - 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.phploads the page-hero partial →page.phploads the page-intro partial (if intro is set) →the_contentrenders the body blocks. - Per-page variation: hero image, vector, intro text, and body content. Everything else is shared.
Error Handling and Edge Cases
hero_imageempty → hero uses the existing dark background color (or the default dark text-only style).hero_vectorempty → no vector renders; the right side of the hero is empty space.introempty → page-intro partial is not loaded; the page goes straight from hero to body.headingempty → falls back togetTheTitle()(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/completebefore Playwright reads dimensions (existing project convention; seetests/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.jscovering:- 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
introfield renders hero + body with no empty intro wrapper. - A page with no
hero_imageand nohero_vectorrenders 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
beforeAlland tears it down inafterAll. - Run
npm run buildto regeneratestatic/dist/. - Run the full Playwright suite (
npx playwright test) — including the existingtests/mobile-homepage.spec.jsandtests/site-a11y.spec.js— to confirm no regressions. - Run
composer lintto 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
- Every WordPress page renders the new layout when the page-hero partial is active.
- The hero shows the page title (or heading override) and, when supplied, a background image and a decorative vector on the right.
- When the
introfield is set, a centered narrow column appears under the hero. When empty, it is omitted entirely (no empty wrapper). - The body renders
the_contentas before, with the standard block editor. - The hero background image is masked so text on the left remains readable at all viewports.
- The vector scales proportionally from 320px through desktop and is repositioned below 768px to fill the lower portion of the hero.
- The page renders without console errors, axe violations, or PHPCS errors.
- Existing single posts, archives, search, and 404 pages render the page-hero partial unchanged.
- Existing tests (
mobile-homepage,site-a11y) continue to pass.