diff --git a/app/components/Footer.test.tsx b/app/components/Footer.test.tsx index 632daeb..28bde0d 100644 --- a/app/components/Footer.test.tsx +++ b/app/components/Footer.test.tsx @@ -28,6 +28,10 @@ describe("Footer contact and navigation", () => { const servicesLink = screen.getByRole("link", { name: "Hub de serviços" }); expect(servicesLink).toHaveAttribute("href", "/servicos"); expect(servicesLink).not.toHaveAttribute("target", "_blank"); + expect(screen.getByRole("link", { name: "Privacidade" })).toHaveAttribute( + "href", + "/privacidade", + ); }); }); diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx index 55dd4a6..3c4b109 100644 --- a/app/components/Footer.tsx +++ b/app/components/Footer.tsx @@ -29,6 +29,7 @@ const socialLinks: SocialLink[] = [ const quickLinks = [ { label: "Hub de serviços", href: "/servicos" }, { label: "Cases", href: "/cases" }, + { label: "Privacidade", href: "/privacidade" }, { label: "Consultoria Next.js", href: "/servicos/consultoria-nextjs" }, { label: "Integração hardware + software", diff --git a/app/components/GoogleAnalytics.test.tsx b/app/components/GoogleAnalytics.test.tsx index 9567d1e..43deb40 100644 --- a/app/components/GoogleAnalytics.test.tsx +++ b/app/components/GoogleAnalytics.test.tsx @@ -61,6 +61,9 @@ describe("GoogleAnalytics consent", () => { expect( screen.queryByTestId("google-analytics-script"), ).not.toBeInTheDocument(); + expect( + screen.getByRole("link", { name: "Ler política de privacidade" }), + ).toHaveAttribute("href", "/privacidade"); }); it("persists acceptance and enables analytics without advertising consent", async () => { @@ -187,6 +190,9 @@ describe("GoogleAnalytics consent", () => { name: "Preferências de analytics", }); await user.click(preferencesButton); + expect( + screen.getByRole("link", { name: "Ler política de privacidade" }), + ).toHaveAttribute("href", "/privacidade"); await user.click( screen.getByRole("button", { name: "Fechar preferências" }), ); diff --git a/app/components/GoogleAnalytics.tsx b/app/components/GoogleAnalytics.tsx index 09ffd60..d424d6f 100644 --- a/app/components/GoogleAnalytics.tsx +++ b/app/components/GoogleAnalytics.tsx @@ -198,6 +198,12 @@ function GoogleAnalyticsConsent() { Recusar analytics + + Ler política de privacidade + ); } @@ -270,6 +276,12 @@ function GoogleAnalyticsConsent() { Fechar preferências + + Ler política de privacidade + )} diff --git a/app/privacidade/opengraph-image.tsx b/app/privacidade/opengraph-image.tsx new file mode 100644 index 0000000..50a7437 --- /dev/null +++ b/app/privacidade/opengraph-image.tsx @@ -0,0 +1,17 @@ +import { ImageResponse } from "next/og"; +import { OG_CONTENT_TYPE, OG_SIZE, OgCard } from "@/lib/og-card"; + +export const alt = "Privacidade e consentimento no Evandro.dev.br"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export default function Image() { + return new ImageResponse( + , + OG_SIZE, + ); +} diff --git a/app/privacidade/page.test.tsx b/app/privacidade/page.test.tsx new file mode 100644 index 0000000..14ecd63 --- /dev/null +++ b/app/privacidade/page.test.tsx @@ -0,0 +1,57 @@ +import { render, screen, within } from "@testing-library/react"; +import PrivacyPage, { metadata } from "./page"; + +it("publishes factual privacy content and a breadcrumb-only JSON-LD graph", () => { + const { container } = render(); + + expect( + screen.getByRole("heading", { level: 1, name: /privacidade/i }), + ).toBeVisible(); + expect(screen.getByText(/evandro\.analytics-consent\.v1/)).toBeVisible(); + expect(screen.getAllByText(/WhatsApp/).length).toBeGreaterThan(0); + expect(screen.getByText(/nenhuma tag do Google é carregada/i)).toBeVisible(); + expect(screen.getByText(/2026-09-04/)).toBeVisible(); + + const breadcrumb = screen.getByRole("navigation", { name: "Breadcrumb" }); + const breadcrumbLinks = within(breadcrumb).getAllByRole("link"); + expect(breadcrumbLinks.map((link) => link.textContent)).toEqual([ + "Início", + "Privacidade", + ]); + expect(breadcrumbLinks[0]).toHaveAttribute("href", "/"); + expect(breadcrumbLinks[1]).toHaveAttribute("href", "/privacidade"); + expect(breadcrumbLinks[1]).toHaveAttribute("aria-current", "page"); + + const scripts = container.querySelectorAll( + 'script[type="application/ld+json"]', + ); + expect(scripts).toHaveLength(1); + const jsonLd = JSON.parse(scripts[0]?.textContent ?? "{}"); + expect(jsonLd["@graph"]).toHaveLength(1); + expect(jsonLd["@graph"][0]["@type"]).toBe("BreadcrumbList"); + expect(JSON.stringify(jsonLd)).not.toMatch( + /Person|Organization|LocalBusiness|ProfessionalService|Service/, + ); + expect(jsonLd["@graph"][0].itemListElement).toEqual([ + { + "@type": "ListItem", + position: 1, + name: "Início", + item: "https://evandro.dev.br", + }, + { + "@type": "ListItem", + position: 2, + name: "Privacidade", + item: "https://evandro.dev.br/privacidade", + }, + ]); +}); + +it("uses privacy-specific canonical metadata", () => { + expect(metadata).toMatchObject({ + title: expect.stringMatching(/privacidade/i), + description: expect.any(String), + alternates: { canonical: "https://evandro.dev.br/privacidade" }, + }); +}); diff --git a/app/privacidade/page.tsx b/app/privacidade/page.tsx new file mode 100644 index 0000000..d2c838d --- /dev/null +++ b/app/privacidade/page.tsx @@ -0,0 +1,151 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { safeJsonLd } from "@/lib/jsonld"; +import { buildBreadcrumbList } from "@/lib/seo"; +import { siteName, siteUrl } from "@/lib/site-content"; + +const pageUrl = `${siteUrl}/privacidade`; + +export const metadata: Metadata = { + title: "Privacidade e consentimento", + description: + "Como o portfólio trata dados de contato, consentimento de analytics, links externos e solicitações de privacidade.", + alternates: { canonical: pageUrl }, + openGraph: { + title: `Privacidade e consentimento | ${siteName}`, + description: + "Informações factuais sobre contato, analytics e preferências de privacidade no portfólio.", + url: pageUrl, + siteName, + type: "website", + }, +}; + +export default function PrivacyPage() { + const jsonLd = { + "@context": "https://schema.org", + "@graph": [ + buildBreadcrumbList(pageUrl, [ + ["Início", siteUrl], + ["Privacidade", pageUrl], + ] as const), + ], + }; + + return ( +
+ + +
+
+ +

Privacidade e consentimento

+

+ Privacidade e consentimento +

+

+ Esta página explica, de forma objetiva, como funcionam o contato e + as escolhas de analytics neste portfólio. +

+
+
+ +
+
+
+
+

Responsável e contato

+

Evandro Fonseca Junior

+

+ O responsável pelo site é Evandro Fonseca Junior. Para contato + ou solicitações relacionadas a esta página, escreva para{" "} + + evandrofonsecajunior@gmail.com + + . +

+
+ +
+

Formulário de contato

+

Mensagem pelo WhatsApp

+

+ Os dados digitados no formulário são transformados em uma + mensagem e abrem o WhatsApp. O site não possui endpoint próprio + de armazenamento desse formulário. +

+
+ +
+

Analytics

+

Escolha antes da medição

+

+ Antes de granted, nenhuma tag do Google é + carregada. A escolha fica no localStorage sob a + chave evandro.analytics-consent.v1, com os valores{" "} + granted ou denied. +

+

+ A publicidade permanece negada. Ao conceder analytics, o Google + pode criar cookies _ga*. Ao revogar, novas coletas + são desativadas e os cookies de analytics acessíveis são + expirados. +

+
+ +
+

Preferências e links

+

Você pode mudar a escolha

+

+ Para alterar a preferência, use o controle existente{" "} + “Preferências de analytics” para acessar suas + preferências de privacidade. Links externos para GitHub, + LinkedIn, Instagram, demos e WhatsApp seguem as políticas + próprias de cada serviço. +

+

+ Solicitações relacionadas aos seus direitos devem ser + encaminhadas ao e-mail publicado acima. +

+ + Voltar ao início + +
+
+ +
+

Vigência

+

+ Esta versão está vigente desde{" "} + . +

+
+
+
+
+ ); +} diff --git a/app/sitemap.ts b/app/sitemap.ts index eba467f..f9547d4 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -5,6 +5,7 @@ export default function sitemap(): MetadataRoute.Sitemap { return [ { url: siteUrl }, { url: `${siteUrl}/curriculo` }, + { url: `${siteUrl}/privacidade` }, { url: `${siteUrl}/servicos` }, { url: `${siteUrl}/cases` }, ...servicePages.map(({ slug }) => ({ diff --git a/lib/seo.test.ts b/lib/seo.test.ts index 6fb6818..b808f18 100644 --- a/lib/seo.test.ts +++ b/lib/seo.test.ts @@ -107,6 +107,7 @@ it("keeps profile data timeless and sitemap output deterministic", () => { const expectedUrls = [ siteUrl, `${siteUrl}/curriculo`, + `${siteUrl}/privacidade`, `${siteUrl}/servicos`, `${siteUrl}/cases`, ...servicePages.map(({ slug }) => `${siteUrl}/servicos/${slug}`), @@ -117,5 +118,8 @@ it("keeps profile data timeless and sitemap output deterministic", () => { expect(first).toEqual(expectedUrls.map((url) => ({ url }))); expect(second).toEqual(first); - expect(first).toHaveLength(16); + expect(first).toHaveLength(17); + expect( + first.filter(({ url }) => url === `${siteUrl}/privacidade`), + ).toHaveLength(1); });