- Goal: hero is now a blue-to-teal gradient (not a flat dark color) and the intro block lives inside the hero composition (not as a separate page section). - Architecture: reduced from 3-part (hero / intro / body) to 2-part (hero / body) since the intro is now embedded in the hero. - Hero Partial: dropped the bg-dark utility (gradient is set in CSS), the hero image is anchored to the right with a left-to-right fade, and the vector is full-width anchored to the bottom at z-index 2. - Hero Partial CSS: the file is at styles/blocks/page-hero.css (not views/partials/page-hero.css). Updated the rules to match the new gradient + image right-side + vector bottom-center + intro grid layout. - Intro Partial: rewritten as a fragment for embedding inside the hero (two-column heading + body grid) rather than a centered standalone section. Partial is loaded by page-hero.php, not page.php. - page.php Wiring: no longer loads the page-intro partial directly; the partial is loaded by page-hero.php. - Error Handling, Verification, and Acceptance Criteria: updated to match the new structure (no empty .page-intro wrapper check; intro is a descendant of the hero; gradient + image-right + vector-bottom-center expectations). Co-Authored-By: Claude <noreply@anthropic.com>
11 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 blue-to-teal gradient hero with a background image and a decorative vector, the page title, an optional intro block (heading + body) inside the hero, 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 two-part composition:
- Hero —
views/partials/page-hero.php(existing), extended with the blue-to-teal gradient backdrop, two new ACF fields, the hero image rendered on the right (faded to transparent on the left so the heading has a clear backdrop), a decorative vector along the bottom, the heading, and the intro block. - Body —
the_content, unchanged.
page-hero.php is the orchestrator: it renders the gradient backdrop, the optional hero image and vector, the heading, and the intro block all inside a single .page-hero wrapper. The intro ACF field, when set, renders as a two-column heading + body block inside the hero (not as a separate page section). The intro heading is a hard-coded "Our Work" label; the body is the editor-supplied intro text.
page.php continues to render the_content after the hero. Sidebar behavior (already gated by hasSidebar() and the Page Sidebar field group) is preserved. page.php no longer loads the page-intro partial directly — the partial is loaded by page-hero.php instead.
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:
- The wrapper
<div>no longer carries thebg-darkorbg-cover bg-no-repeatTailwind utilities. The wrapper inherits a blue-to-teal gradient from.page-hero(defined instyles/blocks/page-hero.css), the same gradient used by the homepage-hero block. When the editor sets abackground_colorACF field, the inlinestyleattribute on the wrapper overrides the gradient with that solid color. - Read the two new fields (
hero_image,hero_vector). - Render
hero_imageas an absolutely-positioned<img>inside a new.page-hero__mediacontainer. The image is anchored to the right (object-position: right center) and masked with a left-to-right fade (transparent on the left, visible on the right) so the heading on the left has the gradient as a clean backdrop. - Render
hero_vectoras an absolutely-positioned, full-width<img>anchored to the bottom of the hero, witharia-hidden="true"androle="presentation". The vector sits above the media layer (z-index: 2) and below the content (z-index: 10), so it is visible across the bottom of the hero. - Render the heading (h1) inside the new
.page-hero__contentcontainer, above the media layers. - Load the
page-intropartial inside.page-hero__content, below the heading. The intro block only renders when theintroACF field is non-empty. - Keep the breadcrumb rendering unchanged.
- When no
hero_imageis set, the gradient (or the overridebackground_color) is the only background.
Both new images are loading="lazy" and decorative.
Hero Partial CSS — styles/blocks/page-hero.css (new)
A dedicated file follows the project's existing pattern (contact-block.css, homepage-hero.css).
Rules:
.page-hero— the wrapper.position: relative; isolation: isolate; overflow: hidden;so the new media layers stack predictably.background: linear-gradient(83.68deg, #032F46 3.13%, #006196 45.91%, #8FC9E6 96.27%);— same gradient as the homepage-hero block..page-hero__media—position: absolute; inset: 0; z-index: 1; -webkit-mask-image: linear-gradient(to left, transparent 0%, black 50%); mask-image: linear-gradient(to left, transparent 0%, black 50%);. The image inside it iswidth: 100%; height: 100%; object-fit: cover; object-position: right center;. The mask fades the image from invisible on the left to fully visible on the right..page-hero__vector—position: absolute; left: 0; bottom: 0; width: 100%; height: auto; z-index: 2; pointer-events: none;. The mobile breakpoint (≤ 767px) repositions the vector to fill the lower portion of the hero:width: 100vw; right: auto;and the__mediamask switches to a top-to-bottom gradient..page-hero__content—position: relative; z-index: 10;so the heading and intro block always sit above the media and vector layers..page-hero__intro—margin-top: clamp(2rem, 6vw, 4rem);provides spacing between the H1 and the intro block..page-hero__intro-inner—display: grid; gap: clamp(1.5rem, 4vw, 3rem); grid-template-columns: 1fr;on mobile,grid-template-columns: 1fr 2fr;on desktop, so the heading and body form a two-column layout matching the mockup.
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 two-column heading + body block inside the page hero:
- Wrapper:
<div class="page-hero__intro text-light"> - Inner:
<div class="page-hero__intro-inner">— a CSS grid (1 column on mobile, 1fr / 2fr on desktop) holding:<h2 class="page-hero__intro-heading text-2xl lg:text-3xl font-quincy font-normal leading-tight">containing the hard-coded label "Our Work".<p class="page-hero__intro-text text-base lg:text-lg leading-relaxed text-light/90">containingwp_kses_post( $intro ).
- The partial returns early when the
introfield is empty, so no empty wrapper is rendered.
The partial is loaded by page-hero.php (not page.php). The intro is part of the hero composition in the rendered page, not a separate page section.
page.php Wiring
page.php continues to render the article container, the_content, and the sidebar (when hasSidebar()). The hero is rendered by header.php before this template runs. The page-intro partial is no longer loaded directly by page.php — it is loaded by page-hero.php (inside the hero wrapper). 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-hero.phprenders the gradient + optional hero image + vector + heading + (whenintrois set) the intro block, all inside.page-hero→page.phprendersthe_contentfor 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 gradient (or the overridebackground_color) as the only background.hero_vectorempty → no vector renders; the bottom of the hero is just the gradient.introempty → page-intro partial returns early; the hero renders without the intro block, and the page goes straight from hero to body.headingempty → falls back togetTheTitle()(existing behavior, unchanged).- Background color set without a hero image → existing behavior; the inline
styleoverrides the gradient with the solid color. - 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 with the intro block as a descendant of the hero. The heading and intro heading/body are all visible. The hero exposes the gradient backdrop.
- Mobile (402px): intro is inside the hero and fits within the viewport. The vector is visible at the bottom of the hero. No clipped text.
- 320px and 767px: nothing clips, hero and intro remain inside the viewport.
- A page with no
introfield renders the hero without a.page-hero__introwrapper. - A page with no
hero_imageand nohero_vectorrenders the existing gradient 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 blue-to-teal gradient backdrop, the page title (or heading override), and — when supplied — a background image on the right side and a decorative vector along the bottom.
- When the
introfield is set, a two-column heading + body block appears inside the hero below the H1. 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 it is visible on the right and fades to transparent on the left, so the heading on the left has the gradient as a clean backdrop 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.