From 0b03d2708bef448ec2f7c125d389ce5ed13e2273 Mon Sep 17 00:00:00 2001 From: Guilherme Biff Zarelli Date: Wed, 28 Oct 2026 15:52:48 -0300 Subject: [PATCH] fix(adsense): publish ads.txt and hide unfilled slot --- .../2026-10-28-adsense-delivery-readiness.md | 39 +++++++++++++++++++ .../2026-10-28-adsense-footer-slot-design.md | 6 +++ public/ads.txt | 1 + src/app/components/AdSenseBanner/index.tsx | 30 +++++++++++--- 4 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 docs/superpowers/plans/2026-10-28-adsense-delivery-readiness.md create mode 100644 public/ads.txt diff --git a/docs/superpowers/plans/2026-10-28-adsense-delivery-readiness.md b/docs/superpowers/plans/2026-10-28-adsense-delivery-readiness.md new file mode 100644 index 0000000..15722d8 --- /dev/null +++ b/docs/superpowers/plans/2026-10-28-adsense-delivery-readiness.md @@ -0,0 +1,39 @@ +# AdSense Delivery Readiness Implementation Plan + +> **For agentic workers:** Execute inline in this session; validate with a production build and Playwright browser check. + +**Goal:** Publish the site's ads.txt seller declaration and remove the visible ad frame when AdSense reports no fill. + +**Architecture:** Add the exact seller line to Next.js `public/ads.txt`, which exports to the domain root. Observe the rendered AdSense slot's `data-ad-status` attribute and unmount the banner when it becomes `unfilled`. + +**Tech Stack:** Next.js static export, React, TypeScript, Playwright CLI. + +**Spec:** `docs/superpowers/specs/2026-10-28-adsense-footer-slot-design.md` + +## Global Constraints + +- Use publisher ID `ca-pub-4334384724173584` and responsive slot ID `8535614411`. +- Keep the banner responsive and after page content, before the shared footer. +- Do not treat client code as a substitute for AdSense account activation or site approval. + +## Review Focus + +- AdSense sets `data-ad-status="unfilled"` asynchronously: observe the attribute and hide the whole banner container. +- A filled ad remains visible: only hide on the exact `unfilled` status. +- Next static export serves `/ads.txt` at the root with plain text, not the app fallback HTML. + +### Task 1: Publish ads.txt and hide unfilled banner + +**Files:** +- Create: `public/ads.txt` +- Modify: `src/app/components/AdSenseBanner/index.tsx` + +**Interfaces:** +- The ads.txt line is `google.com, pub-4334384724173584, DIRECT, f08c47fec0942fa0`. +- The component continues to render only when both existing environment values are configured. + +- [x] Check the live failure in Playwright: with `data-ad-status="unfilled"`, the `Publicidade` aside is currently visible. +- [x] Add the seller declaration and observe slot status changes with a `MutationObserver`; return `null` only after status is exactly `unfilled`. +- [x] Run `NEXT_PUBLIC_ADSENSE_CLIENT_ID=ca-pub-4334384724173584 NEXT_PUBLIC_ADSENSE_SLOT_ID=8535614411 npm run build`. +- [x] Serve the static export and verify `/ads.txt` contains the seller line and an unfilled slot hides the whole aside. +- [x] Commit as `fix(adsense): publish ads.txt and hide unfilled slot`. diff --git a/docs/superpowers/specs/2026-10-28-adsense-footer-slot-design.md b/docs/superpowers/specs/2026-10-28-adsense-footer-slot-design.md index 4c90c9f..3341891 100644 --- a/docs/superpowers/specs/2026-10-28-adsense-footer-slot-design.md +++ b/docs/superpowers/specs/2026-10-28-adsense-footer-slot-design.md @@ -39,3 +39,9 @@ Create a reusable client component that renders only when both `NEXT_PUBLIC_ADSE - When configured, the slot appears after route content and before the footer. - It is responsive and does not stick to or cover the viewport. - The production build succeeds. + +## Delivery readiness follow-up + +- Publish the AdSense seller declaration at `/ads.txt` using the existing HelpDev publisher ID. +- Hide the complete banner container after AdSense marks its slot `data-ad-status="unfilled"`; a blank placeholder should not remain visible when Google has no ad to serve. +- These changes do not bypass AdSense account setup or site review. diff --git a/public/ads.txt b/public/ads.txt new file mode 100644 index 0000000..0bacac9 --- /dev/null +++ b/public/ads.txt @@ -0,0 +1 @@ +google.com, pub-4334384724173584, DIRECT, f08c47fec0942fa0 diff --git a/src/app/components/AdSenseBanner/index.tsx b/src/app/components/AdSenseBanner/index.tsx index cb6135b..fae6e49 100644 --- a/src/app/components/AdSenseBanner/index.tsx +++ b/src/app/components/AdSenseBanner/index.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; declare global { interface Window { @@ -13,16 +13,35 @@ const slotId = process.env.NEXT_PUBLIC_ADSENSE_SLOT_ID; export default function AdSenseBanner() { const initialized = useRef(false); + const adElement = useRef(null); + const [unfilled, setUnfilled] = useState(false); const enabled = Boolean(publisherId && slotId); useEffect(() => { - if (!enabled || initialized.current) return; + if (!enabled) return; - initialized.current = true; - (window.adsbygoogle ||= []).push({}); + const element = adElement.current; + if (!element) return; + + const observer = new MutationObserver(() => { + setUnfilled(element.getAttribute('data-ad-status') === 'unfilled'); + }); + observer.observe(element, { + attributes: true, + attributeFilter: ['data-ad-status'], + }); + + setUnfilled(element.getAttribute('data-ad-status') === 'unfilled'); + + if (!initialized.current) { + initialized.current = true; + (window.adsbygoogle ||= []).push({}); + } + + return () => observer.disconnect(); }, [enabled]); - if (!enabled) { + if (!enabled || unfilled) { return null; } @@ -36,6 +55,7 @@ export default function AdSenseBanner() { AnĂșncio