Files
portifolio-evandrodevbr/lib/seo.test.ts
T
Evandro Fonseca Junior 6e1022b58c feat(cases): cases de clientes, serviços ligados à prova e SEO por página
Cases da SEV Moda Íntima e da Lavação 047; case no formato cliente, problema, entrega, mídia e contato, com detalhes técnicos recolhidos; sem GitHub, ressalvas e fontes de commit. /cases separa clientes e Laboratório. Serviços mostram o case de cliente certo e o bloco de investimento e prazo só aparece preenchido. Metadata social própria por página, not-found com noindex, sitemap com /ia.
2026-10-14 03:31:57 -03:00

180 lines
5.2 KiB
TypeScript

import sitemap from "../app/sitemap";
import {
buildBreadcrumbList,
buildPageSocialMetadata,
buildPersonJsonLd,
buildProfilePageJsonLd,
buildServiceJsonLd,
buildWebSiteJsonLd,
SEO_IDS,
twitterHandle,
} from "./seo";
import { caseStudies, servicePages, siteName, siteUrl } from "./site-content";
const representativeService = servicePages[0];
if (!representativeService) throw new Error("Missing representative service");
it("uses one canonical identity across website, profile and service", () => {
expect(SEO_IDS).toEqual({
website: `${siteUrl}/#website`,
person: `${siteUrl}/#person`,
profilePage: `${siteUrl}/curriculo#profile-page`,
});
expect(buildWebSiteJsonLd().publisher).toEqual({ "@id": SEO_IDS.person });
expect(buildProfilePageJsonLd()).toMatchObject({
"@id": SEO_IDS.profilePage,
isPartOf: { "@id": SEO_IDS.website },
mainEntity: { "@id": SEO_IDS.person },
});
expect(buildServiceJsonLd(representativeService).provider).toEqual({
"@id": SEO_IDS.person,
});
});
it("keeps the root graph limited to verified website and person fields", () => {
const website = buildWebSiteJsonLd();
const person = buildPersonJsonLd();
expect(Object.keys(website).sort()).toEqual(
["@id", "@type", "inLanguage", "name", "publisher", "url"].sort(),
);
expect(Object.keys(person).sort()).toEqual(
[
"@id",
"@type",
"homeLocation",
"image",
"jobTitle",
"knowsAbout",
"name",
"sameAs",
"url",
].sort(),
);
expect(website).toMatchObject({
"@type": "WebSite",
"@id": SEO_IDS.website,
url: siteUrl,
name: siteName,
});
expect(person).toMatchObject({
"@type": "Person",
"@id": SEO_IDS.person,
image: `${siteUrl}/edev.jpg`,
jobTitle: "Engenheiro da Computação e Desenvolvedor de Software",
homeLocation: { "@type": "Place", name: "Garuva, Santa Catarina, Brasil" },
sameAs: ["https://www.linkedin.com/in/evandrodevbr/"],
});
expect(JSON.stringify([website, person])).not.toMatch(
/ProfessionalService|LocalBusiness|GeoCoordinates|PostalAddress|Instagram|telephone|address|geo|priceRange|OfferCatalog|hasOfferCatalog|offers/i,
);
});
it("builds a canonical service with only the verified service area", () => {
const service = representativeService;
const pageUrl = `${siteUrl}/servicos/${service.slug}`;
expect(buildServiceJsonLd(service)).toEqual({
"@type": "Service",
"@id": `${pageUrl}#service`,
url: pageUrl,
name: service.navLabel,
description: service.metaDescription,
provider: { "@id": SEO_IDS.person },
areaServed: [
{ "@type": "City", name: "Garuva" },
{ "@type": "AdministrativeArea", name: "Norte de Santa Catarina" },
],
});
});
it("builds contiguous breadcrumbs from readonly tuples", () => {
const pageUrl = `${siteUrl}/servicos/x`;
const items = [
["Início", siteUrl],
["Serviços", `${siteUrl}/servicos`],
["X", pageUrl],
] as const;
const breadcrumb = buildBreadcrumbList(pageUrl, items);
expect(breadcrumb["@id"]).toBe(`${pageUrl}#breadcrumb`);
expect(breadcrumb.itemListElement).toEqual([
{ "@type": "ListItem", position: 1, name: "Início", item: siteUrl },
{
"@type": "ListItem",
position: 2,
name: "Serviços",
item: `${siteUrl}/servicos`,
},
{ "@type": "ListItem", position: 3, name: "X", item: pageUrl },
]);
});
it("keeps profile data timeless and sitemap output deterministic", () => {
expect(buildProfilePageJsonLd()).not.toHaveProperty("dateModified");
const expectedUrls = [
siteUrl,
`${siteUrl}/curriculo`,
`${siteUrl}/privacidade`,
`${siteUrl}/servicos`,
`${siteUrl}/cases`,
`${siteUrl}/ia`,
...servicePages.map(({ slug }) => `${siteUrl}/servicos/${slug}`),
...caseStudies.map(({ id }) => `${siteUrl}/cases/${id}`),
];
const first = sitemap();
const second = sitemap();
expect(first.map(({ url }) => url)).toEqual(expectedUrls);
expect(second).toEqual(first);
expect(first).toHaveLength(20);
expect(
first.filter(({ url }) => url === `${siteUrl}/privacidade`),
).toHaveLength(1);
});
it("lists the client cases and the AI page, each with a fixed modification date", () => {
const entries = sitemap();
const urls = entries.map(({ url }) => url);
expect(urls).toContain(`${siteUrl}/cases/sev-modaintima`);
expect(urls).toContain(`${siteUrl}/cases/lavacao-047`);
expect(urls).toContain(`${siteUrl}/ia`);
for (const entry of entries) {
expect(entry.lastModified).toBeInstanceOf(Date);
expect(Number.isNaN(new Date(entry.lastModified ?? "").getTime())).toBe(
false,
);
}
});
it("gives every page a complete social card, so it never inherits the home one", () => {
const url = `${siteUrl}/cases`;
expect(
buildPageSocialMetadata({
url,
title: "Título",
description: "Descrição",
type: "website",
}),
).toEqual({
alternates: { canonical: url },
openGraph: {
locale: "pt_BR",
title: "Título",
description: "Descrição",
url,
siteName,
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Título",
description: "Descrição",
creator: twitterHandle,
},
});
});