fix: eliminate mobile CLS on calculator pages via fixed-height ad slots#4
Conversation
In-flow AdSense slots in CalculatorLayout reserved only min-height:100px with responsive fill, so a filled creative (~250px) expanded the box and shifted content (mobile CLS ~0.45 vs 0.045 on the ad-free homepage). - AdUnit: add reserveHeight prop for a fixed-height box + fixed-height ins (data-ad-format/data-full-width-responsive omitted per AdSense 9183363), aria-hidden placeholder, collapse scoped to :not(.ad-reserved), drop the animated max-height transition - CalculatorLayout: reserveHeight=100 on the two in-flow mobile slots - BaseLayout: skip reserved slots in the collapse loop; set body.has-anchor-ad only when the anchor fills - global.css: define the previously no-op .safe-area-bottom; conditional anchor padding Playwright: in-flow slots hold 100px (CLS 0); legacy sim expands to 250px (CLS 0.91). Build green, 898 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The has-anchor-ad branch fired on !unfilled, but at the 3s check the anchor's AdSense status is often still pending (delayed script, slow network, ad blocker). Pending was treated as filled, adding 100px bottom padding that nothing removed if the ad never filled -> a permanent bottom gap on ad-blocked/unfilled pages. Gate on a positively-observed filled state (status==='filled' || hasContent). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploying boring-maths with
|
| Latest commit: |
d9ec74e
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2ca33e37.boring-maths.pages.dev |
| Branch Preview URL: | https://fix-mobile-cls-adunit.boring-maths.pages.dev |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d9ec74edae
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } else if (anchor && filled) { | ||
| // Anchor POSITIVELY filled: reserve bottom space so the fixed bar doesn't | ||
| // cover content. Gated on an observed fill (not !unfilled) so a pending, | ||
| // blocked, or never-loaded anchor never leaves a permanent bottom gap. | ||
| document.body.classList.add('has-anchor-ad'); |
There was a problem hiding this comment.
Recheck anchor fill after AdSense actually processes
On mobile sessions with no early interaction, the AdSense loader's own 3s timer only appends the async script, while this one-shot status check runs at the same 3s mark and can see neither data-ad-status nor an iframe yet. If the anchor fills after the script finishes loading, this branch is never revisited, so has-anchor-ad is not added and the fixed bottom ad can cover the page content.
Useful? React with 👍 / 👎.
Problem
Lighthouse mobile CLS is ~0.45 on every calculator page (good is <0.1) vs 0.045 on the ad-free homepage. CLS is a Core Web Vitals ranking signal and 60%+ of traffic is mobile, so this suppresses rankings across all 149 calculator pages and degrades the tap experience.
Root cause
Calculator pages route through
CalculatorLayoutwith in-flowAdUnits; the homepage usesBaseLayoutwith none. The two in-content mobile slots reserved onlymin-height:100pxwith responsive fill, so a filled creative (~250px) expanded the box and pushed content down. A separate animated post-paint collapse compounded it.Fix (4 files)
reserveHeightprop renders a fixed-height box + fixed-height<ins>(width:100%;height:Hpx,data-ad-format/data-full-width-responsiveomitted, per Google AdSense fixed-size responsive-width pattern 9183363) so a fill cannot expand it. aria-hidden placeholder, collapse scoped to:not(.ad-reserved), animatedmax-heighttransition removed.reserveHeight={100}on the two in-flow mobile slots.body.has-anchor-adonly when the anchor is positively observed filled (so a pending/blocked anchor leaves no permanent gap)..safe-area-bottom; conditional anchor padding.Test plan
npm run buildclean (224 pages)npm test- 159 files / 898 unit tests passeslint0 errors.ad-container[cid]:not(.ad-reserved):has(...); both in-flow slots emit fixedheight:100pxwith no responsive attrs; animated transition goneReview
Passed dev-framework-rl gates: plan-eng 84 / plan-design 88, code-review 91, independent-review 90, codex (gpt-5.5) cross-model review which caught a false-positive-filled anchor bug now fixed.
🤖 Generated with Claude Code