Files
portifolio-evandrodevbr/app/cases/page.tsx
T
Evandro Fonseca Junior e80f73d075 feat(cases): mídia no topo, títulos de busca curtos e CreativeWork
Case com mídia ao lado do título e play acessível; problema de cada cliente reescrito com base nos dados; serviços relacionados em 2 cards; FAQ antes do próximo passo e bloco 'Onde já apliquei isso'; Apolo citada em IoT e arquitetura. metaTitle até 43 caracteres e descrições até 155 com teste; JSON-LD CreativeWork; imagens de compartilhamento abaixo de 100 KB; 404 e robots corrigidos; server-only nos módulos com node:.
2026-10-14 14:15:25 -03:00

205 lines
6.0 KiB
TypeScript

import type { Metadata } from "next";
import Link from "next/link";
import { Breadcrumb } from "@/app/components/Breadcrumb";
import {
pageTop,
primaryButton,
secondaryButtonDark,
secondaryButtonLight,
textLink,
} from "@/app/components/button-styles";
import WhatsAppLink from "@/app/components/WhatsAppLink";
import { safeJsonLd } from "@/lib/jsonld";
import { buildBreadcrumbList, buildPageSocialMetadata } from "@/lib/seo";
import {
type CaseStudyContent,
clientCases,
labCases,
siteUrl,
} from "@/lib/site-content";
import { CaseCoverLink } from "./_components/CaseCoverLink";
const pageUrl = `${siteUrl}/cases`;
const pageTitle = "Cases: sites e sistemas para empresas";
const pageDescription =
"Veja o que já foi feito para empresas: loja virtual com pedido pelo WhatsApp, site com painel para o dono e páginas institucionais.";
const allCases = [...clientCases, ...labCases];
export const metadata: Metadata = {
title: pageTitle,
description: pageDescription,
...buildPageSocialMetadata({
url: pageUrl,
title: pageTitle,
description: pageDescription,
type: "website",
}),
};
function CaseCard({
caseStudy,
withCover,
ctaLabel,
priority = false,
}: {
caseStudy: CaseStudyContent;
withCover: boolean;
ctaLabel: string;
/** Capa que abre a página (a primeira, o maior elemento visível): carrega antes das demais. */
priority?: boolean;
}) {
return (
<article className="ds-card !h-auto">
{withCover ? (
<CaseCoverLink
id={caseStudy.id}
title={caseStudy.title}
demoUrl={caseStudy.demoUrl}
priority={priority}
prefetch={false}
/>
) : null}
<div className="ds-kicker">{caseStudy.title}</div>
<h3 className="ds-card-title">{caseStudy.headline}</h3>
<p className="ds-copy">{caseStudy.summary}</p>
<div className="mt-sp-6 flex flex-wrap items-center gap-sp-3">
<Link
href={`/cases/${caseStudy.id}`}
prefetch={false}
className={secondaryButtonLight}
>
{ctaLabel}
</Link>
{caseStudy.demoUrl ? (
<a
href={caseStudy.demoUrl}
className={textLink}
target="_blank"
rel="noreferrer"
>
Abrir o site
</a>
) : null}
</div>
</article>
);
}
export default function CasesPage() {
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "CollectionPage",
"@id": `${pageUrl}#page`,
name: pageTitle,
url: pageUrl,
description: pageDescription,
},
{
"@type": "ItemList",
itemListElement: allCases.map((caseStudy, index) => ({
"@type": "ListItem",
position: index + 1,
name: caseStudy.title,
url: `${pageUrl}/${caseStudy.id}`,
})),
},
buildBreadcrumbList(pageUrl, [
["Início", siteUrl],
["Cases", pageUrl],
] as const),
],
};
return (
<main>
<script type="application/ld+json">{safeJsonLd(jsonLd)}</script>
<section className={`ds-section ds-section-dark ${pageTop}`}>
<div className="ds-section-shell">
<div className="max-w-[860px]">
<Breadcrumb
items={[
{ label: "Início", href: "/" },
{ label: "Cases", href: "/cases" },
]}
/>
<p className="ds-eyebrow">Trabalhos entregues</p>
<h1 className="ds-section-title max-w-[18ch] text-balance text-white">
Veja o que já fiz para empresas.
</h1>
<p className="ds-copy max-w-[64ch] !text-[rgba(255,255,255,0.78)]">
{pageDescription}
</p>
</div>
</div>
</section>
<section className="ds-section" aria-labelledby="cases-clientes">
<div className="ds-section-shell">
<h2 id="cases-clientes" className="ds-card-title !mt-0">
Projetos de clientes
</h2>
<div className="ds-grid-2 mt-sp-8 items-start">
{clientCases.map((caseStudy, index) => (
<CaseCard
key={caseStudy.id}
caseStudy={caseStudy}
withCover
ctaLabel="Ver o case"
priority={index === 0}
/>
))}
</div>
</div>
</section>
<section className="ds-section" aria-labelledby="cases-laboratorio">
<div className="ds-section-shell">
<h2 id="cases-laboratorio" className="ds-card-title !mt-0">
Laboratório
</h2>
<p className="ds-copy max-w-[64ch]">
Projetos pessoais e de estudo, feitos para praticar e testar ideias.
</p>
<div className="ds-grid-2 mt-sp-8 items-start">
{labCases.map((caseStudy) => (
<CaseCard
key={caseStudy.id}
caseStudy={caseStudy}
withCover={false}
ctaLabel="Ver o projeto"
/>
))}
</div>
</div>
</section>
<section className="ds-section ds-section-dark">
<div className="ds-section-shell">
<h2 className="ds-card-title !mt-0 text-white">
Quer um projeto parecido com algum destes?
</h2>
<p className="ds-copy max-w-[60ch]">
Conte o que você precisa. A conversa começa direto no WhatsApp, sem
formulário.
</p>
<div className="mt-sp-6 flex flex-wrap items-center gap-sp-3">
<WhatsAppLink
service="um projeto parecido com os cases do portfólio"
placement="cases_hub_footer"
className={primaryButton}
>
Conversar sobre meu projeto
</WhatsAppLink>
<Link href="/servicos" className={secondaryButtonDark}>
Ver os serviços
</Link>
</div>
</div>
</section>
</main>
);
}