Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ jobs:
build:
runs-on: ubuntu-latest
environment: deploy
env:
NEXT_PUBLIC_ADSENSE_CLIENT_ID: ${{ vars.NEXT_PUBLIC_ADSENSE_CLIENT_ID }}
NEXT_PUBLIC_ADSENSE_SLOT_ID: ${{ vars.NEXT_PUBLIC_ADSENSE_SLOT_ID }}

steps:
- name: Checkout code
Expand Down
45 changes: 45 additions & 0 deletions docs/superpowers/plans/2026-10-28-adsense-footer-slot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# AdSense Footer Slot Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add one responsive, discreet AdSense slot after each route’s content and before the shared footer.

**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.

**Tech Stack:** Next.js 15 App Router, React 19, TypeScript, Tailwind CSS.

**Spec:** `docs/superpowers/specs/2026-10-28-adsense-footer-slot-design.md`

## Global Constraints

- Keep ads off when either `NEXT_PUBLIC_ADSENSE_CLIENT_ID` or `NEXT_PUBLIC_ADSENSE_SLOT_ID` is missing.
- Do not add Auto Ads, sticky ads, or overlays.
- Do not invent account IDs.
- Store the AdSense IDs as GitHub Actions variables in the `deploy` environment.
- Configure both GitHub variables before the release build so the unit appears on every route.
- The repository has no configured test suite; run `npm run build` as the validation command.

## Review Focus

- Missing either ID must not render an empty advertising container.
- Load the verification script only when the publisher ID is present; initialize the ad slot only when both IDs are present.
- The unit must remain responsive on narrow screens.
- The slot must remain after the route content and before the shared footer.
- Initialization must not run more than once during normal navigation.

---

### Task 1: Add the global AdSense slot

**Files:**
- Create: `src/app/components/AdSenseBanner/index.tsx`
- Modify: `src/app/layout.tsx`

**Interfaces:**
- Consumes: `NEXT_PUBLIC_ADSENSE_CLIENT_ID` and `NEXT_PUBLIC_ADSENSE_SLOT_ID`.
- Produces: `AdSenseBanner`, a default-exported React component rendered once by the root layout.

- [ ] Add a client component that returns `null` until both IDs are present.
- [ ] Load the AdSense verification script in the root document head and initialize one responsive slot with the “Anúncio” label.
- [ ] Render the component between `{children}` and `<Footer />` in the root layout.
- [ ] Run `npm run build`; expected result: production build succeeds.
41 changes: 41 additions & 0 deletions docs/superpowers/specs/2026-10-28-adsense-footer-slot-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# AdSense Footer Slot Design

## Intent

Add one discreet responsive AdSense placement to every site route, after page content and before the shared footer.

## Goals

- Keep the existing content and primary calls to action ahead of the ad.
- Use the same placement on desktop and mobile, with responsive sizing.
- Keep ads disabled until publisher and slot IDs are configured.
- Provide a public privacy page describing AdSense cookies and ad personalization.

## Non-goals

- Enable AdSense Auto Ads or sticky/overlay formats.
- Add a consent-management platform or change other account-level AdSense settings.

## Constraints

- The Next.js root layout renders `children` followed by the shared `Footer`.
- IDs are created in the user's AdSense account, then stored as GitHub Actions environment variables; no IDs or credentials are invented.
- AdSense code needed for site verification is emitted in the document head.
- The repository has no configured test suite. Validate with the production build.

## Approaches

- **Global slot before the footer (chosen):** one component in the root layout gives route coverage and preserves the page's primary content first.
- **AdSense Auto Ads:** less placement control, which conflicts with the request for a discreet fixed location.
- **Sticky or overlay banner:** rejected because it obscures content and interrupts navigation.

## Design

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.

## Acceptance

- The ad slot is absent when either configuration value is missing.
- 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.
49 changes: 49 additions & 0 deletions src/app/components/AdSenseBanner/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use client';

import { useEffect, useRef } from 'react';

declare global {
interface Window {
adsbygoogle?: Record<string, unknown>[];
}
}

const publisherId = process.env.NEXT_PUBLIC_ADSENSE_CLIENT_ID;
const slotId = process.env.NEXT_PUBLIC_ADSENSE_SLOT_ID;

