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.
This commit is contained in:
1 parent
3756905a1b
commit
6e1022b58c
44 files changed
+2691
-1424
No files matched your search
+125
-74
@@ -1,98 +1,140 @@
|
||||
import { caseStudies, getRelatedServices, servicePages } from "./site-content";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
caseStudies,
|
||||
clientCases,
|
||||
getRelatedCases,
|
||||
getRelatedServices,
|
||||
isClientCase,
|
||||
labCases,
|
||||
servicePages,
|
||||
} from "./site-content";
|
||||
|
||||
const detailedIds = [
|
||||
"as-servicos",
|
||||
"gcc-installer",
|
||||
"nutriscan",
|
||||
"reddit-explorer",
|
||||
"twins-lab",
|
||||
] as const;
|
||||
const immutableGitHubBlobUrl =
|
||||
/^https:\/\/github\.com\/[^/]+\/[^/]+\/blob\/[0-9a-f]{40}\/.+$/;
|
||||
const clientIds = ["sev-modaintima", "lavacao-047", "sc-plus", "as-servicos"];
|
||||
const labIds = ["nutriscan", "twins-lab", "reddit-explorer", "gcc-installer"];
|
||||
|
||||
function getDetail(id: (typeof detailedIds)[number]) {
|
||||
const detail = caseStudies.find((item) => item.id === id)?.detail;
|
||||
expect(detail).toBeDefined();
|
||||
if (!detail) throw new Error(`Missing detail for ${id}`);
|
||||
return detail;
|
||||
function getService(slug: string) {
|
||||
const service = servicePages.find((item) => item.slug === slug);
|
||||
if (!service) throw new Error(`Missing service ${slug}`);
|
||||
return service;
|
||||
}
|
||||
|
||||
it("limits detailed content to the evidenced cases (public repositories pinned to a commit)", () => {
|
||||
it("separates client work from the laboratory using the kind in the portfolio data", () => {
|
||||
expect(clientCases.map(({ id }) => id)).toEqual(clientIds);
|
||||
expect(labCases.map(({ id }) => id)).toEqual(labIds);
|
||||
expect(caseStudies).toHaveLength(8);
|
||||
expect(
|
||||
caseStudies
|
||||
.filter(({ detail }) => detail !== undefined)
|
||||
.map(({ id }) => id)
|
||||
.sort(),
|
||||
).toEqual([...detailedIds].sort());
|
||||
caseStudies.every(
|
||||
(item) => isClientCase(item) === clientIds.includes(item.id),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(isClientCase({ id: "projeto-que-nao-existe" })).toBe(false);
|
||||
});
|
||||
|
||||
it("uses unique source IDs and immutable GitHub commit URLs", () => {
|
||||
for (const id of detailedIds) {
|
||||
const { sources } = getDetail(id);
|
||||
const sourceIds = sources.map(({ id }) => id);
|
||||
expect(new Set(sourceIds).size).toBe(sourceIds.length);
|
||||
|
||||
for (const { url } of sources) {
|
||||
expect(url).toMatch(immutableGitHubBlobUrl);
|
||||
}
|
||||
it("gives every case the sections of the business structure", () => {
|
||||
for (const caseStudy of caseStudies) {
|
||||
expect(caseStudy.headline.length).toBeGreaterThan(10);
|
||||
expect(caseStudy.context.length).toBeGreaterThan(20);
|
||||
expect(caseStudy.problem.length).toBeGreaterThan(20);
|
||||
expect(caseStudy.deliveries.length).toBeGreaterThanOrEqual(3);
|
||||
expect(caseStudy.stack.length).toBeGreaterThan(0);
|
||||
expect(caseStudy.decisions.length).toBeGreaterThan(0);
|
||||
// sem dado real e autorizado, não há seção de resultado
|
||||
expect(caseStudy.result).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("requires sources for factual statements and resolves every source ID", () => {
|
||||
for (const id of detailedIds) {
|
||||
const detail = getDetail(id);
|
||||
const sourceIds = new Set(detail.sources.map(({ id }) => id));
|
||||
const factualStatements = [
|
||||
detail.answer,
|
||||
detail.challenge,
|
||||
...detail.implementation,
|
||||
];
|
||||
|
||||
for (const statement of [...factualStatements, ...detail.boundaries]) {
|
||||
expect(
|
||||
statement.sourceIds.every((sourceId) => sourceIds.has(sourceId)),
|
||||
).toBe(true);
|
||||
}
|
||||
for (const statement of factualStatements) {
|
||||
expect(statement.sourceIds.length).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
expect(
|
||||
getDetail("as-servicos").boundaries.map(({ sourceIds }) => sourceIds),
|
||||
).toEqual([[]]);
|
||||
|
||||
for (const id of ["nutriscan", "twins-lab"] as const) {
|
||||
expect(
|
||||
getDetail(id).boundaries.every(({ sourceIds }) => sourceIds.length > 0),
|
||||
).toBe(true);
|
||||
}
|
||||
expect(getDetail("nutriscan").boundaries[0]?.sourceIds).toContain(
|
||||
"nutri-readme",
|
||||
it("writes the NutriScan case in business language", () => {
|
||||
expect(caseStudies.find(({ id }) => id === "nutriscan")?.headline).toBe(
|
||||
"Consulta de alimentos por nome ou código de barras",
|
||||
);
|
||||
});
|
||||
|
||||
// reddit-explorer e gcc-installer: o 1º limite vem da documentação do repositório; o 2º é o escopo editorial (sem fonte)
|
||||
for (const id of ["reddit-explorer", "gcc-installer"] as const) {
|
||||
const { boundaries } = getDetail(id);
|
||||
expect(boundaries[0]?.sourceIds.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
boundaries.slice(1).every(({ sourceIds }) => sourceIds.length === 0),
|
||||
).toBe(true);
|
||||
it("has no GitHub link, source list or disqualifying caveat in the public text", () => {
|
||||
const text = JSON.stringify({ caseStudies, servicePages });
|
||||
expect(text).not.toMatch(/github\.com/i);
|
||||
expect(text).not.toMatch(/\/#contact/);
|
||||
expect(text).not.toMatch(
|
||||
/Limites desta evidência|Desafio verificável|Exemplos ilustrativos|não projetos já entregues|Sem promessas|nem CI|não há CI|escopo definido|não comprova|não representa/i,
|
||||
);
|
||||
for (const caseStudy of caseStudies) {
|
||||
expect(caseStudy).not.toHaveProperty("githubUrl");
|
||||
expect(caseStudy).not.toHaveProperty("detail");
|
||||
}
|
||||
});
|
||||
|
||||
it("never mentions an unavailable demo in the case text", () => {
|
||||
expect(JSON.stringify(caseStudies)).not.toMatch(/indispon/i);
|
||||
});
|
||||
|
||||
it("leaves the commercial offer of every service empty for the owner to fill in", () => {
|
||||
for (const service of servicePages) {
|
||||
expect(service.startingPrice).toBeUndefined();
|
||||
expect(service.typicalTimeline).toBeUndefined();
|
||||
expect(service.guarantee).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("mentions the region and a benefit in the title and description of every service", () => {
|
||||
for (const service of servicePages) {
|
||||
expect(`${service.seoTitle} ${service.metaDescription}`).toMatch(
|
||||
/Garuva|Joinville|Santa Catarina/,
|
||||
);
|
||||
expect(service.metaDescription).toMatch(/remoto|Brasil/);
|
||||
}
|
||||
const descriptions = servicePages.map(
|
||||
({ metaDescription }) => metaDescription,
|
||||
);
|
||||
expect(new Set(descriptions).size).toBe(descriptions.length);
|
||||
});
|
||||
|
||||
it("maps each service to its client cases", () => {
|
||||
expect(getService("criacao-de-sites").relatedCaseIds).toEqual(clientIds);
|
||||
expect(getService("engenharia-de-software").relatedCaseIds).toEqual([
|
||||
"sev-modaintima",
|
||||
"lavacao-047",
|
||||
]);
|
||||
expect(getService("consultoria-nextjs").relatedCaseIds).toEqual([
|
||||
"sev-modaintima",
|
||||
"lavacao-047",
|
||||
"sc-plus",
|
||||
]);
|
||||
expect(getService("arquitetura-de-software").relatedCaseIds).toEqual([
|
||||
"sev-modaintima",
|
||||
]);
|
||||
// GCC Installer é Python/Tkinter: não combina com um serviço de Tauri e Rust
|
||||
expect(getService("tauri-rust").relatedCaseIds).toEqual([]);
|
||||
expect(getService("integracao-hardware-software-iot").relatedCaseIds).toEqual(
|
||||
["twins-lab"],
|
||||
);
|
||||
});
|
||||
|
||||
it("puts client cases before laboratory cases when listing related work", () => {
|
||||
expect(
|
||||
getRelatedCases(["twins-lab", "lavacao-047", "sev-modaintima"]).map(
|
||||
({ id }) => id,
|
||||
),
|
||||
).toEqual(["lavacao-047", "sev-modaintima", "twins-lab"]);
|
||||
expect(getRelatedCases(["nao-existe"])).toEqual([]);
|
||||
});
|
||||
|
||||
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("twins-lab")
|
||||
getRelatedServices("sev-modaintima")
|
||||
.map(({ slug }) => slug)
|
||||
.sort(),
|
||||
).toEqual(
|
||||
[
|
||||
"criacao-de-sites",
|
||||
"engenharia-de-software",
|
||||
"consultoria-nextjs",
|
||||
"integracao-hardware-software-iot",
|
||||
"arquitetura-de-software",
|
||||
].sort(),
|
||||
);
|
||||
expect(getRelatedServices("gcc-installer")).toEqual([]);
|
||||
});
|
||||
|
||||
it("resolves every declared related case ID", () => {
|
||||
@@ -112,13 +154,22 @@ it("does not publish the unavailable Twins Lab demo", () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps AS Serviços as a project (code only) and states the author's participation", () => {
|
||||
it("keeps AS Serviços without a public site and states the author's participation", () => {
|
||||
const asServicos = caseStudies.find(({ id }) => id === "as-servicos");
|
||||
expect(asServicos?.demoUrl).toBeNull();
|
||||
expect(asServicos?.githubUrl).toBe(
|
||||
"https://github.com/evandrodevbr/as-servicos",
|
||||
);
|
||||
expect(asServicos?.role).toMatch(/primeira landing page/i);
|
||||
expect(asServicos?.role).toMatch(/animações 3D/i);
|
||||
expect(asServicos?.role).toMatch(/painel para controle de projetos/i);
|
||||
});
|
||||
|
||||
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",
|
||||
"portfolio-data",
|
||||
"proof",
|
||||
"project-health",
|
||||
]) {
|
||||
const source = readFileSync(join(__dirname, `${file}.ts`), "utf8");
|
||||
expect(source).not.toMatch(/from "node:/);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user