feat(servicos): ofertas no topo de /servicos, capa por assunto e diagramas

/servicos abre com as 3 ofertas e depois outros serviços; H1 de criação de sites e de engenharia com o nome da oferta; capa de cada serviço pelo assunto (SEV fora do próprio case cai de ~11 para 2 imagens); diagrama de fluxo em SVG nos serviços sem case; cases com o que o cliente passou a poder fazer e contato logo depois de Como ficou; FAQ num componente só; lastmod por rota; ProfessionalService com areaServed.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 19:14:46 -03:00
1 parent e63f3c2ebd
commit fc44a69fb3
41 files changed
+2486 -550

No files matched your search

+48
View File
@@ -4,6 +4,7 @@ import {
buildCaseJsonLd,
buildPageSocialMetadata,
buildPersonJsonLd,
buildProfessionalServiceJsonLd,
buildProfilePageJsonLd,
buildServiceJsonLd,
buildWebSiteJsonLd,
@@ -14,6 +15,7 @@ import {
import {
caseStudies,
isClientCase,
serviceOffers,
servicePages,
siteName,
siteUrl,
@@ -27,6 +29,7 @@ it("uses one canonical identity across website, profile and service", () => {
website: `${siteUrl}/#website`,
person: `${siteUrl}/#person`,
profilePage: `${siteUrl}/curriculo#profile-page`,
professionalService: `${siteUrl}/#professional-service`,
});
expect(buildWebSiteJsonLd().publisher).toEqual({ "@id": SEO_IDS.person });
expect(buildProfilePageJsonLd()).toMatchObject({
@@ -98,6 +101,47 @@ it("builds a canonical service with only the verified service area", () => {
});
});
it("describes the business as a ProfessionalService with the service area and the three offers as a catalog", () => {
const business = buildProfessionalServiceJsonLd();
expect(business).toMatchObject({
"@type": "ProfessionalService",
"@id": SEO_IDS.professionalService,
name: siteName,
url: siteUrl,
founder: { "@id": SEO_IDS.person },
});
expect(business.areaServed.map(({ name }) => name)).toEqual([
"Garuva",
"Joinville",
"Santa Catarina",
"Brasil",
]);
const catalog = business.hasOfferCatalog;
expect(catalog["@type"]).toBe("OfferCatalog");
// as mesmas três ofertas da home, com o mesmo nome e a página de cada uma
expect(
catalog.itemListElement.map(({ itemOffered }) => itemOffered.name),
).toEqual([
"Site que traz pedidos pelo WhatsApp",
"Sistema para tirar a operação da planilha",
"Revisão de sistema que já existe, inclusive feito com IA",
]);
expect(
catalog.itemListElement.map(({ itemOffered }) => itemOffered.url),
).toEqual(serviceOffers.map(({ href }) => `${siteUrl}${href}`));
for (const offer of catalog.itemListElement) {
expect(offer["@type"]).toBe("Offer");
expect(offer.itemOffered["@type"]).toBe("Service");
expect(offer.itemOffered.description.length).toBeGreaterThan(30);
}
// nada que o site não publique: sem preço, prazo, endereço de rua, telefone ou e-mail
expect(JSON.stringify(business)).not.toMatch(
/price|priceCurrency|availability|telephone|email|streetAddress|PostalAddress|geo|aggregateRating|review/i,
);
});
function getCase(id: string) {
const caseStudy = caseStudies.find((item) => item.id === id);
if (!caseStudy) throw new Error(`Missing case ${id}`);
@@ -214,6 +258,10 @@ it("lists the client cases and the AI page, each with a fixed modification date"
false,
);
}
// a data é a do conteúdo de cada rota, não uma data única para o mapa inteiro
expect(
new Set(entries.map(({ lastModified }) => String(lastModified))).size,
).toBeGreaterThan(1);
});
it("gives every page a complete social card, so it never inherits the home one", () => {