export default function AdSenseBanner() {
const initialized = useRef(false);
const enabled = Boolean(publisherId && slotId);

useEffect(() => {
if (!enabled || initialized.current) return;

initialized.current = true;
(window.adsbygoogle ||= []).push({});
}, [enabled]);

if (!enabled) {
return null;
}

return (
<aside
aria-label="Publicidade"
className="mx-auto my-8 w-full max-w-7xl px-4 sm:px-6 lg:px-8"
>
<div className="mx-auto max-w-[760px] rounded-lg border border-gray-200 bg-gray-50 p-3">
<p className="mb-2 text-[10px] font-medium uppercase tracking-wide text-gray-500">
Anúncio
</p>
<ins
className="adsbygoogle block min-h-[90px] w-full"
style={{ display: 'block' }}
data-ad-client={publisherId}
data-ad-slot={slotId}
data-ad-format="auto"
data-full-width-responsive="true"
/>
</div>
</aside>
);
}
1 change: 1 addition & 0 deletions src/app/components/Footer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const Footer: React.FC = () => {
<li><Link href="/projects" className="text-gray-400 hover:text-white transition-colors">Projetos</Link></li>
<li><Link href="/blog" className="text-gray-400 hover:text-white transition-colors">Blog</Link></li>
<li><Link href="/#contato" className="text-gray-400 hover:text-white transition-colors">Contato</Link></li>
<li><Link href="/privacidade" className="text-gray-400 hover:text-white transition-colors">Privacidade</Link></li>
</ul>
</div>

Expand Down
12 changes: 12 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { Inter } from "next/font/google";
import "./globals.css";
import Footer from "./components/Footer";
import GoogleAnalytics from "./components/GoogleAnalytics";
import AdSenseBanner from "./components/AdSenseBanner";
import Script from "next/script";
import { serviceOfferings } from "@/data/services";

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

export const metadata: Metadata = {
title: {
Expand Down Expand Up @@ -77,6 +79,15 @@ export default function RootLayout({
<html lang="pt-BR">
<head>
<GoogleAnalytics />
{adsensePublisherId && (
<Script
id="google-adsense"
async
crossOrigin="anonymous"
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${adsensePublisherId}`}
strategy="beforeInteractive"
/>
)}
</head>
<body className={inter.className} suppressHydrationWarning>
<Script id="ld-organization" type="application/ld+json" strategy="afterInteractive"
Expand Down Expand Up @@ -124,6 +135,7 @@ export default function RootLayout({
}) }}
/>
{children}
<AdSenseBanner />
<Footer />
</body>
</html>
Expand Down
109 changes: 109 additions & 0 deletions src/app/privacidade/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "Privacidade e cookies",
description: "Informações sobre cookies e anúncios exibidos no site HelpDev.",
};

export default function PrivacyPage() {
return (
<main className="mx-auto w-full max-w-4xl px-4 py-16 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold text-gray-900 sm:text-4xl">
Privacidade e cookies
</h1>
<div className="mt-8 space-y-6 text-base leading-7 text-gray-700">
<p>
O site usa Google Analytics para medir visitas e uso das páginas. Na
implementação padrão, o Google Analytics utiliza o cookie próprio{" "}
<code className="rounded bg-gray-100 px-1">_ga</code> para distinguir
navegadores e sessões e coleta informações como estatísticas de
sessão, localização aproximada e dados do navegador e do dispositivo.
Consulte as informações do Google sobre{" "}
<a
className="text-blue-700 underline hover:text-blue-900"
href="https://support.google.com/analytics/answer/11593727?hl=pt-BR"
target="_blank"
rel="noopener noreferrer"
>
coleta de dados do Analytics
</a>
.
</p>
<p>
Este site exibe anúncios fornecidos pelo Google AdSense. O Google e
parceiros de publicidade podem usar cookies e identificadores para
veicular e personalizar anúncios, medir o desempenho das campanhas e
evitar que o mesmo anúncio seja exibido repetidamente. Esses recursos
podem considerar visitas anteriores a este site ou a outros sites.
</p>
<p>
Você pode gerenciar a personalização de anúncios nas{" "}
<a
className="text-blue-700 underline hover:text-blue-900"
href="https://adssettings.google.com/"
target="_blank"
rel="noopener noreferrer"
>
Configurações de anúncios do Google
</a>
. Também pode consultar as opções de desativação disponibilizadas pela{" "}
<a
className="text-blue-700 underline hover:text-blue-900"
href="https://www.aboutads.info/choices/"
target="_blank"
rel="noopener noreferrer"
>
Digital Advertising Alliance
</a>
.
</p>
<p>
Para desativar a medição do Google Analytics neste navegador, o Google
oferece o{" "}
<a
className="text-blue-700 underline hover:text-blue-900"
href="https://tools.google.com/dlpage/gaoptout?hl=pt-BR"
target="_blank"
rel="noopener noreferrer"
>
complemento de desativação do Analytics
</a>
.
</p>
<p>
O tratamento de dados feito pelo Google segue a{" "}
<a
className="text-blue-700 underline hover:text-blue-900"
href="https://policies.google.com/privacy?hl=pt-BR"
target="_blank"
rel="noopener noreferrer"
>
Política de Privacidade do Google
</a>
. Para saber mais sobre cookies usados pelo AdSense, consulte a página
oficial sobre{" "}
<a
className="text-blue-700 underline hover:text-blue-900"
href="https://support.google.com/adsense/answer/7549925?hl=pt-BR"
target="_blank"
rel="noopener noreferrer"
>
como o AdSense usa cookies
</a>
.
</p>
<p>
Para dúvidas ou solicitações relacionadas à privacidade neste site,
entre em contato pelo e-mail{" "}
<a
className="text-blue-700 underline hover:text-blue-900"
href="mailto:gbzarelli@helpdev.com.br"
>
gbzarelli@helpdev.com.br
</a>
.
</p>
</div>
</main>
);
}
1 change: 1 addition & 0 deletions src/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
'/articles',
'/projects',
'/blog',
'/privacidade',
];

return routes.map((route) => ({
Expand Down
Loading