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:
Evandro Fonseca Junior committed 2026-10-14 03:31:57 -03:00
1 parent 3756905a1b
commit 6e1022b58c
44 files changed
+2691 -1424

No files matched your search

+1
View File
@@ -68,6 +68,7 @@ export function OgCard({
/>
</>
)}
{/* biome-ignore lint/performance/noImgElement: o ImageResponse (satori) só desenha <img> */}
<img
src={brandMark}
alt="Evandro.dev.br"
+2 -1
View File
@@ -8,7 +8,8 @@ jest.mock("./project-health", () => ({
}));
import { readFile } from "node:fs/promises";
import { loadFeaturedProjects, portfolio } from "./portfolio-data";
import { portfolio } from "./portfolio-data";
import { loadFeaturedProjects } from "./portfolio-data.server";
import { createProjectHealthSnapshot } from "./project-health";
const featuredIds = portfolio.projects
+70
View File
@@ -0,0 +1,70 @@
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import {
getFeaturedProjects,
type PortfolioProjectView,
portfolio,
} from "./portfolio-data";
import {
createProjectHealthSnapshot,
PROJECT_HEALTH_MAX_AGE_MS,
type ProjectHealthSnapshot,
} from "./project-health";
// Só no servidor: lê o arquivo de saúde das demonstrações (node:fs). lib/portfolio-data.ts fica sem Node,
// porque lib/site-content.ts e lib/proof.ts o importam e componentes de cliente chegam a eles.
function isSnapshotFresh(
snapshot: ProjectHealthSnapshot | null,
now = new Date(),
) {
if (!snapshot) return false;
const checkedAt = Date.parse(snapshot.checkedAt);
return (
Number.isFinite(checkedAt) &&
now.getTime() - checkedAt <= PROJECT_HEALTH_MAX_AGE_MS
);
}
export async function loadProjectHealthSnapshot(): Promise<ProjectHealthSnapshot | null> {
try {
const path = resolve(process.cwd(), "data/project-health.json");
return JSON.parse(await readFile(path, "utf8")) as ProjectHealthSnapshot;
} catch {
return null;
}
}
export async function loadFeaturedProjects(): Promise<PortfolioProjectView[]> {
const snapshot = await loadProjectHealthSnapshot();
if (process.env.NODE_ENV !== "development" || isSnapshotFresh(snapshot)) {
return getFeaturedProjects(snapshot);
}
const projectDemos = portfolio.projects.reduce<
Array<{ id: string; demoUrl: string }>
>((demos, project) => {
if (project.demoUrl)
demos.push({ id: project.id, demoUrl: project.demoUrl });
return demos;
}, []);
try {
const refreshed = await createProjectHealthSnapshot(projectDemos);
const projects = { ...refreshed.projects };
for (const [id, check] of Object.entries(projects)) {
if (!check.error && check.status !== null) continue;
const previous = snapshot?.projects[id];
if (previous && !previous.error && previous.status !== null) {
projects[id] = previous;
} else {
delete projects[id];
}
}
return getFeaturedProjects({ ...refreshed, projects });
} catch {
return getFeaturedProjects(snapshot);
}
}
+33 -2
View File
@@ -63,7 +63,9 @@ describe("factual portfolio records", () => {
const project = portfolio.projects.find((item) => item.id === id);
expect(project).toBeDefined();
if (!project) throw new Error(`Missing project ${id}`);
return JSON.stringify(project);
// "kind" (cliente ou laboratório) é classificação do portfólio, não afirmação sobre o projeto
const { kind: _kind, ...claims } = project;
return JSON.stringify(claims);
}
it("keeps every public field of the three cases within verified claims", () => {
@@ -73,7 +75,7 @@ describe("factual portfolio records", () => {
expect(nutriscan).toContain("Open Food Facts");
expect(nutriscan).not.toMatch(
/Next\.js\s*\d|PWA|Service Worker|MongoDB|históric|scanner|offline|NutriScore|produç|24\/7|2M\+|disponibilidade|volume de produtos|resultado de saúde/i,
/Next\.js\s*\d|PWA|Service Worker|MongoDB|históric|scanner|offline|NutriScore|24\/7|2M\+|disponibilidade|volume de produtos|resultado de saúde/i,
);
expect(twins).toContain("SSE");
@@ -112,4 +114,33 @@ describe("factual portfolio records", () => {
portfolio.projects.find((item) => item.id === "twins-lab")?.demoUrl,
).toBeUndefined();
});
it("leaves no GitHub link in the portfolio data", () => {
expect(JSON.stringify(portfolio)).not.toMatch(/github\.com/i);
for (const project of portfolio.projects) {
expect(project.githubUrl).toBe("");
}
});
it("does not call a project public code while no repository link is published", () => {
expect(JSON.stringify(portfolio)).not.toMatch(/código público/i);
const status = (id: string) =>
portfolio.projects.find((item) => item.id === id)?.status;
// o NutriScan tem demonstração no ar; o site da AS Serviços está concluído, mas não tem endereço público
expect(status("nutriscan")).toBe("Em Produção");
expect(status("as-servicos")).toBe("Concluído");
});
it("keeps a single LinkedIn profile", () => {
const linkedinUrls = JSON.stringify(portfolio).match(
/https:\/\/www\.linkedin\.com\/in\/[^"]+/g,
);
expect(new Set(linkedinUrls)).toEqual(
new Set(["https://www.linkedin.com/in/evandrodevbr/"]),
);
});
it("states no limitation about the Reddit Posts Explorer in its public text", () => {
expect(serializeProject("reddit-explorer")).not.toMatch(/nem CI|não tem/i);
});
});
+1 -63
View File
@@ -1,19 +1,12 @@
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import portfolioData from "@/public/portfolio-data.json";
import {
createProjectHealthSnapshot,
type DemoAvailability,
getDemoAvailability,
PROJECT_HEALTH_MAX_AGE_MS,
type ProjectHealthSnapshot,
} from "./project-health";
export type PortfolioData = typeof portfolioData;
type PortfolioData = typeof portfolioData;
export type PortfolioProject = PortfolioData["projects"][number];
export type PortfolioExperience = PortfolioData["experiences"][number];
export type PortfolioSkillCategory =
PortfolioData["skills"]["technical"][number];
export type PortfolioProjectView = PortfolioProject & {
demoAvailability: DemoAvailability;
@@ -36,58 +29,3 @@ export function getFeaturedProjects(
};
});
}
function isSnapshotFresh(
snapshot: ProjectHealthSnapshot | null,
now = new Date(),
) {
if (!snapshot) return false;
const checkedAt = Date.parse(snapshot.checkedAt);
return (
Number.isFinite(checkedAt) &&
now.getTime() - checkedAt <= PROJECT_HEALTH_MAX_AGE_MS
);
}
export async function loadProjectHealthSnapshot(): Promise<ProjectHealthSnapshot | null> {
try {
const path = resolve(process.cwd(), "data/project-health.json");
return JSON.parse(await readFile(path, "utf8")) as ProjectHealthSnapshot;
} catch {
return null;
}
}
export async function loadFeaturedProjects(): Promise<PortfolioProjectView[]> {
const snapshot = await loadProjectHealthSnapshot();
if (process.env.NODE_ENV !== "development" || isSnapshotFresh(snapshot)) {
return getFeaturedProjects(snapshot);
}
const projectDemos = portfolio.projects.reduce<
Array<{ id: string; demoUrl: string }>
>((demos, project) => {
if (project.demoUrl)
demos.push({ id: project.id, demoUrl: project.demoUrl });
return demos;
}, []);
try {
const refreshed = await createProjectHealthSnapshot(projectDemos);
const projects = { ...refreshed.projects };
for (const [id, check] of Object.entries(projects)) {
if (!check.error && check.status !== null) continue;
const previous = snapshot?.projects[id];
if (previous && !previous.error && previous.status !== null) {
projects[id] = previous;
} else {
delete projects[id];
}
}
return getFeaturedProjects({ ...refreshed, projects });
} catch {
return getFeaturedProjects(snapshot);
}
}
+49 -6
View File
@@ -1,11 +1,13 @@
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";
@@ -61,10 +63,7 @@ it("keeps the root graph limited to verified website and person fields", () => {
image: `${siteUrl}/edev.jpg`,
jobTitle: "Engenheiro da Computação e Desenvolvedor de Software",
homeLocation: { "@type": "Place", name: "Garuva, Santa Catarina, Brasil" },
sameAs: [
"https://github.com/evandrodevbr",
"https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/",
],
sameAs: ["https://www.linkedin.com/in/evandrodevbr/"],
});
expect(JSON.stringify([website, person])).not.toMatch(
@@ -121,16 +120,60 @@ it("keeps profile data timeless and sitemap output deterministic", () => {
`${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).toEqual(expectedUrls.map((url) => ({ url })));
expect(first.map(({ url }) => url)).toEqual(expectedUrls);
expect(second).toEqual(first);
expect(first).toHaveLength(17);
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,
},
});
});
+38 -4
View File
@@ -1,5 +1,8 @@
import type { Metadata } from "next";
import { type ServicePageContent, siteName, siteUrl } from "./site-content";
export const twitterHandle = "@evandrodevbr";
export const SEO_IDS = {
website: `${siteUrl}/#website`,
person: `${siteUrl}/#person`,
@@ -40,10 +43,7 @@ export function buildPersonJsonLd() {
"@type": "Place",
name: "Garuva, Santa Catarina, Brasil",
},
sameAs: [
"https://github.com/evandrodevbr",
"https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/",
],
sameAs: ["https://www.linkedin.com/in/evandrodevbr/"],
} as const;
}
@@ -93,3 +93,37 @@ export function buildBreadcrumbList(
})),
} as const;
}
/**
* Canonical, Open Graph e Twitter de uma página. Quem define `openGraph` ou `twitter` numa página substitui o do layout inteiro,
* então o cartão precisa vir completo daqui; a imagem vem do `opengraph-image` da própria rota.
*/
export function buildPageSocialMetadata({
url,
title,
description,
type,
}: {
url: string;
title: string;
description: string;
type: "website" | "article" | "profile";
}): Pick<Metadata, "alternates" | "openGraph" | "twitter"> {
return {
alternates: { canonical: url },
openGraph: {
locale: "pt_BR",
title,
description,
url,
siteName,
type,
},
twitter: {
card: "summary_large_image",
title,
description,
creator: twitterHandle,
},
};
}
+125 -74
View File
@@ -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:/);
}
});
+282 -423
View File
@@ -1,49 +1,9 @@
import { portfolio } from "./portfolio-data";
import { isClientProject } from "./proof";
export const siteUrl = "https://evandro.dev.br";
export const siteName = "Evandro.dev.br";
export const intentCtas = {
project: {
label: "Conversar sobre meu projeto",
href: "/#contact",
},
consulting: {
label: "Conhecer as soluções",
href: "/servicos",
},
} as const;
export type CtaIntent = keyof typeof intentCtas;
export const regionCities = [
"Garuva",
"Joinville",
"São Francisco do Sul",
"Itapoá",
"Balneário Barra do Sul",
"Barra Velha",
"Jaraguá do Sul",
"Curitiba",
];
export interface EvidenceSource {
id: string;
label: string;
url: string;
}
export interface SourcedStatement {
text: string;
sourceIds: string[];
}
export interface CaseStudyDetail {
answer: SourcedStatement;
challenge: SourcedStatement;
implementation: SourcedStatement[];
boundaries: SourcedStatement[];
sources: EvidenceSource[];
}
export interface ServicePageContent {
slug: string;
navLabel: string;
@@ -59,25 +19,42 @@ export interface ServicePageContent {
examples: string[];
technicalDetails: string[];
stack: string[];
ctaIntent: "consulting";
ctaLabel: string;
ctaHref: string;
/** Ids dos cases relacionados; os de cliente aparecem primeiro na página. */
relatedCaseIds: string[];
/** Oferta: preencha só com valores reais. O bloco "Investimento e prazo" só aparece quando algum campo existe. */
startingPrice?: string;
typicalTimeline?: string;
guarantee?: string;
faq?: { question: string; answer: string }[];
}
export interface CaseStudyContent {
id: string;
/** Nome do projeto ou do cliente. */
title: string;
/** Tipo do projeto, em poucas palavras. */
subtitle: string;
/** Título da página, em linguagem de negócio. */
headline: string;
/** Resumo de uma ou duas frases (cards, descrição de busca e abertura da página). */
summary: string;
/** O que muda para quem usa, em uma frase. */
impact: string;
/** Quem é o cliente e em que situação o projeto nasceu. */
context: string;
/** O problema que o projeto resolve. */
problem: string;
/** O que foi feito, escrito como benefício. */
deliveries: string[];
/** Resultado com dado real e autorizado. Sem dado, o campo fica de fora e a seção não aparece. */
result?: string[];
/** Detalhes técnicos: tecnologias e decisões de projeto (ficam recolhidos no fim da página). */
stack: string[];
decisions: string[];
demoUrl?: string | null;
githubUrl?: string | null;
/** Papel do autor quando o projeto não é só dele (texto editorial, sem fonte pública). */
/** Papel do autor quando o projeto não é só dele. */
role?: string;
detail?: CaseStudyDetail;
}
export const servicePages: ServicePageContent[] = [
@@ -85,9 +62,9 @@ export const servicePages: ServicePageContent[] = [
slug: "criacao-de-sites",
navLabel: "Sites profissionais",
eyebrow: "Presença digital",
seoTitle: "Criação de Sites Profissionais e Institucionais",
seoTitle: "Criação de Sites para Empresas em Garuva e Joinville",
metaDescription:
"Sites profissionais para apresentar serviços, produtos e canais de contato. Desenvolvimento responsivo com acessibilidade e SEO técnico, em escopo definido.",
"Site profissional para apresentar sua empresa e levar o cliente ao seu WhatsApp. Desenvolvido em Garuva-SC, com atendimento em Joinville, região e remoto no Brasil.",
headline: "Um site que explica seu negócio.",
answer:
"Crio sites para apresentar sua empresa, mostrar o que você oferece e facilitar o contato. O projeto considera leitura no celular e no computador, conteúdo, páginas e funcionalidades combinadas antes do desenvolvimento.",
@@ -101,7 +78,7 @@ export const servicePages: ServicePageContent[] = [
outcomes: [
"Páginas com uma sequência clara: o que você faz, para quem e como entrar em contato.",
"Layout adaptado a celular e computador, com atenção à leitura e à navegação.",
"Publicação e orientação de uso dentro do escopo; domínio, hospedagem, produção de conteúdo e manutenção são combinados na proposta.",
"Publicação do site e orientação de uso. Domínio, hospedagem, produção de conteúdo e manutenção são combinados à parte, na proposta.",
],
examples: [
"Um site institucional para uma prestadora de serviços, com apresentação, áreas de atuação e contato.",
@@ -117,13 +94,11 @@ export const servicePages: ServicePageContent[] = [
technicalDetails: [
"Implementação com Next.js e TypeScript, escolhendo renderização e carregamento conforme o conteúdo e as integrações.",
"HTML semântico, navegação por teclado, imagens dimensionadas e avaliação de carregamento orientam acessibilidade e performance.",
"Metadados, URLs canônicas, sitemap e dados estruturados quando aplicáveis ajudam a interpretação do site pelos buscadores; não garantem posição no Google.",
"Metadados, URLs canônicas, sitemap e dados estruturados ajudam os buscadores a entender o site.",
],
stack: ["Next.js", "TypeScript", "Tailwind CSS", "SEO técnico", "Vercel"],
ctaIntent: "consulting",
ctaLabel: "Conversar sobre meu site",
ctaHref: "/#contact",
relatedCaseIds: ["sc-plus", "nutriscan"],
relatedCaseIds: ["sev-modaintima", "lavacao-047", "sc-plus", "as-servicos"],
faq: [
{
question: "Quanto custa e o que preciso ter pronto?",
@@ -133,7 +108,7 @@ export const servicePages: ServicePageContent[] = [
{
question: "O site também funciona no celular?",
answer:
"Sim, a proposta considera um site responsivo, acessado pelo navegador no celular e no computador. Isso não equivale a um aplicativo instalado; se você precisa de um app, avaliamos esse escopo separadamente.",
"Sim. O site se adapta à tela e abre pelo navegador, no celular e no computador. Se você precisa de um aplicativo instalado no aparelho, conversamos sobre isso em um projeto à parte.",
},
{
question: "Posso contratar melhorias depois da publicação?",
@@ -146,9 +121,9 @@ export const servicePages: ServicePageContent[] = [
slug: "engenharia-de-software",
navLabel: "Sistemas web e aplicativos mobile",
eyebrow: "Engenharia de software",
seoTitle: "Sistemas Web e Aplicativos Mobile sob Medida",
seoTitle: "Sistemas Web e Aplicativos sob Medida em Santa Catarina",
metaDescription:
"Sistemas web e aplicativos mobile sob medida, integração entre ferramentas e evolução de software existente. Escopo, plataformas e entregas definidos por projeto.",
"Sistema web ou aplicativo sob medida para organizar pedidos, clientes e rotinas em um só lugar. Desenvolvedor em Garuva-SC, com atendimento em Joinville e remoto no Brasil.",
headline: "Sistemas e apps para o seu trabalho.",
answer:
"Desenvolvo sistemas web, usados no navegador, e aplicativos mobile para celular. Também conecto ferramentas e evoluo sistemas existentes. Definimos juntos os usuários, as funções, as plataformas atendidas e a primeira entrega útil para o negócio.",
@@ -162,7 +137,7 @@ export const servicePages: ServicePageContent[] = [
outcomes: [
"Uma primeira versão com as funções prioritárias, em vez de tentar resolver toda a operação de uma vez.",
"Fluxos para consultar, registrar e compartilhar informações, com acessos definidos por perfil.",
"Escopo explícito para web ou mobile: dispositivos, sistemas operacionais, uso sem internet e distribuição são avaliados na proposta, não presumidos.",
"Web ou mobile, conforme o seu caso: dispositivos, sistemas operacionais, uso sem internet e distribuição são combinados na proposta.",
],
examples: [
"Um sistema web para acompanhar pedidos e atendimentos em uma única tela.",
@@ -178,13 +153,11 @@ export const servicePages: ServicePageContent[] = [
technicalDetails: [
"A arquitetura parte das regras do negócio, dos limites entre módulos e dos contratos de API. Migrações e integrações consideram os dados e o sistema já em uso.",
"Autorização por perfil, validação no servidor, tratamento de falhas e testes dos fluxos críticos entram conforme os riscos do projeto.",
"A experiência profissional declarada inclui Web e Mobile. A tecnologia mobile é definida após avaliar dispositivos e recursos necessários; os cases abaixo não comprovam um framework mobile específico.",
"Em aplicativos mobile, a tecnologia é escolhida depois de avaliar os dispositivos e os recursos necessários, como câmera, notificações e uso sem internet.",
],
stack: ["Next.js", "TypeScript", "Node.js", "PostgreSQL", "APIs REST"],
ctaIntent: "consulting",
ctaLabel: "Conversar sobre meu sistema ou app",
ctaHref: "/#contact",
relatedCaseIds: ["nutriscan", "reddit-explorer", "sc-plus"],
relatedCaseIds: ["sev-modaintima", "lavacao-047"],
faq: [
{
question: "Preciso saber qual tecnologia usar?",
@@ -199,7 +172,7 @@ export const servicePages: ServicePageContent[] = [
{
question: "O aplicativo inclui publicação em lojas?",
answer:
"A forma de distribuição precisa ser avaliada no projeto. Plataformas, contas, requisitos de terceiros e eventual publicação são definidos antes da contratação; não fazem parte de uma promessa genérica de entrega.",
"A forma de distribuição é definida no projeto. Plataformas, contas de desenvolvedor, requisitos das lojas e a publicação são combinados antes da contratação.",
},
],
},
@@ -207,9 +180,9 @@ export const servicePages: ServicePageContent[] = [
slug: "consultoria-nextjs",
navLabel: "Melhorias em sites e sistemas Next.js",
eyebrow: "Consultoria Next.js",
seoTitle: "Consultoria Next.js para Sites e Sistemas",
seoTitle: "Consultoria Next.js para Melhorar Seu Site ou Sistema",
metaDescription:
"Diagnóstico e melhorias em sites e sistemas Next.js: carregamento, navegação, SEO técnico e manutenção. Prioridades explicadas e implementação em escopo definido.",
"Descubra por que seu site ou sistema Next.js está lento ou com falhas e receba um plano de correção claro. Atendimento a partir de Garuva-SC, em Joinville e remoto no Brasil.",
headline: "Seu projeto Next.js pode funcionar melhor.",
answer:
"Analiso sites e sistemas feitos em Next.js para entender lentidão, falhas ou dificuldades de manutenção. Você recebe prioridades explicadas, um plano de correção e, quando contratado, a implementação das melhorias.",
@@ -223,7 +196,7 @@ export const servicePages: ServicePageContent[] = [
outcomes: [
"Diagnóstico com problemas observados, impacto para o usuário e prioridades.",
"Plano de correção que distingue ajustes pontuais de mudanças maiores.",
"Implementação ou acompanhamento do time, conforme a contratação; metas de performance dependem da medição e do contexto, sem promessa de ranking.",
"Implementação das correções ou acompanhamento do seu time, conforme a contratação. As metas de desempenho partem de medições do seu site.",
],
examples: [
"Investigar por que uma página demora a abrir no celular.",
@@ -242,10 +215,8 @@ export const servicePages: ServicePageContent[] = [
"Auditoria de metadados, canônicas, sitemap, dados estruturados e links internos, preservando URLs e requisitos de indexação nas mudanças.",
],
stack: ["Next.js", "React", "TypeScript", "Tailwind", "SEO técnico"],
ctaIntent: "consulting",
ctaLabel: "Conversar sobre meu projeto Next.js",
ctaHref: "/#contact",
relatedCaseIds: ["nutriscan", "twins-lab", "sc-plus"],
relatedCaseIds: ["sev-modaintima", "lavacao-047", "sc-plus"],
faq: [
{
question: "Não sei se meu site usa Next.js. Posso conversar?",
@@ -255,7 +226,7 @@ export const servicePages: ServicePageContent[] = [
{
question: "A consultoria inclui corrigir o código?",
answer:
"Pode incluir. Um diagnóstico pontual, a implementação das correções e o acompanhamento de um time são formatos diferentes. A proposta deixa claro quais deles entram e como a entrega será avaliada.",
"Pode incluir. Um diagnóstico pontual, a implementação das correções e o acompanhamento de um time são formatos diferentes. A proposta informa quais deles entram e como a entrega será avaliada.",
},
{
question: "Precisa substituir o site inteiro?",
@@ -268,12 +239,12 @@ export const servicePages: ServicePageContent[] = [
slug: "tauri-rust",
navLabel: "Aplicativos desktop para Windows e Linux",
eyebrow: "Aplicativos desktop",
seoTitle: "Aplicativos Desktop com Tauri e Rust",
seoTitle: "Aplicativos Desktop para Windows e Linux com Tauri e Rust",
metaDescription:
"Desenvolvimento de aplicativos desktop para Windows e Linux com Tauri e Rust. Ferramentas locais, arquivos e integrações com compatibilidade definida por projeto.",
"Aplicativo desktop para Windows e Linux que automatiza tarefas no computador da sua empresa. Desenvolvedor em Garuva-SC, com atendimento em Joinville e remoto no Brasil.",
headline: "Aplicativos para Windows e Linux.",
answer:
"Desenvolvo aplicativos desktop para trabalhar no computador, lidar com arquivos e integrar recursos locais. Windows e Linux são plataformas oferecidas; as versões, distribuições e funções compatíveis são definidas e validadas no escopo de cada projeto.",
"Desenvolvo aplicativos desktop para trabalhar no computador, lidar com arquivos e integrar recursos locais. Windows e Linux são as plataformas atendidas; as versões e distribuições compatíveis são combinadas em cada projeto.",
intro:
"Crie uma ferramenta para Windows ou Linux: organize arquivos, execute rotinas locais ou conecte o computador aos sistemas da empresa.",
audience: [
@@ -283,8 +254,8 @@ export const servicePages: ServicePageContent[] = [
],
outcomes: [
"Uma interface para executar as tarefas locais prioritárias e entender o resultado de cada operação.",
"Compatibilidade acordada para Windows e/ou Linux, incluindo versões, distribuições e dependências necessárias.",
"Instalação, atualizações, permissões e uso sem internet tratados no escopo; suportar uma plataforma não significa compatibilidade automática com todas as outras.",
"Compatibilidade combinada para Windows e/ou Linux, incluindo versões, distribuições e dependências necessárias.",
"Instalação, atualizações, permissões e uso sem internet combinados no projeto. Cada plataforma é testada separadamente.",
],
examples: [
"Um aplicativo para conferir e organizar arquivos antes de enviá-los a outro sistema.",
@@ -299,35 +270,32 @@ export const servicePages: ServicePageContent[] = [
],
technicalDetails: [
"Tauri conecta uma interface web a comandos nativos em Rust. Permissões e acesso a arquivos devem ser limitados ao necessário, com validação na fronteira entre interface e sistema.",
"Empacotamento e dependências do WebView variam por sistema. Integrações nativas precisam ser verificadas por alvo; o mesmo código não elimina trabalho específico de Windows ou Linux.",
"O case GCC Installer abaixo é uma referência de automação desktop em Python/Tkinter e Win32 API no Windows. Não é uma implementação em Tauri/Rust nem comprovação de implantação em Linux.",
"Empacotamento e dependências variam por sistema, então cada integração nativa é testada no Windows e no Linux.",
],
stack: [
"Tauri",
"Rust",
"TypeScript",
"Desktop Apps",
"System Integration",
"Aplicativos desktop",
"Integração com o sistema",
],
ctaIntent: "consulting",
ctaLabel: "Conversar sobre meu aplicativo desktop",
ctaHref: "/#contact",
relatedCaseIds: ["gcc-installer"],
relatedCaseIds: [],
faq: [
{
question: "O mesmo aplicativo pode funcionar em Windows e Linux?",
answer:
"Pode ser projetado para os dois, mas isso precisa fazer parte do escopo. Versões, distribuições Linux, recursos locais e dependências são definidos e testados separadamente. Não presumo que uma entrega para Windows já esteja pronta para Linux.",
"Sim, desde que os dois entrem no projeto. Versões do Windows, distribuições Linux, recursos locais e dependências são definidos e testados separadamente.",
},
{
question: "O aplicativo funciona sem internet?",
answer:
"Funções locais podem ser projetadas para isso. Login remoto, sincronização e serviços externos podem continuar dependendo de conexão. Identificamos cada necessidade antes de prometer operação sem internet.",
"As funções locais podem funcionar sem internet. Login remoto, sincronização e serviços externos continuam precisando de conexão. Vemos cada caso na conversa inicial.",
},
{
question: "Você já tem um case desktop público?",
question: "Como o aplicativo é instalado e atualizado?",
answer:
"Sim: GCC Installer, uma ferramenta Windows feita em Python, Tkinter e Win32 API. Ela demonstra trabalho com rotinas locais e instalação, não uma entrega anterior em Tauri, Rust ou Linux.",
"A forma de instalação e de atualização é combinada no projeto, conforme o ambiente de uso: Windows, Linux ou os dois.",
},
],
},
@@ -335,12 +303,12 @@ export const servicePages: ServicePageContent[] = [
slug: "integracao-hardware-software-iot",
navLabel: "Integração de equipamentos e sistemas",
eyebrow: "Hardware, software e IoT",
seoTitle: "Integração de Equipamentos, Sistemas e IoT",
seoTitle: "Integração de Equipamentos e Sistemas (IoT) em Santa Catarina",
metaDescription:
"Integração entre equipamentos e software para consultar dados e acompanhar operações. Viabilidade, interfaces e tratamento de falhas definidos por projeto.",
"Leve os dados dos seus equipamentos para um painel e acompanhe a operação sem anotações manuais. Desenvolvedor em Garuva-SC, com atendimento em Joinville e remoto no Brasil.",
headline: "Dados dos equipamentos no seu sistema.",
answer:
"Conecto equipamentos e software para transformar leituras e eventos em informação útil para a operação. Primeiro avaliamos o que cada dispositivo permite acessar; a integração depende dessas interfaces e dos requisitos de segurança e funcionamento.",
"Conecto equipamentos e software para transformar leituras e eventos em informação útil para a operação. Primeiro vemos o que cada dispositivo permite acessar; a integração segue essas interfaces e os requisitos de segurança e funcionamento.",
intro:
"Leve informações de equipamentos para um sistema ou painel. Acompanhe leituras e eventos sem depender apenas de registros manuais.",
audience: [
@@ -351,7 +319,7 @@ export const servicePages: ServicePageContent[] = [
outcomes: [
"Um caminho definido entre a informação do equipamento e a tela de quem precisa usá-la.",
"Tratamento combinado para falta de conexão, leituras inválidas e outras falhas relevantes.",
"Escopo condicionado a protocolos, documentação e acesso ao equipamento; compra de hardware, instalação física e certificações não são presumidas.",
"O trabalho parte dos protocolos, da documentação e do acesso ao equipamento. Compra de hardware, instalação física e certificações são tratadas à parte.",
],
examples: [
"Um painel para consultar leituras de sensores compatíveis.",
@@ -367,24 +335,16 @@ export const servicePages: ServicePageContent[] = [
technicalDetails: [
"A descoberta identifica APIs, arquivos, portas seriais ou protocolos expostos pelo equipamento; a escolha depende da interface realmente disponível.",
"Contratos de dados, validação, persistência, reconexão e rastreamento de falhas organizam o fluxo entre dispositivo, backend e painel. WebSockets entram apenas quando há necessidade de atualização contínua.",
"Twins Lab é um simulador/protótipo documentado. Ele ilustra uma interface para processo físico, mas não comprova integração com equipamento real, WebSockets ou resultado industrial.",
"O Twins Lab, protótipo de laboratório, simula um reator e envia o estado dele a um painel a cada dois segundos, por eventos do servidor.",
],
stack: [
"IoT",
"WebSockets",
"APIs",
"Dashboards",
"Integração de sistemas",
],
ctaIntent: "consulting",
stack: ["IoT", "WebSockets", "APIs", "Painéis", "Integração de sistemas"],
ctaLabel: "Avaliar minha integração",
ctaHref: "/#contact",
relatedCaseIds: ["twins-lab"],
faq: [
{
question: "Qualquer equipamento pode ser conectado?",
answer:
"Não é possível garantir sem avaliar. Precisamos saber quais dados e interfaces ele disponibiliza, se há documentação e quais restrições existem no ambiente. Essa avaliação pode concluir que a integração não é viável no escopo pretendido.",
"Depende do equipamento. Precisamos saber quais dados e interfaces ele disponibiliza, se há documentação e quais restrições existem no ambiente. Fazemos essa avaliação antes de fechar o projeto.",
},
{
question: "É necessário trocar os sistemas existentes?",
@@ -394,7 +354,7 @@ export const servicePages: ServicePageContent[] = [
{
question: "É possível começar pequeno?",
answer:
"Sim. Um equipamento, uma leitura ou um fluxo pode servir para avaliar viabilidade antes de ampliar o projeto. Os critérios dessa avaliação são combinados para não confundir um protótipo com uma solução pronta para toda a operação.",
"Sim. Um equipamento, uma leitura ou um fluxo já permite validar a ideia antes de ampliar o projeto. Os critérios dessa primeira etapa são combinados antes de começar.",
},
],
},
@@ -402,9 +362,10 @@ export const servicePages: ServicePageContent[] = [
slug: "arquitetura-de-software",
navLabel: "Diagnóstico e arquitetura de software",
eyebrow: "Diagnóstico e decisão",
seoTitle: "Diagnóstico e Arquitetura de Software",
seoTitle:
"Diagnóstico e Arquitetura de Software para Decidir com Segurança",
metaDescription:
"Diagnóstico de software para entender falhas, organizar prioridades e planejar evolução. Arquitetura com riscos, alternativas e plano de mudança explicado.",
"Entenda por que seu sistema ficou difícil de mudar e decida o que corrigir, manter ou trocar. Diagnóstico com plano de ação, de Garuva-SC para Joinville e todo o Brasil.",
headline: "Entenda o problema antes de mudar tudo.",
answer:
"Analiso como um sistema está organizado e por que ficou difícil de manter ou evoluir. Você recebe um diagnóstico explicado, prioridades e um plano de mudança para decidir o que corrigir, preservar ou substituir.",
@@ -434,19 +395,17 @@ export const servicePages: ServicePageContent[] = [
technicalDetails: [
"Análise de dependências, fronteiras entre módulos, contratos de API, fluxos de dados e pontos de falha para localizar acoplamento e riscos.",
"A arquitetura proposta considera carga, consistência de dados, operação, segurança e capacidade real do time, evitando distribuir um sistema sem necessidade.",
"Migrações são divididas em etapas verificáveis, com estratégia de transição e reversão quando aplicável. As decisões registram os trade-offs, não apenas um diagrama final.",
"Migrações são divididas em etapas verificáveis, com estratégia de transição e reversão quando aplicável. As decisões ficam registradas com seus prós e contras, não apenas em um diagrama final.",
],
stack: [
"Arquitetura de software",
"APIs",
"Performance",
"Desempenho",
"Integrações",
"Sistemas críticos",
],
ctaIntent: "consulting",
ctaLabel: "Conversar sobre um diagnóstico",
ctaHref: "/#contact",
relatedCaseIds: ["nutriscan", "twins-lab", "reddit-explorer"],
relatedCaseIds: ["sev-modaintima"],
faq: [
{
question: "Preciso ter uma equipe técnica para contratar?",
@@ -468,368 +427,260 @@ export const servicePages: ServicePageContent[] = [
];
export const caseStudies: CaseStudyContent[] = [
{
id: "sev-modaintima",
title: "SEV Moda Íntima",
subtitle: "Catálogo digital e loja virtual",
headline: "Loja virtual de moda íntima com pedido direto pelo WhatsApp",
summary:
"Catálogo digital e loja virtual de moda íntima. A cliente encontra o produto, escolhe o tamanho e envia o pedido pelo WhatsApp. A loja cuida de tudo em um painel próprio.",
impact:
"A cliente encontra o produto e envia o pedido pelo WhatsApp; a loja controla produtos, estoque e pedidos em um painel.",
context:
"A SEV Moda Íntima vende moda íntima pela internet. O site funciona como vitrine e loja virtual: apresenta os produtos, recebe os pedidos pelo WhatsApp e dá à loja um painel para cuidar do catálogo.",
problem:
"Uma loja de moda íntima tem muitos produtos, em vários modelos e tamanhos. A cliente precisa achar o que procura com facilidade e fechar o pedido sem complicação. A loja precisa manter catálogo, estoque e pedidos organizados.",
deliveries: [
"Uma vitrine com animações que dão destaque aos produtos e deixam a navegação agradável.",
"Filtros e busca inteligente: a cliente encontra o que procura e os itens mais relevantes aparecem primeiro.",
"Pedido pelo WhatsApp: a cliente escolhe os produtos no site e envia o pedido para a loja.",
"Frete calculado pela distância até o endereço da cliente.",
"Painel administrativo para a loja cadastrar produtos, acompanhar pedidos e controlar o estoque.",
"Estoque controlado por tamanho, para cada medida ter a sua quantidade.",
],
stack: [
"Next.js 16",
"TypeScript",
"PostgreSQL",
"Prisma",
"Tailwind CSS 4",
"Framer Motion",
],
decisions: [
"A busca usa ranking de relevância e vários filtros combinados.",
"O cálculo de frete usa geocodificação híbrida para chegar à distância entre loja e cliente.",
"A vitrine tem modo escuro e animações com Framer Motion.",
"A área administrativa cobre produtos, estoque e pedidos.",
],
demoUrl: "https://sevmodaintima.com.br",
},
{
id: "lavacao-047",
title: "Lavação 047",
subtitle: "Site e painel para lavação automotiva",
headline:
"Site com tabela de preços e pedido pelo WhatsApp para uma lavação em Garuva",
summary:
"Site da Lavação 047, em Garuva-SC. O cliente vê o preço pelo porte do veículo e pede o serviço pelo WhatsApp com a mensagem já pronta. O dono edita preços, textos e fotos sozinho.",
impact:
"O cliente descobre o preço e pede pelo WhatsApp; o dono atualiza o site sem depender de um desenvolvedor.",
context:
"A Lavação 047 é uma lavação automotiva em Garuva, Santa Catarina. O site apresenta os serviços, a tabela de preços e a localização, e leva o cliente direto para o WhatsApp.",
problem:
"Quem procura uma lavação quer saber quanto custa para o seu veículo e como chegar. O dono, por sua vez, precisa manter preços, textos e fotos em dia sem depender de alguém a cada mudança.",
deliveries: [
"Tabela de preços por porte do veículo: o cliente vê quanto paga pelo seu carro.",
"Pedido pelo WhatsApp com mensagem pronta: um toque abre a conversa já com o serviço escolhido.",
"Painel do dono: ele altera preços, textos, fotos e o mapa sozinho, e a mudança aparece no site sem nova publicação.",
"Galeria de trabalhos por serviço, para o cliente ver o resultado antes de chamar.",
"Mapa de localização, para o cliente achar a lavação.",
"Uma animação de rolagem que leva o carro do sujo ao brilho e perguntas frequentes montadas com os dados reais do negócio.",
"Informações organizadas para o Google e para assistentes de IA entenderem o que a lavação faz e onde fica.",
],
stack: [
"Next.js 16",
"React 19",
"TypeScript",
"Tailwind CSS 4",
"Motion",
"Upstash Redis",
"Vercel Blob",
],
decisions: [
"O conteúdo salvo no painel alimenta a página, os dados estruturados, o sitemap e o cartão de compartilhamento.",
"Conteúdo e imagens ficam guardados em Redis e em armazenamento privado, com arquivo local como alternativa.",
"A animação respeita a preferência do sistema por menos movimento.",
"O mapa de localização usa OpenStreetMap.",
],
demoUrl: "https://landing-page-lavacao047.vercel.app",
},
{
id: "nutriscan",
title: "NutriScan",
subtitle: "Consulta de produtos por nome e código de barras",
headline: "Consulta de alimentos por nome ou código de barras",
summary:
"Aplicação web para consultar produtos alimentícios por nome ou código de barras, combinando cache JSON com a API Open Food Facts.",
"Aplicação web para consultar produtos alimentícios por nome ou código de barras, combinando um cache próprio com a base pública Open Food Facts.",
impact:
"Reduzir a dependência da API externa e manter um caminho explícito para buscas e consulta por código.",
"A consulta encontra o produto por nome ou código e só vai à base externa quando precisa.",
context:
"Projeto próprio, criado para estudar a integração com uma base pública de alimentos. Está no ar para quem quiser testar.",
problem:
"As informações dos produtos vêm de uma base pública externa. O desafio era não depender só dela a cada consulta e deixar claros dois caminhos: buscar pelo nome e consultar pelo código de barras.",
deliveries: [
"Busca por nome ou por código de barras.",
"As consultas passam primeiro pelo cache próprio e só vão à base externa quando faltam resultados, o que reduz a dependência dela.",
"O código de barras é validado (de 8 a 13 dígitos) antes da consulta, para evitar buscas inválidas.",
"Se a base externa demora mais de oito segundos, a consulta é cancelada e a pessoa não fica esperando.",
],
stack: ["Next.js", "TypeScript", "Cache JSON", "Open Food Facts"],
decisions: [
"A busca consulta o cache primeiro, tem um modo somente-cache e recorre ao Open Food Facts quando faltam resultados.",
"A consulta por código remove caracteres não numéricos, aceita de 8 a 13 dígitos, consulta o cache e usa a base externa se não achar.",
"A camada de acesso à base externa cancela requisições após oito segundos e separa a busca por nome da busca por código.",
],
demoUrl: "https://nutriscan.evandro.dev.br",
githubUrl: "https://github.com/evandrodevbr/nutriscan",
detail: {
answer: {
text: "Aplicação web para consultar produtos alimentícios por nome ou código de barras, combinando cache JSON com a API Open Food Facts.",
sourceIds: ["nutri-search", "nutri-barcode", "nutri-wrapper"],
},
challenge: {
text: "Reduzir a dependência da API externa e manter um caminho explícito para buscas e consulta por código.",
sourceIds: ["nutri-search", "nutri-barcode"],
},
implementation: [
{
text: "A busca consulta o cache primeiro, permite modo somente-cache e recorre ao Open Food Facts quando faltam resultados.",
sourceIds: ["nutri-search"],
},
{
text: "A consulta por código remove caracteres não numéricos, aceita de 8 a 13 dígitos, consulta cache e usa a API externa como fallback.",
sourceIds: ["nutri-barcode"],
},
{
text: "O wrapper cancela requisições após oito segundos e diferencia busca por nome de busca por código.",
sourceIds: ["nutri-wrapper"],
},
],
boundaries: [
{
text: "A página não afirma PWA, MongoDB, histórico de usuários, disponibilidade, volume de produtos ou resultado de saúde, porque as fontes atuais não sustentam esse conjunto de forma consistente.",
sourceIds: [
"nutri-search",
"nutri-barcode",
"nutri-wrapper",
"nutri-package",
"nutri-prisma",
"nutri-readme",
],
},
],
sources: [
{
id: "nutri-search",
label: "Rota de busca",
url: "https://github.com/evandrodevbr/nutriscan/blob/ef0e6d4eca1f8588bb6981c1cfa58e4b7db1302b/app/api/search/route.ts",
},
{
id: "nutri-barcode",
label: "Rota de consulta por código",
url: "https://github.com/evandrodevbr/nutriscan/blob/ef0e6d4eca1f8588bb6981c1cfa58e4b7db1302b/app/api/products/%5Bbarcode%5D/route.ts",
},
{
id: "nutri-wrapper",
label: "Wrapper Open Food Facts",
url: "https://github.com/evandrodevbr/nutriscan/blob/ef0e6d4eca1f8588bb6981c1cfa58e4b7db1302b/lib/openFoodFactsApi.ts",
},
{
id: "nutri-package",
label: "Manifesto do projeto",
url: "https://github.com/evandrodevbr/nutriscan/blob/ef0e6d4eca1f8588bb6981c1cfa58e4b7db1302b/package.json",
},
{
id: "nutri-prisma",
label: "Schema Prisma",
url: "https://github.com/evandrodevbr/nutriscan/blob/ef0e6d4eca1f8588bb6981c1cfa58e4b7db1302b/prisma/schema.prisma",
},
{
id: "nutri-readme",
label: "README do projeto",
url: "https://github.com/evandrodevbr/nutriscan/blob/ef0e6d4eca1f8588bb6981c1cfa58e4b7db1302b/README.md",
},
],
},
},
{
id: "twins-lab",
title: "Twins Lab",
subtitle: "Protótipo de gêmeo digital industrial com SSE",
subtitle: "Protótipo de laboratório de gêmeo digital industrial",
headline:
"Painel ao vivo para acompanhar a simulação de um reator industrial",
summary:
"Protótipo de gêmeo digital industrial com autenticação, simulador de reator, persistência MongoDB e streaming de eventos do servidor.",
"Protótipo de gêmeo digital industrial: um simulador de reator envia seu estado a um painel com acesso por login, atualizado a cada dois segundos.",
impact:
"Expor estados simulados do reator a uma interface autenticada e manter histórico/status do simulador.",
"O estado do reator simulado chega ao painel sozinho, só para quem tem acesso, e o histórico fica guardado.",
context:
"Projeto próprio, um protótipo de laboratório para explorar como os dados de um processo físico chegam a uma tela. O reator é simulado.",
problem:
"Como levar o estado de um processo físico para uma tela, mostrar só a quem tem acesso e manter o histórico do que aconteceu?",
deliveries: [
"Acesso por login: só quem está autenticado abre o painel.",
"O estado do reator chega à tela sozinho, a cada dois segundos, sem recarregar a página.",
"Dados e estado do simulador ficam guardados no banco, o que mantém o histórico.",
"Quando a execução ou o cenário muda, todos os painéis abertos são avisados.",
],
stack: ["Next.js", "TypeScript", "NextAuth", "MongoDB", "SSE"],
githubUrl: "https://github.com/evandrodevbr/twins-lab",
detail: {
answer: {
text: "Protótipo de gêmeo digital industrial com autenticação, simulador de reator, persistência MongoDB e streaming de eventos do servidor.",
sourceIds: ["twins-package", "twins-stream", "twins-simulator"],
},
challenge: {
text: "Expor estados simulados do reator a uma interface autenticada e manter histórico/status do simulador.",
sourceIds: ["twins-stream", "twins-simulator"],
},
implementation: [
{
text: "A rota exige sessão do NextAuth antes de abrir o stream.",
sourceIds: ["twins-stream"],
},
{
text: "A resposta usa `text/event-stream`, envia estados/dados/erros em intervalos de dois segundos e limita a conexão a dez minutos.",
sourceIds: ["twins-stream"],
},
{
text: "O simulador salva dados e estado no MongoDB e notifica clientes quando execução ou cenário mudam.",
sourceIds: ["twins-simulator"],
},
],
boundaries: [
{
text: "O case não usa as alegações de economia, ROI, precisão, manutenção preditiva futura, equipamento real, WebSocket, OpenRouter ou atualização sub-segundo.",
sourceIds: [
"twins-package",
"twins-stream",
"twins-simulator",
"twins-readme",
],
},
],
sources: [
{
id: "twins-package",
label: "Manifesto do projeto",
url: "https://github.com/evandrodevbr/twins-lab/blob/f83f99a7417f16ece0cd7fe4832ba122e7152d0c/package.json",
},
{
id: "twins-stream",
label: "Rota de streaming do reator",
url: "https://github.com/evandrodevbr/twins-lab/blob/f83f99a7417f16ece0cd7fe4832ba122e7152d0c/app/api/reactor/stream/route.ts",
},
{
id: "twins-simulator",
label: "Simulador do reator",
url: "https://github.com/evandrodevbr/twins-lab/blob/f83f99a7417f16ece0cd7fe4832ba122e7152d0c/simulators/reactor/reactor-simulator.ts",
},
{
id: "twins-readme",
label: "README do projeto",
url: "https://github.com/evandrodevbr/twins-lab/blob/f83f99a7417f16ece0cd7fe4832ba122e7152d0c/README.md",
},
],
},
decisions: [
"A rota exige sessão do NextAuth antes de abrir a transmissão de eventos.",
"A resposta usa text/event-stream (SSE), envia estados, dados e erros em intervalos de dois segundos e limita a conexão a dez minutos.",
"O simulador salva dados e estado no MongoDB e notifica os clientes quando a execução ou o cenário mudam.",
],
},
{
id: "reddit-explorer",
title: "Reddit Posts Explorer",
subtitle: "Posts do r/artificial em MongoDB, filtrados por período",
headline:
"Posts de um fórum de inteligência artificial, coletados e filtrados por período",
summary:
"Aplicação Astro SSR que coleta os posts em alta do r/artificial, grava no MongoDB sem duplicar e filtra por período, upvotes ou comentários.",
"Aplicação que coleta os posts em alta do r/artificial, guarda tudo no MongoDB sem duplicar e permite filtrar por período e ordenar por votos ou comentários.",
impact:
"Atualizar a coleta sem duplicar posts e consultar o histórico por intervalo de datas e ordenação.",
stack: ["Astro", "MongoDB", "Mongoose", "TypeScript", "Reddit API"],
githubUrl: "https://github.com/evandrodevbr/reddit-search",
detail: {
answer: {
text: "Aplicação Astro em modo SSR que coleta os posts em alta do r/artificial pela API pública do Reddit, grava no MongoDB com upsert por id e permite filtrar por período e ordenar por upvotes ou comentários.",
sourceIds: ["rd-readme", "rd-service", "rd-astro"],
},
challenge: {
text: "Atualizar a coleta sem duplicar posts e consultar o histórico por intervalo de datas e ordenação.",
sourceIds: ["rd-service", "rd-filter"],
},
implementation: [
{
text: "O serviço consulta r/artificial/hot.json com User-Agent identificado e limite de 15 segundos, e grava os posts em lote com upsert pelo id; rodar a coleta de novo atualiza os existentes.",
sourceIds: ["rd-service"],
},
{
text: "O endpoint de filtro valida as datas e o campo de ordenação (ups ou num_comments) e responde 400 quando faltam parâmetros ou eles são inválidos.",
sourceIds: ["rd-filter"],
},
{
text: "O Astro roda com output server e o adapter Node em modo standalone.",
sourceIds: ["rd-astro", "rd-package"],
},
],
boundaries: [
{
text: "O subreddit é fixo, e o projeto não tem paginação, busca textual, autenticação, testes automatizados nem CI.",
sourceIds: ["rd-readme"],
},
{
text: "O case demonstra implementação e decisões técnicas; não atribui ganho de performance, volume de dados ou resultado comercial.",
sourceIds: [],
},
],
sources: [
{
id: "rd-readme",
label: "README do projeto",
url: "https://github.com/evandrodevbr/reddit-search/blob/ff76e08379a51fc02c3b8e4d6c415d627d8c90de/README.md",
},
{
id: "rd-service",
label: "Serviço de coleta e consulta",
url: "https://github.com/evandrodevbr/reddit-search/blob/ff76e08379a51fc02c3b8e4d6c415d627d8c90de/src/lib/services/redditService.ts",
},
{
id: "rd-filter",
label: "Endpoint de filtro",
url: "https://github.com/evandrodevbr/reddit-search/blob/ff76e08379a51fc02c3b8e4d6c415d627d8c90de/src/pages/api/posts/filter.ts",
},
{
id: "rd-astro",
label: "Configuração do Astro",
url: "https://github.com/evandrodevbr/reddit-search/blob/ff76e08379a51fc02c3b8e4d6c415d627d8c90de/astro.config.mjs",
},
{
id: "rd-package",
label: "Manifesto do projeto",
url: "https://github.com/evandrodevbr/reddit-search/blob/ff76e08379a51fc02c3b8e4d6c415d627d8c90de/package.json",
},
],
},
"Rodar a coleta de novo atualiza os posts que já existem, sem duplicar, e o histórico pode ser consultado por datas.",
context:
"Projeto próprio, feito para estudar a coleta de dados de uma API pública e a consulta por período.",
problem:
"Os posts em alta mudam o tempo todo. O desafio era coletar de novo sem duplicar o que já estava guardado e consultar o histórico por datas e por popularidade.",
deliveries: [
"Coleta dos posts em alta do r/artificial pela API pública do Reddit.",
"Cada post é guardado uma vez só: coletar de novo atualiza o que já existe em vez de duplicar.",
"Filtro por período e ordenação por votos ou comentários.",
"A consulta confere as datas e a ordenação e avisa quando os parâmetros estão errados.",
],
stack: ["Astro", "MongoDB", "Mongoose", "TypeScript", "API do Reddit"],
decisions: [
"O serviço consulta r/artificial/hot.json com identificação própria e limite de 15 segundos, e grava os posts em lote com atualização pelo id.",
"O endpoint de filtro valida as datas e o campo de ordenação (votos ou comentários) e responde com erro 400 quando faltam parâmetros ou eles são inválidos.",
"O Astro roda com saída de servidor e o adaptador Node em modo standalone.",
],
},
{
id: "sc-plus",
title: "SC Plus Aquicultura",
subtitle: "Landing page institucional em Next.js 15",
subtitle: "Página institucional",
headline:
"Página institucional para a SC Plus Aquicultura ser encontrada na internet",
summary:
"Landing page institucional em Next.js 15, React 19 e Tailwind CSS 4, com metadados e dados estruturados para indexação.",
"Página institucional da SC Plus Aquicultura, com apresentação da instituição, áreas e projetos, apoiadores e contato, preparada para os buscadores e para o compartilhamento.",
impact:
"Dar à instituição uma página própria, com base técnica para ser encontrada em buscadores.",
"A instituição ganhou uma página própria, preparada para ser encontrada nos buscadores.",
context:
"A SC Plus Aquicultura é uma instituição do setor de aquicultura. Ela precisava de uma página própria para se apresentar, mostrar suas áreas de atuação e seus projetos e receber contato.",
problem:
"Sem uma página própria, quem procura a SC Plus na internet não encontra uma apresentação clara do que a instituição faz. Faltava um endereço próprio, com as áreas, os projetos, os apoiadores e o contato.",
deliveries: [
"Uma página própria que apresenta a instituição, as áreas de atuação e os projetos.",
"Uma seção de apoiadores e de participação, para quem quer apoiar ou participar.",
"Contato direto na própria página.",
"Título, descrição e imagem de compartilhamento próprios, para o link aparecer bem nos buscadores e nas redes.",
"Dados estruturados que ajudam o Google a entender de quem é a página.",
],
stack: ["Next.js 15", "React 19", "Tailwind CSS 4", "TypeScript"],
decisions: [
"A página publica título, descrição, endereço canônico e imagem de compartilhamento.",
"Os dados estruturados descrevem a instituição para os buscadores.",
],
demoUrl: "https://scplus.evandro.dev.br",
githubUrl: "https://github.com/evandrodevbr/scplus",
},
{
id: "gcc-installer",
title: "MinGW GCC Installer",
subtitle: "Instalador gráfico de MinGW-w64/GCC para Windows",
headline: "Instalador do compilador GCC para Windows em uma única janela",
summary:
"Aplicativo Python com interface tkinter que lista as builds do MinGW-w64, baixa a escolhida, instala em C:\\mingw64 e adiciona o GCC ao PATH do usuário.",
"Aplicativo para Windows que lista as versões do MinGW-w64, baixa a escolhida, instala em C:\\mingw64 e deixa o GCC pronto para uso, tudo em uma janela.",
impact:
"Reduzir a configuração manual do GCC no Windows (escolher a build, baixar, extrair e ajustar o PATH) a uma única janela.",
"Escolher a versão, baixar, extrair e ajustar o PATH deixam de ser passos manuais: tudo acontece em uma janela.",
context:
"Projeto próprio, criado para resolver uma tarefa repetitiva de quem programa no Windows: instalar o compilador GCC.",
problem:
"Instalar o GCC no Windows exige vários passos manuais: escolher a versão, baixar, extrair os arquivos e configurar o PATH do sistema.",
deliveries: [
"Lista as versões publicadas do MinGW-w64 e destaca as mais usadas (ucrt e posix).",
"Baixa o pacote escolhido e instala na pasta C:\\mingw64.",
"Configura o PATH do usuário e avisa o Windows da mudança, para o GCC funcionar sem ajustes manuais.",
"Deixa o comando make disponível, renomeando o mingw32-make.exe.",
],
stack: ["Python", "Tkinter", "Win32 API", "GitHub API", "7-Zip"],
githubUrl: "https://github.com/evandrodevbr/gcc-installer",
detail: {
answer: {
text: "Aplicativo Python com interface tkinter que lê as builds publicadas do MinGW-w64, baixa o pacote .7z escolhido, instala em C:\\mingw64 e adiciona o toolchain ao PATH do usuário no registro do Windows.",
sourceIds: ["gcc-readme", "gcc-main"],
},
challenge: {
text: "Reduzir a configuração manual do GCC no Windows (escolher a build, baixar, extrair e ajustar o PATH) a uma única janela.",
sourceIds: ["gcc-readme"],
},
implementation: [
{
text: "A lista de versões vem da API de releases do repositório niXman/mingw-builds-binaries, e as builds com ucrt e thread model posix são destacadas na tabela.",
sourceIds: ["gcc-main"],
},
{
text: "A instalação extrai o pacote com o 7-Zip (caminho padrão fixo) em uma pasta temporária, move o resultado para C:\\mingw64 e renomeia mingw32-make.exe para make.exe.",
sourceIds: ["gcc-main"],
},
{
text: "Adicionar ao PATH grava em HKCU\\Environment e avisa o Windows com WM_SETTINGCHANGE; fora do Windows o programa encerra com um aviso.",
sourceIds: ["gcc-main"],
},
],
boundaries: [
{
text: "Funciona só no Windows, o caminho do 7-Zip é fixo e o fluxo da interface não foi exercitado de ponta a ponta no Windows nesta revisão; os 23 testes cobrem a lógica independente de plataforma, e não há CI.",
sourceIds: ["gcc-readme", "gcc-tests"],
},
{
text: "O case demonstra implementação e decisões técnicas; não atribui ganho de performance, número de usuários ou resultado comercial.",
sourceIds: [],
},
],
sources: [
{
id: "gcc-readme",
label: "README do projeto",
url: "https://github.com/evandrodevbr/gcc-installer/blob/bab64fc5b5c20128e7b06e1afc448c5efe71b747/README.md",
},
{
id: "gcc-main",
label: "Aplicativo (main.py)",
url: "https://github.com/evandrodevbr/gcc-installer/blob/bab64fc5b5c20128e7b06e1afc448c5efe71b747/main.py",
},
{
id: "gcc-tests",
label: "Testes do núcleo",
url: "https://github.com/evandrodevbr/gcc-installer/blob/bab64fc5b5c20128e7b06e1afc448c5efe71b747/tests/test_core.py",
},
],
},
decisions: [
"A lista de versões vem da API de releases do repositório niXman/mingw-builds-binaries, e as versões com ucrt e posix aparecem em destaque na tabela.",
"A instalação extrai o pacote com o 7-Zip (no caminho padrão) em uma pasta temporária e move o resultado para C:\\mingw64.",
"Adicionar ao PATH grava em HKCU\\Environment e avisa o Windows com WM_SETTINGCHANGE.",
"Os 23 testes automatizados cobrem a lógica do núcleo, que não depende da plataforma.",
],
},
{
id: "as-servicos",
title: "AS Serviços",
subtitle: "Site institucional com cena 3D procedural",
subtitle: "Site institucional com cena 3D",
headline: "Site institucional com uma cena 3D que acompanha a rolagem",
summary:
"Site institucional em Next.js 16 com cena Three.js gerada em código: uma casa wireframe e uma placa-mãe ligadas ao progresso da rolagem.",
"Site institucional da AS Serviços com uma cena 3D criada em código: uma casa e uma placa-mãe desenhadas em linhas que reagem à rolagem da página.",
impact:
"Representar engenharia física e software na mesma narrativa visual sem depender de um modelo 3D pronto.",
"Engenharia física e software contados na mesma história visual, sem depender de um modelo 3D pronto.",
context:
"A AS Serviços reúne engenharia física e software. O site precisava mostrar as duas frentes na mesma história.",
problem:
"Como contar, em uma única página, duas frentes tão diferentes quanto a engenharia física e o software, de um jeito que prenda a atenção de quem rola a página?",
deliveries: [
"Uma cena 3D que acompanha a rolagem: casa, instalação elétrica e placa-mãe aparecem desenhadas em linhas.",
"As duas frentes da empresa, a física e a digital, aparecem na mesma história visual.",
"A qualidade da cena se ajusta ao aparelho: celulares, conexões mais simples e quem prefere menos movimento recebem uma versão mais leve.",
"Um painel para controle de projetos, desenvolvido junto com a primeira página do site.",
],
stack: ["Next.js 16", "Three.js", "TypeScript"],
decisions: [
"A cena é gerada em código, sem arquivo 3D pronto: o motor monta casa, instalação elétrica e placa-mãe com geometrias e linhas do Three.js.",
"O canvas converte a rolagem em um progresso de 0 a 1 e o entrega ao motor da cena.",
"A qualidade tem quatro níveis (estático, baixo, médio e alto), escolhidos conforme o WebGL, a preferência por menos movimento, a economia de dados, a conexão e a capacidade do dispositivo.",
],
demoUrl: null,
githubUrl: "https://github.com/evandrodevbr/as-servicos",
role: "Desenvolvimento da primeira landing page do site, com animações 3D, e de um painel para controle de projetos.",
detail: {
answer: {
text: "Site institucional em Next.js 16 com cena Three.js gerada em código: uma casa wireframe e uma placa-mãe ligadas ao progresso da rolagem.",
sourceIds: ["as-readme", "as-package", "as-engine"],
},
challenge: {
text: "Representar engenharia física e software na mesma narrativa visual sem depender de um modelo 3D pronto.",
sourceIds: ["as-readme", "as-engine"],
},
implementation: [
{
text: "O motor monta casa, instalação elétrica e placa-mãe com geometrias e linhas do Three.js.",
sourceIds: ["as-engine"],
},
{
text: "O canvas converte a rolagem em progresso de 0 a 1 e o entrega ao motor da cena.",
sourceIds: ["as-canvas"],
},
{
text: "A qualidade pode cair para `static`, `low`, `medium` ou `high` conforme WebGL, reduced motion, economia de dados, conexão e capacidade do dispositivo.",
sourceIds: ["as-engine"],
},
],
boundaries: [
{
text: "O case demonstra implementação e decisões técnicas; não atribui geração de leads, ranking ou ganho de performance.",
sourceIds: [],
},
],
sources: [
{
id: "as-readme",
label: "README do projeto",
url: "https://github.com/evandrodevbr/as-servicos/blob/abeb9946a704139336942bfd41ad163ccfe1e685/README.md",
},
{
id: "as-package",
label: "Manifesto do projeto",
url: "https://github.com/evandrodevbr/as-servicos/blob/abeb9946a704139336942bfd41ad163ccfe1e685/package.json",
},
{
id: "as-engine",
label: "Motor da cena Three.js",
url: "https://github.com/evandrodevbr/as-servicos/blob/abeb9946a704139336942bfd41ad163ccfe1e685/lib/three/engine.ts",
},
{
id: "as-canvas",
label: "Canvas e progresso da cena",
url: "https://github.com/evandrodevbr/as-servicos/blob/abeb9946a704139336942bfd41ad163ccfe1e685/components/scene/scene-canvas.tsx",
},
],
},
},
];
/** Projetos feitos para clientes ficam à frente; os pessoais e de estudo vão para o Laboratório. */
export function isClientCase(caseStudy: { id: string }) {
const project = portfolio.projects.find(({ id }) => id === caseStudy.id);
return project ? isClientProject(project) : false;
}
export const clientCases = caseStudies.filter(isClientCase);
export const labCases = caseStudies.filter(
(caseStudy) => !isClientCase(caseStudy),
);
export const serviceHubLinks = servicePages.map((service) => ({
href: `/servicos/${service.slug}`,
label: service.navLabel,
@@ -840,8 +691,16 @@ export function getServicePage(slug: string) {
return servicePages.find((service) => service.slug === slug);
}
/** Cases na ordem de `caseIds`, com os de cliente à frente dos de laboratório. */
export function getRelatedCases(caseIds: string[]) {
return caseStudies.filter((caseStudy) => caseIds.includes(caseStudy.id));
const cases = caseIds.flatMap((id) => {
const caseStudy = caseStudies.find((item) => item.id === id);
return caseStudy ? [caseStudy] : [];
});
return [
...cases.filter(isClientCase),
...cases.filter((caseStudy) => !isClientCase(caseStudy)),
];
}
export function getRelatedServices(caseId: string): ServicePageContent[] {