Update inner page layout spec for hero restructure (no hard-coded copy, image at natural size)

This commit is contained in:
Keith Solomon
2026-07-01 16:12:48 -05:00
parent 2bda7f21eb
commit 4aa6ff694d
@@ -2,7 +2,7 @@
## Goal ## 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. 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 the page title, an optional hero image at its natural size on the right with the bottom hanging over the hero's bottom edge, a decorative vector along the lower-left with breathing room from the bottom, an optional intro text block (the editor-supplied `intro` field) on the left below the H1, and the standard editor body below the hero. Different pages vary only by hero image, vector, intro text, and body content.
## Scope ## Scope
@@ -38,15 +38,13 @@ No other ACF group changes. The existing `heading`, `intro`, and `call_to_action
Changes: Changes:
- The wrapper `<div>` no longer carries the `bg-dark` or `bg-cover bg-no-repeat` Tailwind utilities. The wrapper inherits a blue-to-teal gradient from `.page-hero` (defined in `styles/blocks/page-hero.css`), the same gradient used by the homepage-hero block. When the editor sets a `background_color` ACF field, the inline `style` attribute on the wrapper overrides the gradient with that solid color. - The wrapper `<div>` no longer carries the `bg-dark` or `bg-cover bg-no-repeat` Tailwind utilities. The wrapper inherits a blue-to-teal gradient from `.page-hero` (defined in `styles/blocks/page-hero.css`), the same gradient used by the homepage-hero block. When the editor sets a `background_color` ACF field, the inline `style` attribute on the wrapper overrides the gradient with that solid color.
- Read the two new fields (`hero_image`, `hero_vector`). - Read the two new fields (`hero_image`, `hero_vector`) and the existing `intro` field.
- Render `hero_image` as an absolutely-positioned `<img>` inside a new `.page-hero__media` container. 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 the breadcrumbs, the H1, and the intro **inside** a single `.page-hero__content` container, **before** the image and vector markup. The H1 has a dedicated `.page-hero__heading` class for typography control. The intro, when non-empty, is a single `.page-hero__intro` div containing only the editor-supplied intro text (no hard-coded heading).
- Render `hero_vector` as an absolutely-positioned, full-width `<img>` anchored to the bottom of the hero, with `aria-hidden="true"` and `role="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 `hero_image` as a foreground absolutely-positioned `<img>` (class `.page-hero__media`), anchored to the right of the hero with `bottom: -10%` so the image's bottom hangs over the bottom edge of the hero. The image is shown at its natural size (no `object-fit: cover`, no mask); it sits above the gradient backdrop and below the content layer.
- Render the heading (h1) inside the new `.page-hero__content` container, above the media layers. - Render `hero_vector` as an absolutely-positioned `<img>` (class `.page-hero__vector`), anchored to the bottom-left of the hero with `bottom: clamp(1.5rem, 4vw, 3rem)` for spacing from the bottom edge. The vector sits above the image and below the content.
- Load the `page-intro` partial inside `.page-hero__content`, below the heading. The intro block only renders when the `intro` ACF field is non-empty. - When no `hero_image` is set, the gradient (or the override `background_color`) is the only background. When no `hero_vector` is set, the bottom of the hero is just the gradient.
- Keep the breadcrumb rendering unchanged.
- When no `hero_image` is set, the gradient (or the override `background_color`) is the only background.
Both new images are `loading="lazy"` and decorative. Both new images are `loading="lazy"` and decorative. No hard-coded copy is rendered.
## Hero Partial CSS — `styles/blocks/page-hero.css` (new) ## Hero Partial CSS — `styles/blocks/page-hero.css` (new)
@@ -54,30 +52,19 @@ A dedicated file follows the project's existing pattern (`contact-block.css`, `h
Rules: 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` — the wrapper. `position: relative; isolation: isolate; overflow: hidden; min-height: clamp(31rem, 38vw, 36rem);`. `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 is `width: 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__content``position: relative; z-index: 10; padding-block: clamp(2rem, 6vw, 4rem);` so the heading, intro, and breadcrumbs always sit above the image and vector layers.
- `.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 `__media` mask switches to a top-to-bottom gradient. - `.page-hero__heading``font-family: var(--font-quincy, 'Quincy', serif); font-weight: 400; font-size: clamp(2.5rem, 6vw, 4.5rem); line-height: 1.05; max-width: 36rem;` so the H1 doesn't span the full hero width and stays aligned with the intro text.
- `.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``font-size: clamp(1rem, 1.5vw, 1.25rem); line-height: 1.5; max-width: 36rem; opacity: 0.95;` so the intro text sits alongside the H1.
- `.page-hero__intro``margin-top: clamp(2rem, 6vw, 4rem);` provides spacing between the H1 and the intro block. - `.page-hero__media``position: absolute; right: clamp(1.5rem, 4vw, 4rem); bottom: -10%; width: clamp(18rem, 32vw, 28rem); height: auto; pointer-events: none; z-index: 1;`. The image is shown at its natural size.
- `.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. - `.page-hero__vector``position: absolute; left: 0; bottom: clamp(1.5rem, 4vw, 3rem); width: clamp(28rem, 60vw, 56rem); height: auto; pointer-events: none; z-index: 5;`. The vector is sized to span the lower-left of the hero with breathing room from the bottom edge.
- Mobile (≤ 767px) breakpoint: hero min-height becomes `clamp(28rem, 130vw, 40rem)`; image shrinks to `60vw` and sits flush right (`right: 0`); vector shrinks to `80vw`.
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. 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">` containing `wp_kses_post( $intro )`.
- The partial returns early when the `intro` field 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 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. `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. No other behavior changes.
## Data Flow ## Data Flow
@@ -89,7 +76,7 @@ The partial is loaded by `page-hero.php` (not `page.php`). The intro is **part o
- `hero_image` empty → hero uses the gradient (or the override `background_color`) as the only background. - `hero_image` empty → hero uses the gradient (or the override `background_color`) as the only background.
- `hero_vector` empty → no vector renders; the bottom of the hero is just the gradient. - `hero_vector` empty → no vector renders; the bottom of the hero is just the gradient.
- `intro` empty → page-intro partial returns early; the hero renders without the intro block, and the page goes straight from hero to body. - `intro` empty → the `.page-hero__intro` div is not rendered; the hero shows the H1 only.
- `heading` empty → falls back to `getTheTitle()` (existing behavior, unchanged). - `heading` empty → falls back to `getTheTitle()` (existing behavior, unchanged).
- Background color set without a hero image → existing behavior; the inline `style` overrides the gradient with the solid color. - Background color set without a hero image → existing behavior; the inline `style` overrides the gradient with the solid color.
- No sidebar field set → existing `hasSidebar()` filter returns false → no sidebar (existing behavior). - No sidebar field set → existing `hasSidebar()` filter returns false → no sidebar (existing behavior).
@@ -107,7 +94,7 @@ The partial is loaded by `page-hero.php` (not `page.php`). The intro is **part o
## Verification ## Verification
- Add `tests/inner-page.spec.js` covering: - Add `tests/inner-page.spec.js` covering:
- 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. - Desktop (1280px): hero, intro, body render with the intro block as a descendant of the hero. The H1 is 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. - 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. - 320px and 767px: nothing clips, hero and intro remain inside the viewport.
- A page with no `intro` field renders the hero without a `.page-hero__intro` wrapper. - A page with no `intro` field renders the hero without a `.page-hero__intro` wrapper.
@@ -121,11 +108,11 @@ The partial is loaded by `page-hero.php` (not `page.php`). The intro is **part o
## Acceptance Criteria ## Acceptance Criteria
1. Every WordPress page renders the new layout when the page-hero partial is active. 1. Every WordPress page renders the new layout when the page-hero partial is active.
2. 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. 2. The hero shows the blue-to-teal gradient backdrop, the page title (or heading override) in the Quincy serif face, and — when supplied — a hero image at its natural size on the right with the bottom hanging over the hero's bottom edge, and a decorative vector along the lower-left with some spacing from the bottom edge.
3. When the `intro` field is set, a two-column heading + body block appears inside the hero below the H1. When empty, it is omitted entirely (no empty wrapper). 3. When the `intro` field is set, a single text block appears inside the hero below the H1, in the same left column. When empty, the intro block is omitted entirely.
4. The body renders `the_content` as before, with the standard block editor. 4. The body renders `the_content` as before, with the standard block editor.
5. 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. 5. No hard-coded copy is rendered by the partial — only the page title, the editor-supplied `intro` text, and the editor-supplied images.
6. The vector scales proportionally from 320px through desktop and is repositioned below 768px to fill the lower portion of the hero. 6. The hero image and vector render at all viewports without clipping.
7. The page renders without console errors, axe violations, or PHPCS errors. 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. 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. 9. Existing tests (`mobile-homepage`, `site-a11y`) continue to pass.