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

+173 -11
View File
@@ -2,12 +2,16 @@ import { readFileSync } from "node:fs";
import { join } from "node:path";
import portfolioData from "@/public/portfolio-data.json";
import {
buttonLabels,
caseStudies,
clientCases,
clientCasesForListing,
getRelatedCases,
getRelatedServices,
isClientCase,
labCases,
pageUpdatedAt,
serviceOffers,
servicePages,
} from "./site-content";
@@ -89,17 +93,25 @@ it("mentions the region and a benefit in the title and description of every serv
expect(new Set(descriptions).size).toBe(descriptions.length);
});
it("maps each service to its client cases", () => {
expect(getService("criacao-de-sites").relatedCaseIds).toEqual(clientIds);
it("maps each service to its client cases, with the SEV only where it is the proof: the systems service", () => {
expect(getService("criacao-de-sites").relatedCaseIds).toEqual([
"lavacao-047",
"sc-plus",
"as-servicos",
]);
expect(getService("engenharia-de-software").relatedCaseIds).toEqual([
"sev-modaintima",
"lavacao-047",
]);
expect(getService("consultoria-nextjs").relatedCaseIds).toEqual([
"sev-modaintima",
"lavacao-047",
"sc-plus",
"lavacao-047",
]);
expect(
servicePages
.filter(({ relatedCaseIds }) => relatedCaseIds.includes("sev-modaintima"))
.map(({ slug }) => slug),
).toEqual(["engenharia-de-software"]);
// nenhum case de cliente é um diagnóstico de sistema: o serviço fica só com a experiência na Apolo
expect(getService("arquitetura-de-software").relatedCaseIds).toEqual([]);
expect(getService("arquitetura-de-software").caseRelevance).toEqual({});
@@ -112,16 +124,163 @@ it("maps each service to its client cases", () => {
it("says what the Next.js service cases really prove: sites built by me, not a diagnosis", () => {
const { caseRelevance } = getService("consultoria-nextjs");
for (const id of ["sev-modaintima", "lavacao-047"]) {
expect(caseRelevance[id]).toMatch(/construí em Next\.js 16/);
expect(caseRelevance[id]).not.toMatch(/diagn|conheço por dentro/i);
expect(caseRelevance["lavacao-047"]).toMatch(/construí em Next\.js 16/);
expect(caseRelevance["sc-plus"]).toMatch(/Next\.js 15/);
for (const sentence of Object.values(caseRelevance)) {
expect(sentence).not.toMatch(/diagn|conheço por dentro/i);
}
});
it("is honest that the desktop case is a Windows app made in Python, not in Tauri", () => {
expect(getService("tauri-rust").caseRelevance["gcc-installer"]).toMatch(
/Windows.*Python/,
it("calls the desktop case a personal Windows app project, without claiming it is made in Tauri", () => {
const sentence = getService("tauri-rust").caseRelevance["gcc-installer"];
expect(sentence).toMatch(/projeto próprio de aplicativo para Windows/);
expect(sentence).not.toMatch(/Tauri|Python/);
});
it("opens the two offer services with the name of the offer as the h1, keeping the search title", () => {
expect(getService("criacao-de-sites").headline).toBe(serviceOffers[0].name);
expect(getService("criacao-de-sites").headline).toBe(
"Site que traz pedidos pelo WhatsApp",
);
expect(getService("engenharia-de-software").headline).toBe(
serviceOffers[1].name,
);
expect(getService("engenharia-de-software").headline).toBe(
"Sistema para tirar a operação da planilha",
);
expect(getService("criacao-de-sites").metaTitle).toBe(
"Criação de sites em Joinville e SC",
);
expect(getService("engenharia-de-software").metaTitle).toBe(
"Sistemas web e aplicativos sob medida",
);
});
it("picks the cover by subject: sites -> Lavação 047, Next.js -> SC Plus, systems -> SEV on the phone, nothing for the rest", () => {
expect(getService("criacao-de-sites").heroCase).toEqual({
id: "lavacao-047",
device: "desktop",
});
expect(getService("consultoria-nextjs").heroCase).toEqual({
id: "sc-plus",
device: "desktop",
});
expect(getService("engenharia-de-software").heroCase).toEqual({
id: "sev-modaintima",
device: "mobile",
});
for (const slug of [
"tauri-rust",
"integracao-hardware-software-iot",
"arquitetura-de-software",
]) {
expect(getService(slug).heroCase).toBeUndefined();
}
// a capa é sempre um dos cases relacionados, e nenhuma capa se repete entre os serviços
const heroIds = servicePages.flatMap(({ heroCase }) =>
heroCase ? [heroCase.id] : [],
);
expect(new Set(heroIds).size).toBe(heroIds.length);
for (const { heroCase, relatedCaseIds } of servicePages) {
if (heroCase) expect(relatedCaseIds).toContain(heroCase.id);
}
});
it("gives each service without a client cover a flow diagram instead, and never both", () => {
expect(getService("tauri-rust").diagram).toBe("desktop");
expect(getService("integracao-hardware-software-iot").diagram).toBe("iot");
expect(getService("arquitetura-de-software").diagram).toBe("arquitetura");
for (const { heroCase, diagram } of servicePages) {
expect(Boolean(heroCase) !== Boolean(diagram)).toBe(true);
}
});
it("lists the client cases for the mosaic and /cases with the SEV last, never first", () => {
expect(clientCasesForListing.map(({ id }) => id)).toEqual([
"lavacao-047",
"sc-plus",
"as-servicos",
"sev-modaintima",
]);
expect(clientCasesForListing).toHaveLength(clientCases.length);
});
it("keeps one list of button labels: 'Falar sobre meu projeto' for contact and 'Ver case' for a case", () => {
expect(buttonLabels.contact).toBe("Falar sobre meu projeto");
expect(buttonLabels.case).toBe("Ver case");
expect(Object.values(buttonLabels).join("|")).not.toMatch(
/Ver o case|Ler case|Conversar|oportunidade/,
);
});
it("reduces 'direto' and 'diretamente' in the case and service text", () => {
const text = JSON.stringify({ caseStudies, servicePages });
expect(text.match(/diret[oa]s?|diretamente/gi) ?? []).toEqual([]);
});
it("explains the three offers of the home with the same names, 3 or 4 deliverables and a place to see them", () => {
expect(serviceOffers.map(({ name }) => 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(serviceOffers.map(({ href }) => href)).toEqual([
"/servicos/criacao-de-sites",
"/servicos/engenharia-de-software",
"/ia",
]);
for (const offer of serviceOffers) {
expect(offer.deliverables.length).toBeGreaterThanOrEqual(3);
expect(offer.deliverables.length).toBeLessThanOrEqual(4);
expect(
offer.proof.caseIds.every((id) => caseStudies.some((c) => c.id === id)),
).toBe(true);
// sem número, preço, prazo ou depoimento inventados
const text = [
offer.name,
offer.summary,
...offer.deliverables,
offer.proof.note ?? "",
].join(" ");
expect(text).not.toMatch(/R\$|\d+\s?%|\d+\s?dias|["“”]/);
}
expect(serviceOffers[0].proof.caseIds).toEqual([
"lavacao-047",
"sev-modaintima",
]);
expect(serviceOffers[1].proof.caseIds).toEqual(["sev-modaintima"]);
expect(serviceOffers[1].proof.note).toMatch(/Apolo Energia/);
expect(serviceOffers[1].proof.note).toMatch(/uso interno/);
expect(serviceOffers[2].proof.caseIds).toEqual([]);
});
it("writes 2 or 3 capabilities for every client case, without numbers, and none for the laboratory", () => {
for (const caseStudy of clientCases) {
expect(caseStudy.capabilities?.length).toBeGreaterThanOrEqual(2);
expect(caseStudy.capabilities?.length).toBeLessThanOrEqual(3);
expect(JSON.stringify(caseStudy.capabilities)).not.toMatch(
/\d+\s?%|R\$|\d+\s?(clientes|pedidos|dias|vezes)/,
);
}
expect(
caseStudies.find(({ id }) => id === "lavacao-047")?.capabilities?.[0],
).toBe(
"O dono muda preços, textos, fotos e o mapa sem pedir a ninguém, e a mudança aparece no site sem nova publicação.",
);
for (const caseStudy of labCases) {
expect(caseStudy.capabilities).toBeUndefined();
}
});
it("dates every service, case and fixed page with a plain calendar day for the sitemap", () => {
const days = [
...servicePages.map(({ updatedAt }) => updatedAt),
...caseStudies.map(({ updatedAt }) => updatedAt),
...Object.values(pageUpdatedAt),
];
for (const day of days) expect(day).toMatch(/^\d{4}-\d{2}-\d{2}$/);
// a política de privacidade informa a própria vigência: a data do mapa é a dela
expect(pageUpdatedAt.privacidade).toBe("2026-09-13");
});
it("places the services in Joinville and SC instead of only 'em Garuva'", () => {
@@ -164,8 +323,11 @@ it("derives the exact related service set from the existing relationship", () =>
expect(getRelatedServices("twins-lab").map(({ slug }) => slug)).toEqual([
"integracao-hardware-software-iot",
]);
expect(getRelatedServices("sev-modaintima").map(({ slug }) => slug)).toEqual([
"engenharia-de-software",
]);
expect(
getRelatedServices("sev-modaintima")
getRelatedServices("lavacao-047")
.map(({ slug }) => slug)
.sort(),
).toEqual(