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:.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 14:15:25 -03:00
1 parent c9a0b80656
commit e80f73d075
52 files changed
+2593 -439

No files matched your search

+66 -1
View File
@@ -1,15 +1,23 @@
import sitemap from "../app/sitemap";
import {
buildBreadcrumbList,
buildCaseJsonLd,
buildPageSocialMetadata,
buildPersonJsonLd,
buildProfilePageJsonLd,
buildServiceJsonLd,
buildWebSiteJsonLd,
getCaseImageUrl,
SEO_IDS,
twitterHandle,
} from "./seo";
import { caseStudies, servicePages, siteName, siteUrl } from "./site-content";
import {
caseStudies,
isClientCase,
servicePages,
siteName,
siteUrl,
} from "./site-content";
const representativeService = servicePages[0];
if (!representativeService) throw new Error("Missing representative service");
@@ -89,6 +97,63 @@ it("builds a canonical service with only the verified service area", () => {
});
});
function getCase(id: string) {
const caseStudy = caseStudies.find((item) => item.id === id);
if (!caseStudy) throw new Error(`Missing case ${id}`);
return caseStudy;
}
it("describes a client case as a CreativeWork made by the person for the client", () => {
const caseStudy = getCase("sev-modaintima");
const pageUrl = `${siteUrl}/cases/${caseStudy.id}`;
expect(buildCaseJsonLd(caseStudy)).toEqual({
"@type": "CreativeWork",
"@id": `${pageUrl}#creative-work`,
name: "SEV Moda Íntima: Catálogo digital e loja virtual",
description: caseStudy.metaDescription,
url: pageUrl,
image: `${pageUrl}/opengraph-image/${caseStudy.id}`,
inLanguage: "pt-BR",
creator: { "@id": SEO_IDS.person },
sourceOrganization: {
"@type": "Organization",
name: "SEV Moda Íntima",
url: "https://sevmodaintima.com.br",
},
keywords:
"Next.js 16, TypeScript, PostgreSQL, Prisma, Tailwind CSS 4, Framer Motion",
});
});
it("builds a CreativeWork for every case, with the client only on client work", () => {
for (const caseStudy of caseStudies) {
const work = buildCaseJsonLd(caseStudy);
expect(work).toMatchObject({
"@type": "CreativeWork",
url: `${siteUrl}/cases/${caseStudy.id}`,
image: getCaseImageUrl(caseStudy.id),
creator: { "@id": SEO_IDS.person },
});
expect(work.name).toContain(caseStudy.title);
expect(work.description.length).toBeGreaterThan(40);
expect(work.keywords).toBe(caseStudy.stack.join(", "));
expect("sourceOrganization" in work).toBe(isClientCase(caseStudy));
}
});
it("omits the client address when a client case has no public site", () => {
const work = buildCaseJsonLd(getCase("as-servicos")) as {
url: string;
sourceOrganization?: Record<string, unknown>;
};
expect(work.sourceOrganization).toStrictEqual({
"@type": "Organization",
name: "AS Serviços",
});
expect(work.url).toBe(`${siteUrl}/cases/as-servicos`);
});
it("builds contiguous breadcrumbs from readonly tuples", () => {
const pageUrl = `${siteUrl}/servicos/x`;
const items = [