Skip to content

fix: eliminate mobile CLS on calculator pages via fixed-height ad slots#4

Merged
kitfunso merged 2 commits into
masterfrom
fix/mobile-cls-adunit
Jun 4, 2026
Merged

fix: eliminate mobile CLS on calculator pages via fixed-height ad slots#4
kitfunso merged 2 commits into
masterfrom
fix/mobile-cls-adunit

Conversation

@kitfunso

@kitfunso kitfunso commented Jun 3, 2026

Copy link
Copy Markdown
Owner

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 CalculatorLayout with in-flow AdUnits; the homepage uses BaseLayout with none. The two in-content mobile slots reserved only min-height:100px with 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)

  • AdUnit.astro: new reserveHeight prop renders a fixed-height box + fixed-height <ins> (width:100%;height:Hpx, data-ad-format/data-full-width-responsive omitted, per Google AdSense fixed-size responsive-width pattern 9183363) so a fill cannot expand it. aria-hidden placeholder, collapse scoped to :not(.ad-reserved), animated max-height transition removed.
  • CalculatorLayout.astro: reserveHeight={100} on the two in-flow mobile slots.
  • BaseLayout.astro: skip reserved slots in the collapse loop; add body.has-anchor-ad only when the anchor is positively observed filled (so a pending/blocked anchor leaves no permanent gap).
  • global.css: define the previously no-op .safe-area-bottom; conditional anchor padding.

Test plan

  • npm run build clean (224 pages)
  • npm test - 159 files / 898 unit tests pass
  • eslint 0 errors
  • Built-output inspection: scoped selector compiles to .ad-container[cid]:not(.ad-reserved):has(...); both in-flow slots emit fixed height:100px with no responsive attrs; animated transition gone
  • Playwright (390x844 mobile): in-flow slots hold 100px when unfilled (CLS 0); legacy min-height-only sim filled to 250px registers CLS 0.91
  • Post-deploy: PageSpeed/Lighthouse mobile re-measure on coffee + mortgage (needs real ad fills) - target CLS < 0.1

Review

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

kitfunso and others added 2 commits June 3, 2026 22:23
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>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying boring-maths with  Cloudflare Pages  Cloudflare Pages

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

View logs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +145 to +149
} 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');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@kitfunso kitfunso merged commit 09bc141 into master Jun 4, 2026
2 checks passed
@kitfunso kitfunso deleted the fix/mobile-cls-adunit branch June 4, 2026 08:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant