feat(servicos): topo, entregáveis e cases por serviço, assunto próprio no WhatsApp e títulos curtos

This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 22:54:18 -03:00
1 parent cf360041bd
commit 2fd8b063d9
33 files changed
+1578 -300

No files matched your search

+92 -8
View File
@@ -6,8 +6,10 @@ import {
caseStudies,
clientCases,
clientCasesForListing,
generalFirstStep,
getRelatedCases,
getRelatedServices,
getServiceOffer,
isClientCase,
labCases,
pageUpdatedAt,
@@ -93,12 +95,18 @@ 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, with the SEV only where it is the proof: the systems service", () => {
it("maps each service to its client cases, with the SEV as proof of both offers: the site and the system", () => {
// a Lavação abre a página, a SEV é o segundo trabalho e os dois sites institucionais vão para o bloco menor
expect(getService("criacao-de-sites").relatedCaseIds).toEqual([
"lavacao-047",
"sev-modaintima",
"sc-plus",
"as-servicos",
]);
expect(getService("criacao-de-sites").alsoCases).toEqual({
title: "Também faço sites institucionais",
caseIds: ["sc-plus", "as-servicos"],
});
expect(getService("engenharia-de-software").relatedCaseIds).toEqual([
"sev-modaintima",
"lavacao-047",
@@ -111,7 +119,12 @@ it("maps each service to its client cases, with the SEV only where it is the pro
servicePages
.filter(({ relatedCaseIds }) => relatedCaseIds.includes("sev-modaintima"))
.map(({ slug }) => slug),
).toEqual(["engenharia-de-software"]);
).toEqual(["criacao-de-sites", "engenharia-de-software"]);
// o bloco menor só cita cases que o serviço já relaciona
for (const { relatedCaseIds, alsoCases } of servicePages) {
for (const id of alsoCases?.caseIds ?? [])
expect(relatedCaseIds).toContain(id);
}
// 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({});
@@ -195,12 +208,12 @@ it("gives each service without a client cover a flow diagram instead, and never
}
});
it("lists the client cases for the mosaic and /cases with the SEV last, never first", () => {
it("lists the client cases for the mosaic and /cases in the order of the home showcase, the SEV first", () => {
expect(clientCasesForListing.map(({ id }) => id)).toEqual([
"sev-modaintima",
"lavacao-047",
"sc-plus",
"as-servicos",
"sev-modaintima",
]);
expect(clientCasesForListing).toHaveLength(clientCases.length);
});
@@ -222,7 +235,12 @@ it("explains the three offers of the home with the same names, 3 or 4 deliverabl
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",
"Revisão de sistema que já existe",
]);
expect(serviceOffers.map(({ whatsappSubject }) => whatsappSubject)).toEqual([
"um site que traz pedidos pelo WhatsApp",
"um sistema para tirar a operação da planilha",
"a revisão de um sistema que já existe",
]);
expect(serviceOffers.map(({ href }) => href)).toEqual([
"/servicos/criacao-de-sites",
@@ -232,6 +250,13 @@ it("explains the three offers of the home with the same names, 3 or 4 deliverabl
for (const offer of serviceOffers) {
expect(offer.deliverables.length).toBeGreaterThanOrEqual(3);
expect(offer.deliverables.length).toBeLessThanOrEqual(4);
expect(offer.firstStep).toMatch(
/^Primeira conversa .* proposta por escrito\.$/,
);
// a frase geral da home não contradiz a da oferta: as duas terminam igual
expect(offer.firstStep.endsWith(generalFirstStep.split(". ")[1])).toBe(
true,
);
expect(
offer.proof.caseIds.every((id) => caseStudies.some((c) => c.id === id)),
).toBe(true);
@@ -240,6 +265,7 @@ it("explains the three offers of the home with the same names, 3 or 4 deliverabl
offer.name,
offer.summary,
...offer.deliverables,
offer.firstStep,
offer.proof.note ?? "",
].join(" ");
expect(text).not.toMatch(/R\$|\d+\s?%|\d+\s?dias|["“”]/);
@@ -254,6 +280,48 @@ it("explains the three offers of the home with the same names, 3 or 4 deliverabl
expect(serviceOffers[2].proof.caseIds).toEqual([]);
});
it("uses one name per offer: the service page, the breadcrumb label and the WhatsApp subject match the offer", () => {
for (const offer of serviceOffers) {
const service = servicePages.find(({ slug }) => offer.href.endsWith(slug));
// a revisão abre /ia, que não é uma página de serviço
if (!service) {
expect(offer.id).toBe("revisao");
continue;
}
expect(service.navLabel).toBe(offer.name);
expect(service.headline).toBe(offer.name);
expect(service.whatsappSubject).toBe(offer.whatsappSubject);
expect(getServiceOffer(service.slug)).toBe(offer);
}
expect(getServiceOffer("consultoria-nextjs")).toBeUndefined();
});
it("gives every service a short name and a lowercase WhatsApp subject with its article, and no two names alike", () => {
expect(servicePages.map(({ navLabel }) => navLabel)).toEqual([
"Site que traz pedidos pelo WhatsApp",
"Sistema para tirar a operação da planilha",
"Consultoria em Next.js",
"Aplicativos desktop",
"Integração de equipamentos",
"Diagnóstico de arquitetura",
]);
for (const service of servicePages) {
expect(service.whatsappSubject).toMatch(/^(um|uma|a) [a-zà-ú]/);
expect(service.whatsappSubject).not.toMatch(/[.!?]$/);
}
expect(getService("consultoria-nextjs").whatsappSubject).toBe(
"uma consultoria em Next.js",
);
const names = servicePages.map(({ navLabel }) => navLabel);
expect(new Set(names).size).toBe(names.length);
});
it("says the systems service in terms of the pain: spreadsheets and rework", () => {
expect(getService("engenharia-de-software").answer).toMatch(
/planilhas.*retrabalho/,
);
});
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);
@@ -323,7 +391,9 @@ it("derives the exact related service set from the existing relationship", () =>
expect(getRelatedServices("twins-lab").map(({ slug }) => slug)).toEqual([
"integracao-hardware-software-iot",
]);
// a SEV liga à oferta de site e à de sistema, nesta ordem
expect(getRelatedServices("sev-modaintima").map(({ slug }) => slug)).toEqual([
"criacao-de-sites",
"engenharia-de-software",
]);
expect(
@@ -373,7 +443,7 @@ it("writes one sentence on why each related case matters to its service", () =>
}
});
it("cites the Apolo Energia experience, from 2022, only on the IoT and architecture services", () => {
it("cites the Apolo Energia experience, from 2022, only on the systems, IoT and architecture services", () => {
const apolo = portfolioData.experiences.find(
({ id }) => id === "apolo-energia",
);
@@ -383,6 +453,7 @@ it("cites the Apolo Energia experience, from 2022, only on the IoT and architect
.filter(({ experienceNote }) => experienceNote)
.map(({ slug }) => slug);
expect(withNote).toEqual([
"engenharia-de-software",
"integracao-hardware-software-iot",
"arquitetura-de-software",
]);
@@ -390,12 +461,24 @@ it("cites the Apolo Energia experience, from 2022, only on the IoT and architect
const note = getService(slug).experienceNote ?? "";
expect(note).toMatch(/Apolo Energia/);
expect(note).toMatch(/desde 2022/i);
expect(note).toMatch(/sistemas internos/i);
expect(note).toMatch(/aplicativos/i);
expect(note).toMatch(/APIs/);
// sem link e sem detalhe de operação do cliente
expect(note).not.toMatch(/https?:|www\.|\.com/i);
}
for (const slug of [
"integracao-hardware-software-iot",
"arquitetura-de-software",
]) {
const note = getService(slug).experienceNote ?? "";
expect(note).toMatch(/sistemas internos/i);
expect(note).toMatch(/aplicativos/i);
}
// o sistema diz o que o currículo diz da Apolo: aplicações web e mobile para rotinas internas, APIs e painéis de operação
const systemsNote = getService("engenharia-de-software").experienceNote ?? "";
expect(systemsNote).toMatch(/aplicações web e mobile para rotinas internas/);
expect(systemsNote).toMatch(/painéis de operação/);
expect(apolo?.description).toMatch(/aplicações Web e Mobile/);
expect(apolo?.description).toMatch(/rotinas internas/);
});
it("keeps the problem of every client case concrete, with no invented figure or quote", () => {
@@ -449,6 +532,7 @@ it("keeps AS Serviços without a public site and states the author's participati
it("keeps site-content and the modules it imports free of Node built-ins, so client components can import it", () => {
for (const file of [
"site-content",
"offer-labels",
"portfolio-data",
"proof",
"project-health",