feat(home): posicionamento com prova, menu sem recarregar e WhatsApp em todo o site

H1 'Sites e sistemas que trazem pedidos pelo WhatsApp e tiram sua operação da planilha.'; HeroWork maior com a SEV; menu Cases, Serviços, Como trabalho, Sobre e IA com next/link e pré-carregamento no hover; menu do celular em tela cheia; WhatsApp flutuante e no rodapé com a mensagem da rota; rodapé em todas as páginas; FAQ da home; /ia como oferta de revisão de app feito com IA; formulário com noscript, validação do mailto e plano B do pop-up; cartões sem foco indevido; sem a fonte Monument.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 14:15:25 -03:00
1 parent e80f73d075
commit 10f170abf4
56 files changed
+3099 -906

No files matched your search

+3 -3
View File
@@ -1,7 +1,7 @@
import { ImageResponse } from "next/og";
import { OG_CONTENT_TYPE, OG_SIZE, OgCard } from "@/lib/og-card";
export const alt = "Começou um sistema com IA? Revisão e continuidade";
export const alt = "Revisão de app feito com IA";
export const size = OG_SIZE;
export const contentType = OG_CONTENT_TYPE;
@@ -9,8 +9,8 @@ export default function Image() {
return new ImageResponse(
<OgCard
eyebrow="IA no desenvolvimento"
title="Começou um sistema com IA? Dá para partir daí."
description="Revisão do que foi gerado, correção dos riscos e continuidade até a produção."
title="Revisão de app feito com IA"
description="Falhas de acesso a dados, o que aproveitar, o que refazer e um orçamento para pôr em produção."
/>,
OG_SIZE,
);
+61 -15
View File
@@ -1,22 +1,26 @@
import { render, screen, within } from "@testing-library/react";
import AiPage, { metadata } from "./page";
it("uses its own title without the site suffix being added twice", () => {
expect(metadata.title).toEqual({
absolute:
"Começou um sistema com IA? Revisão e continuidade | Evandro Fonseca Junior",
});
it("uses a short search title: with the layout suffix it fits in 44 characters", () => {
// o template de app/layout.tsx acrescenta " | Evandro.dev.br"
const suffix = " | Evandro.dev.br";
expect(metadata.title).toBe("Revisão de app feito com IA");
expect(`${metadata.title}${suffix}`.length).toBeLessThanOrEqual(44);
expect(metadata).toMatchObject({
description: expect.any(String),
alternates: { canonical: "https://evandro.dev.br/ia" },
});
// as prévias de compartilhamento não passam pelo template: o título delas já leva o sufixo
expect(metadata.openGraph?.title).toBe(
`Revisão de app feito com IA${suffix}`,
);
});
it("keeps the full AI content, with the cited incidents summarized only here", () => {
render(<AiPage />);
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
"Começou um sistema com IA? Dá para partir daí.",
"Revisão de app feito com IA (Lovable, Bolt, Replit)",
);
for (const heading of [
"Se a IA cria um sistema, por que contratar um desenvolvedor?",
@@ -25,7 +29,7 @@ it("keeps the full AI content, with the cited incidents summarized only here", (
]) {
expect(screen.getByRole("heading", { name: heading })).toBeInTheDocument();
}
for (const incident of [/Lovable/, /Replit/, /Base44/]) {
for (const incident of [/Lovable:/, /Replit:/, /Base44:/]) {
expect(screen.getByRole("heading", { name: incident })).toBeInTheDocument();
}
expect(
@@ -40,16 +44,16 @@ it("offers WhatsApp conversations about reviewing an AI-built system", () => {
.getAllByRole("link")
.filter((link) => link.getAttribute("href")?.startsWith("https://wa.me/"));
expect(links.map((link) => link.textContent)).toEqual([
"Falar sobre meu sistema",
"Pedir a revisão do meu app",
"Avaliar meu projeto",
]);
for (const link of links) {
const text = new URL(link.getAttribute("href") ?? "").searchParams.get(
"text",
);
// a mensagem tem de formar uma frase: "Quero conversar sobre a revisão de um sistema feito com IA."
// a mensagem tem de formar uma frase: "Quero conversar sobre a revisão de um aplicativo feito com IA."
expect(text).toContain(
"Quero conversar sobre a revisão de um sistema feito com IA.",
"Quero conversar sobre a revisão de um aplicativo feito com IA.",
);
}
});
@@ -57,12 +61,54 @@ it("offers WhatsApp conversations about reviewing an AI-built system", () => {
it("has breadcrumb navigation back to the home", () => {
render(<AiPage />);
const breadcrumb = screen.getByRole("navigation", { name: "Breadcrumb" });
const breadcrumb = screen.getByRole("navigation", {
name: "Trilha de navegação",
});
expect(
within(breadcrumb).getByRole("link", { name: "Início" }),
).toHaveAttribute("href", "/");
expect(within(breadcrumb).getByText("IA no desenvolvimento")).toHaveAttribute(
"aria-current",
"page",
);
expect(
within(breadcrumb).getByText("Revisão de app feito com IA"),
).toHaveAttribute("aria-current", "page");
});
it("presents the review as an offer: what the person receives, with no price or deadline", () => {
const { container } = render(<AiPage />);
const offer = screen.getByRole("heading", {
name: "O que você recebe na revisão",
});
const section = offer.closest("section") as HTMLElement;
const items = within(section)
.getAllByRole("listitem")
.map((item) => item.querySelector("h3")?.textContent);
expect(items).toEqual([
"As falhas de acesso a dados",
"O que dá para aproveitar",
"O que é melhor refazer",
"Um orçamento para pôr em produção",
]);
expect(container.textContent).not.toMatch(/R\$|\d+ dias|em até \d+/);
});
it("keeps the incident news collapsed in a <details>, closed by default", () => {
const { container } = render(<AiPage />);
const heading = screen.getByRole("heading", {
name: "O que casos documentados ensinam",
});
const details = heading.closest("details") as HTMLDetailsElement;
expect(details).not.toBeNull();
expect(details.open).toBe(false);
expect(heading.closest("summary")).not.toBeNull();
for (const incident of [/Lovable:/, /Replit:/, /Base44:/]) {
expect(within(details).getByText(incident)).toBeInTheDocument();
}
// só os incidentes ficam recolhidos: a oferta e a LGPD ficam à vista
expect(container.querySelectorAll("details")).toHaveLength(1);
expect(
screen
.getByRole("heading", { name: "O que você recebe na revisão" })
.closest("details"),
).toBeNull();
});
+115 -46
View File
@@ -10,26 +10,51 @@ import WhatsAppLink from "@/app/components/WhatsAppLink";
import { safeJsonLd } from "@/lib/jsonld";
import { buildBreadcrumbList, buildPageSocialMetadata } from "@/lib/seo";
import { siteUrl } from "@/lib/site-content";
import { AI_REVIEW_SUBJECT } from "@/lib/whatsapp";
const pageUrl = `${siteUrl}/ia`;
const pageTitle =
"Começou um sistema com IA? Revisão e continuidade | Evandro Fonseca Junior";
/** Curto de propósito: o template de app/layout.tsx acrescenta " | Evandro.dev.br" e o conjunto cabe em 44 caracteres. */
const pageTitle = "Revisão de app feito com IA";
const pageDescription =
"Protótipo ou sistema criado com IA? Avalio o que pode ser aproveitado, corrijo permissões, dados e publicação e levo o projeto até a produção.";
/** Entra na frase "Quero conversar sobre {assunto}." da mensagem do WhatsApp. */
const WHATSAPP_SERVICE = "a revisão de um sistema feito com IA";
"Fez um app com Lovable, Bolt ou Replit? Eu reviso as falhas de acesso a dados, separo o que aproveitar do que refazer e faço o orçamento para a produção.";
/** Entra na frase "Quero conversar sobre {assunto}." da mensagem do WhatsApp (a mesma do botão flutuante em /ia). */
const WHATSAPP_SERVICE = AI_REVIEW_SUBJECT;
export const metadata: Metadata = {
title: { absolute: pageTitle },
title: pageTitle,
description: pageDescription,
...buildPageSocialMetadata({
url: pageUrl,
title: pageTitle,
// as prévias de compartilhamento não passam pelo template do título
title: `${pageTitle} | Evandro.dev.br`,
description: pageDescription,
type: "website",
}),
};
/** O que a pessoa recebe ao contratar a revisão. Sem prazo e sem preço: isso se combina na conversa. */
const DELIVERABLES = [
{
title: "As falhas de acesso a dados",
description:
"Testo se uma pessoa consegue ver ou alterar os dados de outra e mostro o que precisa de correção.",
},
{
title: "O que dá para aproveitar",
description: "O que já está pronto e pode seguir no projeto.",
},
{
title: "O que é melhor refazer",
description:
"As partes que não se sustentam em uso real, com o motivo de cada uma.",
},
{
title: "Um orçamento para pôr em produção",
description:
"O que falta para o app atender clientes de verdade, com o orçamento por escrito.",
},
];
const RESPONSIBILITIES = [
{
title: "Quem pode acessar cada dado?",
@@ -98,7 +123,7 @@ export default function AiPage() {
"@graph": [
buildBreadcrumbList(pageUrl, [
["Início", siteUrl],
["IA no desenvolvimento", pageUrl],
["Revisão de app feito com IA", pageUrl],
] as const),
],
};
@@ -112,12 +137,12 @@ export default function AiPage() {
<Breadcrumb
items={[
{ label: "Início", href: "/" },
{ label: "IA no desenvolvimento", href: "/ia" },
{ label: "Revisão de app feito com IA", href: "/ia" },
]}
/>
<p className="ds-eyebrow">IA no desenvolvimento</p>
<h1 className="ds-section-title max-w-[22ch] text-balance">
Começou um sistema com IA? Dá para partir daí.
Revisão de app feito com IA (Lovable, Bolt, Replit)
</h1>
<p className="ds-copy max-w-[62ch]">
A IA permite testar ideias e construir protótipos mesmo sem
@@ -131,7 +156,7 @@ export default function AiPage() {
service={WHATSAPP_SERVICE}
placement="ia_hero"
>
Falar sobre meu sistema
Pedir a revisão do meu app
</WhatsAppLink>
<Link href="/cases" className={textLinkDark}>
Ver projetos entregues
@@ -140,6 +165,34 @@ export default function AiPage() {
</div>
</section>
<section className="ds-section" aria-labelledby="ia-receber-title">
<div className="ds-section-shell">
<h2 id="ia-receber-title" className="ds-section-title max-w-[26ch]">
O que você recebe na revisão
</h2>
<ol className="mt-sp-8 grid gap-sp-6 sm:grid-cols-2 lg:grid-cols-4">
{DELIVERABLES.map((item, index) => (
<li
key={item.title}
className="border-t border-[var(--border)] pt-sp-5"
>
<span className="ds-kicker">
{String(index + 1).padStart(2, "0")}
</span>
<h3 className="mt-sp-3 text-lg font-semibold leading-snug">
{item.title}
</h3>
<p className="ds-copy">{item.description}</p>
</li>
))}
</ol>
<p className="ds-copy mt-sp-8 max-w-[65ch]">
Começa por uma conversa pelo WhatsApp, para eu entender o app e o
que ele precisa fazer. Não envie senhas nem acessos por mensagem.
</p>
</div>
</section>
<section className="ds-section" aria-labelledby="ia-contratar-title">
<div className="ds-section-shell">
<h2 id="ia-contratar-title" className="ds-section-title max-w-[26ch]">
@@ -171,40 +224,56 @@ export default function AiPage() {
<section className="ds-section" aria-labelledby="ia-casos-title">
<div className="ds-section-shell">
<h2 id="ia-casos-title" className="ds-section-title max-w-[26ch]">
O que casos documentados ensinam
</h2>
<p className="ds-copy max-w-[75ch]">
São episódios de 2025, não avaliações atuais das plataformas. Eles
mostram riscos de configuração, permissões e operação; não provam
que todo sistema feito com IA seja inseguro ou que um profissional
elimine qualquer risco.
</p>
<div className="mt-sp-6 grid gap-sp-6 lg:grid-cols-3">
{INCIDENTS.map((incident) => (
<article key={incident.title} className="ds-card">
<h3 className="ds-card-title !mt-0">{incident.title}</h3>
<p className="ds-copy">{incident.summary}</p>
<p className="ds-copy">
<strong>O cuidado técnico:</strong> {incident.care}
</p>
<ul className="mt-sp-4 flex flex-col gap-sp-1">
{incident.links.map((link) => (
<li key={link.href}>
<a
className="inline-flex min-h-11 items-center text-sm underline underline-offset-4"
href={link.href}
target="_blank"
rel="noopener noreferrer"
>
{link.label}
</a>
</li>
))}
</ul>
</article>
))}
</div>
{/* Recolhido: as notícias sustentam a oferta, mas não são o que a pessoa veio ver. */}
<details className="group border-y border-[var(--border)]">
<summary className="flex min-h-14 cursor-pointer list-none items-center justify-between gap-sp-4 py-sp-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ink)] [&::-webkit-details-marker]:hidden">
<h2
id="ia-casos-title"
className="font-display text-[clamp(20px,3vw,28px)] font-semibold leading-tight tracking-[-0.02em]"
>
O que casos documentados ensinam
</h2>
<span
aria-hidden="true"
className="shrink-0 font-mono text-2xl leading-none text-[var(--meta)] transition-transform group-open:rotate-45 motion-reduce:transition-none"
>
+
</span>
</summary>
<div className="pb-sp-8">
<p className="ds-copy max-w-[75ch]">
São episódios de 2025, não avaliações atuais das plataformas.
Eles mostram riscos de configuração, permissões e operação; não
provam que todo sistema feito com IA seja inseguro ou que um
profissional elimine qualquer risco.
</p>
<div className="mt-sp-6 grid gap-sp-6 lg:grid-cols-3">
{INCIDENTS.map((incident) => (
<article key={incident.title} className="ds-card">
<h3 className="ds-card-title !mt-0">{incident.title}</h3>
<p className="ds-copy">{incident.summary}</p>
<p className="ds-copy">
<strong>O cuidado técnico:</strong> {incident.care}
</p>
<ul className="mt-sp-4 flex flex-col gap-sp-1">
{incident.links.map((link) => (
<li key={link.href}>
<a
className="inline-flex min-h-11 items-center text-[15px] md:text-sm underline underline-offset-4"
href={link.href}
target="_blank"
rel="noopener noreferrer"
>
{link.label}
</a>
</li>
))}
</ul>
</article>
))}
</div>
</div>
</details>
</div>
</section>
@@ -229,7 +298,7 @@ export default function AiPage() {
adequação.
</p>
<a
className="mt-sp-4 inline-flex min-h-11 items-center text-sm underline underline-offset-4"
className="mt-sp-4 inline-flex min-h-11 items-center text-[15px] md:text-sm underline underline-offset-4"
href="https://www.gov.br/anpd/pt-br/acesso-a-informacao/institucional/atos-normativos/regulamentacoes_anpd/resolucao-cd-anpd-no-2-de-27-de-janeiro-de-2022"
target="_blank"
rel="noopener noreferrer"