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