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:
1 parent
e80f73d075
commit
10f170abf4
56 files changed
+3099
-906
No files matched your search
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user