Skip to content

Commit d1b0cfd

Browse files
authored
feat(adsense): add responsive site-wide ad unit (#38)
1 parent 820af72 commit d1b0cfd

8 files changed

Lines changed: 261 additions & 0 deletions

File tree

‎.github/workflows/deploy.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ jobs:
1111
build:
1212
runs-on: ubuntu-latest
1313
environment: deploy
14+
env:
15+
NEXT_PUBLIC_ADSENSE_CLIENT_ID: ${{ vars.NEXT_PUBLIC_ADSENSE_CLIENT_ID }}
16+
NEXT_PUBLIC_ADSENSE_SLOT_ID: ${{ vars.NEXT_PUBLIC_ADSENSE_SLOT_ID }}
1417

1518
steps:
1619
- name: Checkout code
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# AdSense Footer Slot Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Add one responsive, discreet AdSense slot after each route’s content and before the shared footer.
6+
7+
**Architecture:** A client component reads the publisher and slot IDs from public build-time environment variables. The root layout loads the AdSense verification script in the document head and places the ad component between route content and the global footer. A site privacy page documents AdSense cookies and ad personalization.
8+
9+
**Tech Stack:** Next.js 15 App Router, React 19, TypeScript, Tailwind CSS.
10+
11+
**Spec:** `docs/superpowers/specs/2026-10-28-adsense-footer-slot-design.md`
12+
13+
## Global Constraints
14+
15+
- Keep ads off when either `NEXT_PUBLIC_ADSENSE_CLIENT_ID` or `NEXT_PUBLIC_ADSENSE_SLOT_ID` is missing.
16+
- Do not add Auto Ads, sticky ads, or overlays.
17+
- Do not invent account IDs.
18+
- Store the AdSense IDs as GitHub Actions variables in the `deploy` environment.
19+
- Configure both GitHub variables before the release build so the unit appears on every route.
20+
- The repository has no configured test suite; run `npm run build` as the validation command.
21+
22+
## Review Focus
23+
24+
- Missing either ID must not render an empty advertising container.
25+
- Load the verification script only when the publisher ID is present; initialize the ad slot only when both IDs are present.
26+
- The unit must remain responsive on narrow screens.
27+
- The slot must remain after the route content and before the shared footer.
28+
- Initialization must not run more than once during normal navigation.
29+
30+
---
31+
32+
### Task 1: Add the global AdSense slot
33+
34+
**Files:**
35+
- Create: `src/app/components/AdSenseBanner/index.tsx`
36+
- Modify: `src/app/layout.tsx`
37+
38+
**Interfaces:**
39+
- Consumes: `NEXT_PUBLIC_ADSENSE_CLIENT_ID` and `NEXT_PUBLIC_ADSENSE_SLOT_ID`.
40+
- Produces: `AdSenseBanner`, a default-exported React component rendered once by the root layout.
41+
42+
- [ ] Add a client component that returns `null` until both IDs are present.
43+
- [ ] Load the AdSense verification script in the root document head and initialize one responsive slot with the “Anúncio” label.
44+
- [ ] Render the component between `{children}` and `<Footer />` in the root layout.
45+
- [ ] Run `npm run build`; expected result: production build succeeds.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# AdSense Footer Slot Design
2+
3+
## Intent
4+
5+
Add one discreet responsive AdSense placement to every site route, after page content and before the shared footer.
6+
7+
## Goals
8+
9+
- Keep the existing content and primary calls to action ahead of the ad.
10+
- Use the same placement on desktop and mobile, with responsive sizing.
11+
- Keep ads disabled until publisher and slot IDs are configured.
12+
- Provide a public privacy page describing AdSense cookies and ad personalization.
13+
14+
## Non-goals
15+
16+
- Enable AdSense Auto Ads or sticky/overlay formats.
17+
- Add a consent-management platform or change other account-level AdSense settings.
18+
19+
## Constraints
20+
21+
- The Next.js root layout renders `children` followed by the shared `Footer`.
22+
- IDs are created in the user's AdSense account, then stored as GitHub Actions environment variables; no IDs or credentials are invented.
23+
- AdSense code needed for site verification is emitted in the document head.
24+
- The repository has no configured test suite. Validate with the production build.
25+
26+
## Approaches
27+
28+
- **Global slot before the footer (chosen):** one component in the root layout gives route coverage and preserves the page's primary content first.
29+
- **AdSense Auto Ads:** less placement control, which conflicts with the request for a discreet fixed location.
30+
- **Sticky or overlay banner:** rejected because it obscures content and interrupts navigation.
31+
32+
## Design
33+
34+
Create a reusable client component that renders only when both `NEXT_PUBLIC_ADSENSE_CLIENT_ID` and `NEXT_PUBLIC_ADSENSE_SLOT_ID` are configured. Load the AdSense script in the document head for site verification, initialize the responsive slot once, label it “Anúncio”, and use a centered unit with a restrained neutral container. Mount it between `children` and `Footer` in the root layout. Add a privacy page and footer link describing AdSense cookies and personalization. This includes `/thanks` and privacy pages as part of the request for every route.
35+
36+
## Acceptance
37+
38+
- The ad slot is absent when either configuration value is missing.
39+
- When configured, the slot appears after route content and before the footer.
40+
- It is responsive and does not stick to or cover the viewport.
41+
- The production build succeeds.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use client';
2+
3+
import { useEffect, useRef } from 'react';
4+
5+
declare global {
6+
interface Window {
7+
adsbygoogle?: Record<string, unknown>[];
8+
}
9+
}
10+
11+
const publisherId = process.env.NEXT_PUBLIC_ADSENSE_CLIENT_ID;
12+
const slotId = process.env.NEXT_PUBLIC_ADSENSE_SLOT_ID;
13+
14+
export default function AdSenseBanner() {
15+
const initialized = useRef(false);
16+
const enabled = Boolean(publisherId && slotId);
17+
18+
useEffect(() => {
19+
if (!enabled || initialized.current) return;
20+
21+
initialized.current = true;
22+
(window.adsbygoogle ||= []).push({});
23+
}, [enabled]);
24+
25+
if (!enabled) {
26+
return null;
27+
}
28+
29+
return (
30+
<aside
31+
aria-label="Publicidade"
32+
className="mx-auto my-8 w-full max-w-7xl px-4 sm:px-6 lg:px-8"
33+
>
34+
<div className="mx-auto max-w-[760px] rounded-lg border border-gray-200 bg-gray-50 p-3">
35+
<p className="mb-2 text-[10px] font-medium uppercase tracking-wide text-gray-500">
36+
Anúncio
37+
</p>
38+
<ins
39+
className="adsbygoogle block min-h-[90px] w-full"
40+
style={{ display: 'block' }}
41+
data-ad-client={publisherId}
42+
data-ad-slot={slotId}
43+
data-ad-format="auto"
44+
data-full-width-responsive="true"
45+
/>
46+
</div>
47+
</aside>
48+
);
49+
}

‎src/app/components/Footer/index.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const Footer: React.FC = () => {
2626
<li><Link href="/projects" className="text-gray-400 hover:text-white transition-colors">Projetos</Link></li>
2727
<li><Link href="/blog" className="text-gray-400 hover:text-white transition-colors">Blog</Link></li>
2828
<li><Link href="/#contato" className="text-gray-400 hover:text-white transition-colors">Contato</Link></li>
29+
<li><Link href="/privacidade" className="text-gray-400 hover:text-white transition-colors">Privacidade</Link></li>
2930
</ul>
3031
</div>
3132

‎src/app/layout.tsx‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { Inter } from "next/font/google";
33
import "./globals.css";
44
import Footer from "./components/Footer";
55
import GoogleAnalytics from "./components/GoogleAnalytics";
6+
import AdSenseBanner from "./components/AdSenseBanner";
67
import Script from "next/script";
78
import { serviceOfferings } from "@/data/services";
89

910
const inter = Inter({ subsets: ["latin"] });
11+
const adsensePublisherId = process.env.NEXT_PUBLIC_ADSENSE_CLIENT_ID;
1012

1113
export const metadata: Metadata = {
1214
title: {
@@ -77,6 +79,15 @@ export default function RootLayout({
7779
<html lang="pt-BR">
7880
<head>
7981
<GoogleAnalytics />
82+
{adsensePublisherId && (
83+
<Script
84+
id="google-adsense"
85+
async
86+
crossOrigin="anonymous"
87+
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${adsensePublisherId}`}
88+
strategy="beforeInteractive"
89+
/>
90+
)}
8091
</head>
8192
<body className={inter.className} suppressHydrationWarning>
8293
<Script id="ld-organization" type="application/ld+json" strategy="afterInteractive"
@@ -124,6 +135,7 @@ export default function RootLayout({
124135
}) }}
125136
/>
126137
{children}
138+
<AdSenseBanner />
127139
<Footer />
128140
</body>
129141
</html>

‎src/app/privacidade/page.tsx‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type { Metadata } from "next";
2+
3+
export const metadata: Metadata = {
4+
title: "Privacidade e cookies",
5+
description: "Informações sobre cookies e anúncios exibidos no site HelpDev.",
6+
};
7+
8+
export default function PrivacyPage() {
9+
return (
10+
<main className="mx-auto w-full max-w-4xl px-4 py-16 sm:px-6 lg:px-8">
11+
<h1 className="text-3xl font-bold text-gray-900 sm:text-4xl">
12+
Privacidade e cookies
13+
</h1>
14+
<div className="mt-8 space-y-6 text-base leading-7 text-gray-700">
15+
<p>
16+
O site usa Google Analytics para medir visitas e uso das páginas. Na
17+
implementação padrão, o Google Analytics utiliza o cookie próprio{" "}
18+
<code className="rounded bg-gray-100 px-1">_ga</code> para distinguir
19+
navegadores e sessões e coleta informações como estatísticas de
20+
sessão, localização aproximada e dados do navegador e do dispositivo.
21+
Consulte as informações do Google sobre{" "}
22+
<a
23+
className="text-blue-700 underline hover:text-blue-900"
24+
href="https://support.google.com/analytics/answer/11593727?hl=pt-BR"
25+
target="_blank"
26+
rel="noopener noreferrer"
27+
>
28+
coleta de dados do Analytics
29+
</a>
30+
.
31+
</p>
32+
<p>
33+
Este site exibe anúncios fornecidos pelo Google AdSense. O Google e
34+
parceiros de publicidade podem usar cookies e identificadores para
35+
veicular e personalizar anúncios, medir o desempenho das campanhas e
36+
evitar que o mesmo anúncio seja exibido repetidamente. Esses recursos
37+
podem considerar visitas anteriores a este site ou a outros sites.
38+
</p>
39+
<p>
40+
Você pode gerenciar a personalização de anúncios nas{" "}
41+
<a
42+
className="text-blue-700 underline hover:text-blue-900"
43+
href="https://adssettings.google.com/"
44+
target="_blank"
45+
rel="noopener noreferrer"
46+
>
47+
Configurações de anúncios do Google
48+
</a>
49+
. Também pode consultar as opções de desativação disponibilizadas pela{" "}
50+
<a
51+
className="text-blue-700 underline hover:text-blue-900"
52+
href="https://www.aboutads.info/choices/"
53+
target="_blank"
54+
rel="noopener noreferrer"
55+
>
56+
Digital Advertising Alliance
57+
</a>
58+
.
59+
</p>
60+
<p>
61+
Para desativar a medição do Google Analytics neste navegador, o Google
62+
oferece o{" "}
63+
<a
64+
className="text-blue-700 underline hover:text-blue-900"
65+
href="https://tools.google.com/dlpage/gaoptout?hl=pt-BR"
66+
target="_blank"
67+
rel="noopener noreferrer"
68+
>
69+
complemento de desativação do Analytics
70+
</a>
71+
.
72+
</p>
73+
<p>
74+
O tratamento de dados feito pelo Google segue a{" "}
75+
<a
76+
className="text-blue-700 underline hover:text-blue-900"
77+
href="https://policies.google.com/privacy?hl=pt-BR"
78+
target="_blank"
79+
rel="noopener noreferrer"
80+
>
81+
Política de Privacidade do Google
82+
</a>
83+
. Para saber mais sobre cookies usados pelo AdSense, consulte a página
84+
oficial sobre{" "}
85+
<a
86+
className="text-blue-700 underline hover:text-blue-900"
87+
href="https://support.google.com/adsense/answer/7549925?hl=pt-BR"
88+
target="_blank"
89+
rel="noopener noreferrer"
90+
>
91+
como o AdSense usa cookies
92+
</a>
93+
.
94+
</p>
95+
<p>
96+
Para dúvidas ou solicitações relacionadas à privacidade neste site,
97+
entre em contato pelo e-mail{" "}
98+
<a
99+
className="text-blue-700 underline hover:text-blue-900"
100+
href="mailto:gbzarelli@helpdev.com.br"
101+
>
102+
gbzarelli@helpdev.com.br
103+
</a>
104+
.
105+
</p>
106+
</div>
107+
</main>
108+
);
109+
}

‎src/app/sitemap.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
1010
'/articles',
1111
'/projects',
1212
'/blog',
13+
'/privacidade',
1314
];
1415

1516
return routes.map((route) => ({

0 commit comments

Comments
 (0)