feat: harden portfolio and add SEO foundations

This commit is contained in:
Evandro Fonseca Junior committed 2026-09-04 19:25:30 -03:00
1 parent b079afdd73
commit 62bcfbd8fa
121 files changed
+7769 -10469

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
export const OG_SIZE = { width: 1200, height: 630 } as const;
export const OG_CONTENT_TYPE = "image/png";
export function OgCard({
eyebrow,
title,
description,
}: {
eyebrow: string;
title: string;
description: string;
}) {
return (
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
background: "#050505",
color: "#f4f1e8",
padding: 72,
fontFamily: "Arial, sans-serif",
}}
>
<div
style={{
display: "flex",
color: "#ccff00",
fontSize: 24,
letterSpacing: "0.16em",
textTransform: "uppercase",
}}
>
{eyebrow}
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 28 }}>
<div
style={{
display: "flex",
fontSize: 64,
fontWeight: 700,
lineHeight: 1.08,
}}
>
{title}
</div>
<div
style={{
display: "flex",
maxWidth: 980,
color: "#c7c3ba",
fontSize: 28,
lineHeight: 1.35,
}}
>
{description}
</div>
</div>
<div
style={{
display: "flex",
borderTop: "1px solid #343434",
paddingTop: 24,
fontSize: 20,
letterSpacing: "0.12em",
}}
>
EVANDRO.DEV.BR / ENGENHARIA DE SOFTWARE
</div>
</div>
);
}
+73 -8
View File
@@ -1,8 +1,8 @@
import { getLiveFeaturedProjects } from "./portfolio-data";
import { getFeaturedProjects, portfolio } from "./portfolio-data";
import type { ProjectHealthSnapshot } from "./project-health";
describe("getLiveFeaturedProjects", () => {
it("returns only featured projects validated in the current snapshot", () => {
describe("getFeaturedProjects", () => {
it("keeps every featured project and classifies demo availability", () => {
const snapshot: ProjectHealthSnapshot = {
checkedAt: "2026-07-10T12:00:00.000Z",
projects: {
@@ -14,11 +14,76 @@ describe("getLiveFeaturedProjects", () => {
},
};
const result = getFeaturedProjects(snapshot);
expect(result.map(({ id }) => id)).toEqual(
portfolio.projects
.filter(({ featured }) => featured !== false)
.map(({ id }) => id),
);
expect(
getLiveFeaturedProjects(
snapshot,
new Date("2026-07-10T12:01:00.000Z"),
).map((project) => project.id),
).toEqual(["sev-modaintima"]);
result.find(({ id }) => id === "sev-modaintima")?.demoAvailability,
).toBe("available");
expect(
result.find(({ id }) => id === "sev-modaintima")?.demoCheckedAt,
).toBe("2026-07-10T12:00:00.000Z");
expect(result.find(({ id }) => id === "twins-lab")?.demoAvailability).toBe(
"unknown",
);
expect(
result.find(({ id }) => id === "twins-lab")?.demoCheckedAt,
).toBeNull();
});
it("excludes an isolated featured false fixture and restores the portfolio", () => {
const originalLength = portfolio.projects.length;
const hiddenProject = {
...portfolio.projects[0],
id: "hidden-test-fixture",
featured: false,
};
portfolio.projects.push(hiddenProject);
try {
expect(
getFeaturedProjects(null).some(
({ id }) => id === "hidden-test-fixture",
),
).toBe(false);
} finally {
portfolio.projects.length = originalLength;
}
expect(portfolio.projects).toHaveLength(originalLength);
});
});
describe("factual portfolio records", () => {
function serializeProject(id: string) {
const project = portfolio.projects.find((item) => item.id === id);
expect(project).toBeDefined();
if (!project) throw new Error(`Missing project ${id}`);
return JSON.stringify(project);
}
it("keeps every public field of the three cases within verified claims", () => {
const nutriscan = serializeProject("nutriscan");
const twins = serializeProject("twins-lab");
const asServicos = serializeProject("as-servicos");
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,
);
expect(twins).toContain("SSE");
expect(twins).not.toMatch(
/WebSockets?|OpenRouter|sub[- ]segundo|produç|preditiv|ROI|precisão|equipamento real/i,
);
expect(asServicos).toContain("Three.js");
expect(asServicos).not.toMatch(
/produç|clientes?|leads?|ranking|performance|resultado comercial|anime\.js|shadcn/i,
);
});
});
+34 -30
View File
@@ -1,12 +1,10 @@
import {
createProjectHealthSnapshot,
filterLiveProjects,
isCurrentLiveCheck,
getDemoAvailability,
type ProjectHealthSnapshot,
} from "./project-health";
describe("project health snapshot", () => {
const now = new Date("2026-07-10T12:00:00.000Z");
const snapshot: ProjectHealthSnapshot = {
checkedAt: "2026-07-10T11:55:00.000Z",
projects: {
@@ -28,36 +26,42 @@ describe("project health snapshot", () => {
},
};
it("accepts only fresh final HTTP 200 HTML projects", () => {
expect(isCurrentLiveCheck(snapshot.projects.online, now)).toBe(true);
expect(isCurrentLiveCheck(snapshot.projects.redirect, now)).toBe(false);
expect(isCurrentLiveCheck(snapshot.projects.stale, now)).toBe(false);
it("classifies demo availability from the latest observation", () => {
expect(getDemoAvailability(null, undefined)).toBe("none");
expect(getDemoAvailability("https://demo.example", undefined)).toBe(
"unknown",
);
expect(
filterLiveProjects(
[
{ id: "online", featured: true },
{ id: "redirect", featured: true },
{ id: "stale", featured: true },
{ id: "hidden", featured: false },
],
snapshot,
now,
).map((project) => project.id),
).toEqual(["online"]);
});
it("fails closed when the snapshot is absent or expired", () => {
getDemoAvailability("https://demo.example", {
status: null,
contentType: null,
checkedAt: "2026-07-10T11:55:00.000Z",
error: "network unavailable",
}),
).toBe("unknown");
expect(
filterLiveProjects([{ id: "online", featured: true }], null, now),
).toEqual([]);
getDemoAvailability("https://demo.example", snapshot.projects.online),
).toBe("available");
expect(
filterLiveProjects(
[{ id: "online", featured: true }],
{ ...snapshot, checkedAt: "2026-07-10T10:00:00.000Z" },
now,
),
).toEqual([]);
getDemoAvailability("https://demo.example", snapshot.projects.redirect),
).toBe("unavailable");
expect(
getDemoAvailability("https://demo.example", {
status: 200,
contentType: "application/json",
checkedAt: "2026-07-10T11:55:00.000Z",
}),
).toBe("unavailable");
expect(
getDemoAvailability("https://demo.example", {
status: 200,
contentType: "text/html+custom; charset=utf-8",
checkedAt: "2026-07-10T11:55:00.000Z",
}),
).toBe("unavailable");
expect(
getDemoAvailability("https://demo.example", snapshot.projects.stale),
).toBe("available");
});
it("creates a fresh health snapshot when the stored result is unavailable", async () => {
+17 -39
View File
@@ -14,45 +14,19 @@ export interface ProjectHealthSnapshot {
projects: Record<string, ProjectHealthCheck>;
}
interface ProjectLike {
id: string;
featured?: boolean;
}
export type DemoAvailability = "available" | "unavailable" | "unknown" | "none";
function isFresh(checkedAt: string, now: Date) {
const timestamp = Date.parse(checkedAt);
return (
Number.isFinite(timestamp) &&
now.getTime() - timestamp <= PROJECT_HEALTH_MAX_AGE_MS
);
}
export function isCurrentLiveCheck(
export function getDemoAvailability(
demoUrl: string | null | undefined,
check: ProjectHealthCheck | undefined,
now = new Date(),
) {
return Boolean(
check &&
check.status === 200 &&
check.contentType?.toLowerCase().startsWith("text/html") &&
isFresh(check.checkedAt, now),
);
}
export function filterLiveProjects<T extends ProjectLike>(
projects: T[],
snapshot: ProjectHealthSnapshot | null,
now = new Date(),
) {
if (!snapshot || !isFresh(snapshot.checkedAt, now)) {
return [] as T[];
}
return projects.filter(
(project) =>
project.featured !== false &&
isCurrentLiveCheck(snapshot.projects[project.id], now),
);
): DemoAvailability {
if (!demoUrl) return "none";
if (!check) return "unknown";
if (check.error || check.status === null) return "unknown";
return check.status === 200 &&
check.contentType?.split(";", 1)[0].trim().toLowerCase() === "text/html"
? "available"
: "unavailable";
}
interface ProjectDemo {
@@ -67,7 +41,10 @@ interface HealthResponse {
body?: { cancel?: () => unknown } | null;
}
type HealthFetcher = (url: string, init: RequestInit) => Promise<HealthResponse>;
type HealthFetcher = (
url: string,
init: RequestInit,
) => Promise<HealthResponse>;
export async function createProjectHealthSnapshot(
projects: ProjectDemo[],
@@ -101,7 +78,8 @@ export async function createProjectHealthSnapshot(
status: null,
contentType: null,
checkedAt,
error: error instanceof Error ? error.message : "Health check failed",
error:
error instanceof Error ? error.message : "Health check failed",
},
] as const;
}
+121
View File
@@ -0,0 +1,121 @@
import sitemap from "../app/sitemap";
import {
buildBreadcrumbList,
buildPersonJsonLd,
buildProfilePageJsonLd,
buildServiceJsonLd,
buildWebSiteJsonLd,
SEO_IDS,
} from "./seo";
import { caseStudies, servicePages, siteName, siteUrl } from "./site-content";
const representativeService = servicePages[0];
if (!representativeService) throw new Error("Missing representative service");
it("uses one canonical identity across website, profile and service", () => {
expect(SEO_IDS).toEqual({
website: `${siteUrl}/#website`,
person: `${siteUrl}/#person`,
profilePage: `${siteUrl}/curriculo#profile-page`,
});
expect(buildWebSiteJsonLd().publisher).toEqual({ "@id": SEO_IDS.person });
expect(buildProfilePageJsonLd()).toMatchObject({
"@id": SEO_IDS.profilePage,
isPartOf: { "@id": SEO_IDS.website },
mainEntity: { "@id": SEO_IDS.person },
});
expect(buildServiceJsonLd(representativeService).provider).toEqual({
"@id": SEO_IDS.person,
});
});
it("keeps the root graph limited to verified website and person fields", () => {
const website = buildWebSiteJsonLd();
const person = buildPersonJsonLd();
expect(Object.keys(website).sort()).toEqual(
["@id", "@type", "inLanguage", "name", "publisher", "url"].sort(),
);
expect(Object.keys(person).sort()).toEqual(
["@id", "@type", "homeLocation", "image", "name", "sameAs", "url"].sort(),
);
expect(website).toMatchObject({
"@type": "WebSite",
"@id": SEO_IDS.website,
url: siteUrl,
name: siteName,
});
expect(person).toMatchObject({
"@type": "Person",
"@id": SEO_IDS.person,
image: `${siteUrl}/edev.jpg`,
homeLocation: { "@type": "Place", name: "Garuva, Santa Catarina, Brasil" },
sameAs: [
"https://github.com/evandrodevbr",
"https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/",
],
});
expect(JSON.stringify([website, person])).not.toMatch(
/ProfessionalService|LocalBusiness|GeoCoordinates|PostalAddress|Instagram|telephone|address|geo|priceRange|OfferCatalog|hasOfferCatalog|offers/i,
);
});
it("builds a canonical service with only the verified service area", () => {
const service = representativeService;
const pageUrl = `${siteUrl}/servicos/${service.slug}`;
expect(buildServiceJsonLd(service)).toEqual({
"@type": "Service",
"@id": `${pageUrl}#service`,
url: pageUrl,
name: service.navLabel,
description: service.metaDescription,
provider: { "@id": SEO_IDS.person },
areaServed: [
{ "@type": "City", name: "Garuva" },
{ "@type": "AdministrativeArea", name: "Norte de Santa Catarina" },
],
});
});
it("builds contiguous breadcrumbs from readonly tuples", () => {
const pageUrl = `${siteUrl}/servicos/x`;
const items = [
["Início", siteUrl],
["Serviços", `${siteUrl}/servicos`],
["X", pageUrl],
] as const;
const breadcrumb = buildBreadcrumbList(pageUrl, items);
expect(breadcrumb["@id"]).toBe(`${pageUrl}#breadcrumb`);
expect(breadcrumb.itemListElement).toEqual([
{ "@type": "ListItem", position: 1, name: "Início", item: siteUrl },
{
"@type": "ListItem",
position: 2,
name: "Serviços",
item: `${siteUrl}/servicos`,
},
{ "@type": "ListItem", position: 3, name: "X", item: pageUrl },
]);
});
it("keeps profile data timeless and sitemap output deterministic", () => {
expect(buildProfilePageJsonLd()).not.toHaveProperty("dateModified");
const expectedUrls = [
siteUrl,
`${siteUrl}/curriculo`,
`${siteUrl}/servicos`,
`${siteUrl}/cases`,
...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(second).toEqual(first);
expect(first).toHaveLength(16);
});
+83
View File
@@ -0,0 +1,83 @@
import { type ServicePageContent, siteName, siteUrl } from "./site-content";
export const SEO_IDS = {
website: `${siteUrl}/#website`,
person: `${siteUrl}/#person`,
profilePage: `${siteUrl}/curriculo#profile-page`,
} as const;
export function buildWebSiteJsonLd() {
return {
"@type": "WebSite",
"@id": SEO_IDS.website,
url: siteUrl,
name: siteName,
inLanguage: "pt-BR",
publisher: { "@id": SEO_IDS.person },
} as const;
}
export function buildPersonJsonLd() {
return {
"@type": "Person",
"@id": SEO_IDS.person,
name: "Evandro Fonseca Junior",
url: siteUrl,
image: `${siteUrl}/edev.jpg`,
homeLocation: {
"@type": "Place",
name: "Garuva, Santa Catarina, Brasil",
},
sameAs: [
"https://github.com/evandrodevbr",
"https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/",
],
} as const;
}
export function buildProfilePageJsonLd() {
return {
"@type": "ProfilePage",
"@id": SEO_IDS.profilePage,
url: `${siteUrl}/curriculo`,
inLanguage: "pt-BR",
isPartOf: { "@id": SEO_IDS.website },
mainEntity: { "@id": SEO_IDS.person },
} as const;
}
export function buildServiceJsonLd(service: ServicePageContent) {
const pageUrl = `${siteUrl}/servicos/${service.slug}`;
return {
"@type": "Service",
"@id": `${pageUrl}#service`,
url: pageUrl,
name: service.navLabel,
description: service.metaDescription,
provider: { "@id": SEO_IDS.person },
areaServed: [
{ "@type": "City", name: "Garuva" },
{
"@type": "AdministrativeArea",
name: "Norte de Santa Catarina",
},
],
} as const;
}
export function buildBreadcrumbList(
pageUrl: string,
items: readonly (readonly [name: string, url: string])[],
) {
return {
"@type": "BreadcrumbList",
"@id": `${pageUrl}#breadcrumb`,
itemListElement: items.map(([name, item], index) => ({
"@type": "ListItem" as const,
position: index + 1,
name,
item,
})),
} as const;
}
+100
View File
@@ -0,0 +1,100 @@
import { caseStudies, getRelatedServices, servicePages } from "./site-content";
const detailedIds = ["as-servicos", "nutriscan", "twins-lab"] as const;
const immutableGitHubBlobUrl =
/^https:\/\/github\.com\/[^/]+\/[^/]+\/blob\/[0-9a-f]{40}\/.+$/;
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;
}
it("limits detailed content to the three evidenced cases", () => {
expect(
caseStudies
.filter(({ detail }) => detail !== undefined)
.map(({ id }) => id)
.sort(),
).toEqual([...detailedIds].sort());
});
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("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("derives the exact related service set from the existing relationship", () => {
expect(
getRelatedServices("twins-lab")
.map(({ slug }) => slug)
.sort(),
).toEqual(
[
"consultoria-nextjs",
"integracao-hardware-software-iot",
"arquitetura-de-software",
].sort(),
);
});
it("gives every service a direct answer and consulting CTA", () => {
for (const service of servicePages) {
expect(service.answer.trim().length).toBeGreaterThan(40);
expect(service.ctaIntent).toBe("consulting");
expect(service.ctaHref).toBe("/#contact");
}
});
it("resolves every declared related case ID", () => {
const caseIds = new Set(caseStudies.map(({ id }) => id));
const unresolved = servicePages.flatMap(({ slug, relatedCaseIds }) =>
relatedCaseIds
.filter((caseId) => !caseIds.has(caseId))
.map((caseId) => ({ slug, caseId })),
);
expect(unresolved).toEqual([]);
});
+273 -45
View File
@@ -1,6 +1,19 @@
export const siteUrl = "https://evandro.dev.br";
export const siteName = "Evandro.dev.br";
export const intentCtas = {
hiring: {
label: "Avaliar perfil para contratação",
href: "/curriculo",
},
consulting: {
label: "Solicitar consultoria técnica",
href: "/#contact",
},
} as const;
export type CtaIntent = keyof typeof intentCtas;
export const regionCities = [
"Garuva",
"Joinville",
@@ -12,6 +25,25 @@ export const regionCities = [
"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;
@@ -20,10 +52,12 @@ export interface ServicePageContent {
metaDescription: string;
headline: string;
intro: string;
answer: string;
audience: string[];
outcomes: string[];
process: string[];
stack: string[];
ctaIntent: "consulting";
ctaLabel: string;
ctaHref: string;
relatedCaseIds: string[];
@@ -39,6 +73,7 @@ export interface CaseStudyContent {
stack: string[];
demoUrl?: string | null;
githubUrl?: string | null;
detail?: CaseStudyDetail;
}
export const servicePages: ServicePageContent[] = [
@@ -46,16 +81,17 @@ export const servicePages: ServicePageContent[] = [
slug: "criacao-de-sites",
navLabel: "Criação de Sites",
eyebrow: "Presença digital",
seoTitle: "Criação de Sites em Garuva, Joinville e Curitiba",
seoTitle: "Criação de Sites e Sistemas Web com Next.js",
metaDescription:
"Criação de sites institucionais e sistemas web para empresas em Garuva, Joinville, São Francisco do Sul, Itapoá, Jaraguá do Sul e Curitiba. Sites rápidos, responsivos e otimizados para SEO com Next.js.",
headline:
"Criação de sites institucionais e sistemas web para empresas de Garuva, Joinville e Curitiba.",
"Criação de sites institucionais e sistemas web responsivos, com acessibilidade, performance e SEO técnico considerados desde a implementação.",
headline: "Criação de sites institucionais e sistemas web com Next.js.",
answer:
"Crio sites institucionais e sistemas web em Next.js, com escopo definido por objetivos, páginas, integrações e requisitos de acessibilidade, performance e SEO técnico. Base em Garuva, atendimento a projetos no Norte de Santa Catarina e atuação remota.",
intro:
"Desenvolvo sites e sistemas web rápidos, responsivos e com SEO técnico desde a primeira entrega, para empresas de Garuva, Joinville, Norte de Santa Catarina e Curitiba que precisam aparecer bem nas buscas locais e converter visitantes em clientes.",
"Desenvolvo sites e sistemas web responsivos com estrutura técnica para indexação e evolução do produto, sempre dentro do escopo acordado para conteúdo e integrações.",
audience: [
"Empresas e profissionais em Garuva, Joinville e região que ainda não têm site ou precisam trocar um site lento e desatualizado.",
"Negócios em Curitiba e no Norte de Santa Catarina que querem aparecer melhor no Google para buscas da própria cidade e região.",
"Empresas e profissionais que ainda não têm site ou precisam substituir uma experiência lenta e desatualizada.",
"Negócios que precisam organizar conteúdo e fundamentos técnicos para busca orgânica sem criar páginas locais artificiais.",
"Marcas que precisam de um site institucional rápido, com boa performance e pronto para escalar para um sistema web completo.",
],
outcomes: [
@@ -71,14 +107,15 @@ export const servicePages: ServicePageContent[] = [
"Publicação, configuração de domínio e orientação sobre indexação no Google.",
],
stack: ["Next.js", "TypeScript", "Tailwind CSS", "SEO técnico", "Vercel"],
ctaLabel: "Quero um site para minha empresa",
ctaIntent: "consulting",
ctaLabel: "Conversar sobre o projeto",
ctaHref: "/#contact",
relatedCaseIds: ["sc-plus", "nutriscan"],
faq: [
{
question: "Quanto custa um site profissional?",
answer:
"O custo de um site profissional depende do escopo: número de páginas, funcionalidades, integrações e exigências de SEO técnico e performance. Um site institucional enxuto custa menos do que um sistema web completo. Como desenvolvedor de sites em Joinville e região, eu prefiro definir o orçamento depois de entender o objetivo do projeto, em uma conversa inicial sem compromisso.",
"O custo de um site profissional depende do escopo: número de páginas, funcionalidades, integrações e exigências de SEO técnico e performance. Um site institucional enxuto custa menos do que um sistema web completo. Eu prefiro definir o orçamento depois de entender o objetivo do projeto, em uma conversa inicial sem compromisso.",
},
{
question: "Site sob medida ou WordPress: qual escolher?",
@@ -91,9 +128,9 @@ export const servicePages: ServicePageContent[] = [
"Sim. Além do lançamento, eu estruturo projetos pensando na manutenção do site: atualizações de dependências, ajustes de conteúdo, monitoramento de performance e evolução incremental de funcionalidades. Um site profissional não termina na publicação; ele precisa continuar rápido, seguro e bem posicionado. Por isso a entrega inclui orientação sobre indexação no Google e uma base de código organizada, fácil de evoluir.",
},
{
question: "Você atende empresas só de Joinville ou de outras cidades?",
question: "Como funciona o atendimento local e remoto?",
answer:
"Eu atendo empresas em Garuva, Joinville, São Francisco do Sul, Itapoá, Jaraguá do Sul, Balneário Barra do Sul, Barra Velha e Curitiba, além de projetos remotos em todo o Brasil. O trabalho funciona igual nos dois formatos: conversa inicial, levantamento de escopo e entregas acompanhadas de perto. Para buscas locais, cada site já sai com SEO técnico pensado para a cidade e o segmento do cliente.",
"Base em Garuva, atendimento a projetos no Norte de Santa Catarina e atuação remota. O trabalho segue conversa inicial, levantamento de escopo e entregas acompanhadas de perto; qualquer estratégia local é definida a partir da atuação real de cada negócio.",
},
],
},
@@ -106,8 +143,10 @@ export const servicePages: ServicePageContent[] = [
"Engenharia de software para evolução de produtos, integrações complexas e sistemas críticos. Entrega ponta a ponta com foco em arquitetura clara, estabilidade e velocidade de execução.",
headline:
"Engenharia de software para evoluir produto sem aumentar o caos técnico.",
answer:
"Atuo no diagnóstico, arquitetura, implementação e sustentação de produtos e sistemas, com entregáveis e limites definidos pelo risco técnico e pelo recorte acordado. Base em Garuva, atendimento a projetos no Norte de Santa Catarina e atuação remota.",
intro:
"Atuo em discovery, arquitetura, implementação e sustentação de software para empresas de Joinville, Curitiba e de todo o Brasil que precisam entregar com previsibilidade, sem sacrificar clareza técnica nem operação.",
"Atuo em discovery, arquitetura, implementação e sustentação de software para equipes que precisam entregar com previsibilidade, sem sacrificar clareza técnica nem operação.",
audience: [
"Times que já tem produto em operação e precisam voltar a ganhar velocidade.",
"Empresas que sofrem com integrações, retrabalho e backlog técnico acumulado.",
@@ -126,7 +165,8 @@ export const servicePages: ServicePageContent[] = [
"Handoff ou continuidade com documentação e próximo ciclo recomendado.",
],
stack: ["Next.js", "TypeScript", "Node.js", "PostgreSQL", "APIs REST"],
ctaLabel: "Agendar conversa sobre engenharia",
ctaIntent: "consulting",
ctaLabel: "Conversar sobre o projeto",
ctaHref: "/#contact",
relatedCaseIds: ["nutriscan", "reddit-explorer", "sc-plus"],
faq: [
@@ -162,6 +202,8 @@ export const servicePages: ServicePageContent[] = [
"Consultoria Next.js para App Router, SEO técnico, performance, arquitetura e evolução de produto. Ideal para projetos que precisam escalar sem perder clareza e Core Web Vitals.",
headline:
"Consultoria Next.js para produto, performance e SEO técnico sem gambiarra.",
answer:
"A consultoria Next.js cobre App Router, separação entre server e client, metadata, performance e arquitetura, com diagnóstico e implementação limitados ao escopo combinado. Base em Garuva, atendimento a projetos no Norte de Santa Catarina e atuação remota.",
intro:
"Ajudo equipes a usar Next.js com mais critério arquitetural, melhor separação entre server e client, e foco real em performance, indexação e experiência de produto.",
audience: [
@@ -182,7 +224,8 @@ export const servicePages: ServicePageContent[] = [
"Validação final com build, semântica e consistência entre páginas.",
],
stack: ["Next.js", "React", "TypeScript", "Tailwind CSS", "SEO técnico"],
ctaLabel: "Solicitar consultoria Next.js",
ctaIntent: "consulting",
ctaLabel: "Solicitar consultoria",
ctaHref: "/#contact",
relatedCaseIds: ["nutriscan", "twins-lab", "sc-plus"],
faq: [
@@ -202,9 +245,9 @@ export const servicePages: ServicePageContent[] = [
"Depende do formato de trabalho: uma auditoria pontual de arquitetura e performance tem peso diferente de um acompanhamento contínuo ou da implementação completa das correções. O que define o investimento é o tamanho da base, o número de rotas críticas e a urgência dos gargalos. Depois de uma leitura inicial do projeto, eu consigo propor um escopo com expectativa clara de esforço.",
},
{
question: "Você atende times remotos ou só presencial?",
question: "Como funciona a consultoria remota?",
answer:
"Sim, atendo equipes em Joinville, Curitiba e em qualquer cidade do Brasil, presencial ou remoto. A consultoria funciona bem à distância: auditoria documentada, sessões de alinhamento com o time e implementação guiada nas partes críticas. Para empresas da região Norte de Santa Catarina e de Curitiba, também há a opção de encontros presenciais nos momentos de diagnóstico e validação.",
"Base em Garuva, atendimento a projetos no Norte de Santa Catarina e atuação remota. A consultoria à distância combina auditoria documentada, sessões de alinhamento com o time e implementação guiada nas partes críticas do escopo.",
},
],
},
@@ -217,6 +260,8 @@ export const servicePages: ServicePageContent[] = [
"Desenvolvimento com Tauri e Rust para apps desktop mais leves, seguros e eficientes. Consultoria técnica para produtos que precisam de performance, integração local e distribuição confiável.",
headline:
"Tauri e Rust para apps desktop mais leves, seguros e prontos para operar.",
answer:
"Desenvolvo e avalio aplicativos desktop com Tauri, Rust e interface web quando integração local, distribuição e controle de recursos justificam essa arquitetura. Base em Garuva, atendimento a projetos no Norte de Santa Catarina e atuação remota.",
intro:
"Uso Tauri e Rust quando o produto precisa de executável desktop com footprint menor, melhor controle do ambiente local e mais previsibilidade do que stacks mais pesadas.",
audience: [
@@ -243,7 +288,8 @@ export const servicePages: ServicePageContent[] = [
"Desktop apps",
"System integration",
],
ctaLabel: "Conversar sobre Tauri e Rust",
ctaIntent: "consulting",
ctaLabel: "Conversar sobre o projeto",
ctaHref: "/#contact",
relatedCaseIds: ["gcc-installer"],
faq: [
@@ -263,10 +309,9 @@ export const servicePages: ServicePageContent[] = [
"Rust tem curva de aprendizado real, mas a manutenção tende a ficar mais fácil no longo prazo: o compilador elimina categorias inteiras de bugs antes da produção. Para times pequenos, a estratégia é usar Rust apenas na camada que justifica o investimento, como integração com o sistema, performance e segurança, e manter a interface em TypeScript. Assim a complexidade fica onde traz retorno.",
},
{
question:
"Você desenvolve apps desktop para empresas em Santa Catarina?",
question: "Como funciona o atendimento para aplicativos desktop?",
answer:
"Sim. Eu desenvolvo aplicativos desktop para empresas em Joinville, Curitiba e em todo o Brasil, com atendimento presencial ou remoto. O fluxo cobre da arquitetura ao empacotamento: definição do papel de Rust, implementação dos fluxos críticos, estratégia de distribuição e atualização do executável. Ferramentas internas que precisam rodar em máquinas da operação são um caso muito comum nesse formato de trabalho.",
"Base em Garuva, atendimento a projetos no Norte de Santa Catarina e atuação remota. O fluxo cobre arquitetura, definição do papel de Rust, implementação dos fluxos críticos e estratégia de distribuição e atualização do executável.",
},
],
},
@@ -279,6 +324,8 @@ export const servicePages: ServicePageContent[] = [
"Integração hardware + software, IoT e fluxos conectados para produtos e operações críticas. Da coleta de dados ao painel, com APIs, telemetria e arquitetura de software confiável.",
headline:
"Integração hardware + software para produtos conectados que precisam operar de verdade.",
answer:
"Estruturo integrações entre dispositivos, backend, banco e interfaces, com contratos, tratamento de falhas e documentação definidos para o fluxo necessário. Base em Garuva, atendimento a projetos no Norte de Santa Catarina e atuação remota.",
intro:
"Atuo na camada que costuma quebrar primeiro: a fronteira entre dispositivo, backend, painel e operação. Estruturo esses fluxos para que dados, comando e observabilidade funcionem como produto, não como remendo.",
audience: [
@@ -305,7 +352,8 @@ export const servicePages: ServicePageContent[] = [
"Dashboards",
"Integração de sistemas",
],
ctaLabel: "Falar sobre integração e IoT",
ctaIntent: "consulting",
ctaLabel: "Conversar sobre o projeto",
ctaHref: "/#contact",
relatedCaseIds: ["twins-lab"],
faq: [
@@ -322,12 +370,12 @@ export const servicePages: ServicePageContent[] = [
{
question: "Vale a pena investir em IoT na indústria?",
answer:
"Vale quando existe dado físico hoje desperdiçado: leitura manual, planilha isolada, decisão por estimativa. Projetos de IoT industriais transformam esse dado em telemetria e painéis que sustentam decisão em tempo real, especialmente em parques fabris como o de Joinville e região. O investimento só compensa, porém, quando a integração é tratada como produto, com observabilidade e manutenção previstas, e não como remendo pontual.",
"Vale quando existe dado físico hoje desperdiçado: leitura manual, planilha isolada ou decisão por estimativa. Projetos de IoT podem transformar esse dado em telemetria e painéis, mas o investimento só compensa quando a integração é tratada como produto, com observabilidade e manutenção previstas.",
},
{
question: "Como funciona um projeto de integração com você?",
answer:
"O projeto começa com o mapeamento de sinal, eventos, integrações existentes e pontos de falha. Em seguida eu defino os contratos entre hardware, backend e interfaces, implemento o fluxo com testes e tratamento das exceções relevantes, e entrego com documentação de operação, limites conhecidos e próximo passo recomendado. Atendo indústrias e empresas de tecnologia em Joinville, Curitiba e remotamente.",
"O projeto começa com o mapeamento de sinal, eventos, integrações existentes e pontos de falha. Em seguida eu defino os contratos entre hardware, backend e interfaces, implemento o fluxo com testes e tratamento das exceções relevantes, e entrego com documentação de operação, limites conhecidos e próximo passo recomendado. Base em Garuva, atendimento a projetos no Norte de Santa Catarina e atuação remota.",
},
],
},
@@ -340,6 +388,8 @@ export const servicePages: ServicePageContent[] = [
"Arquitetura de software para reduzir complexidade, destravar evolução e organizar integrações críticas. Consultoria técnica para produtos que precisam de direção antes de mais código.",
headline:
"Arquitetura de software para times que precisam de clareza antes de acelerar.",
answer:
"Realizo diagnóstico de arquitetura com leitura da base, fluxos e integrações, seguido por riscos, trade-offs e um plano de transição executável para o escopo analisado. Base em Garuva, atendimento a projetos no Norte de Santa Catarina e atuação remota.",
intro:
"Quando o problema não é falta de código, mas excesso de acoplamento, retrabalho e decisão mal distribuída, a arquitetura certa devolve velocidade, leitura e confiança ao time.",
audience: [
@@ -366,7 +416,8 @@ export const servicePages: ServicePageContent[] = [
"Integrações",
"Sistemas críticos",
],
ctaLabel: "Solicitar diagnóstico arquitetural",
ctaIntent: "consulting",
ctaLabel: "Solicitar diagnóstico",
ctaHref: "/#contact",
relatedCaseIds: ["nutriscan", "twins-lab", "reddit-explorer"],
faq: [
@@ -386,9 +437,9 @@ export const servicePages: ServicePageContent[] = [
"Eu leio a base de código, os fluxos, as integrações e as dores do time, e devolvo um diagnóstico escrito com pontos de risco, oportunidades e trade-offs explícitos. A partir dele vem a proposta de arquitetura alvo e um plano de transição realista, priorizado por impacto de negócio. O formato funciona tanto para times internos quanto para fundadores técnicos conduzindo a própria evolução.",
},
{
question: "Você faz consultoria de arquitetura remota ou presencial?",
question: "Como funciona a consultoria de arquitetura remota?",
answer:
"Dos dois jeitos. Atendo empresas em Joinville, Curitiba e em todo o Brasil, com diagnóstico e acompanhamento funcionando bem à distância; encontros presenciais ficam para os momentos de maior densidade, como kickoff e validação. Como a entrega central é um diagnóstico escrito e um plano executável, o formato remoto não reduz a profundidade da análise nem a qualidade do handoff para o time.",
"Base em Garuva, atendimento a projetos no Norte de Santa Catarina e atuação remota. O diagnóstico e o acompanhamento funcionam à distância com leitura técnica, alinhamentos e handoff documentado para o time responsável.",
},
],
},
@@ -398,26 +449,152 @@ export const caseStudies: CaseStudyContent[] = [
{
id: "nutriscan",
title: "NutriScan",
subtitle: "PWA com Next.js para análise nutricional e operação offline",
subtitle: "Consulta de produtos por nome e código de barras",
summary:
"Projeto em Next.js 14 com App Router, histórico de uso, scanner de código de barras e cache estratégico para reduzir dependência de rede.",
"Aplicação web para consultar produtos alimentícios por nome ou código de barras, combinando cache JSON com a API Open Food Facts.",
impact:
"Bom exemplo de engenharia de software orientada a produto, performance e experiência de uso em contexto real.",
stack: ["Next.js 14", "TypeScript", "MongoDB", "PWA", "Tailwind CSS"],
"Reduzir a dependência da API externa e manter um caminho explícito para buscas e consulta por código.",
stack: ["Next.js", "TypeScript", "Cache JSON", "Open Food Facts"],
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: "Plataforma IoT industrial com dados em tempo real",
subtitle: "Protótipo de gêmeo digital industrial com SSE",
summary:
"Produto com dashboards, autenticação, WebSockets e camada de IA para monitoramento de dispositivos e operação industrial conectada.",
"Protótipo de gêmeo digital industrial com autenticação, simulador de reator, persistência MongoDB e streaming de eventos do servidor.",
impact:
"Case forte para integração hardware + software, telemetria e sistemas críticos com atualização em tempo real.",
stack: ["Next.js", "TypeScript", "NextAuth", "WebSockets", "OpenRouter AI"],
"Expor estados simulados do reator a uma interface autenticada e manter histórico/status do simulador.",
stack: ["Next.js", "TypeScript", "NextAuth", "MongoDB", "SSE"],
demoUrl: "https://twins.evandro.dev.br",
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",
},
],
},
},
{
id: "reddit-explorer",
@@ -457,21 +634,66 @@ export const caseStudies: CaseStudyContent[] = [
{
id: "as-servicos",
title: "AS Serviços",
subtitle: "Site institucional multidisciplinar com cena 3D procedural",
subtitle: "Site institucional com cena 3D procedural",
summary:
"Site institucional em Next.js 16 com cena 3D procedural em Three.js: uma casa wireframe que se transforma em placa-mãe conforme o scroll. Quatro áreas de engenharia — civil, elétrica, eletrônica/automação e software — sob o mesmo teto.",
"Site institucional em Next.js 16 que representa quatro frentes de engenharia com uma cena Three.js gerada em código: uma casa wireframe e uma placa-mãe ligadas ao progresso da rolagem.",
impact:
"Demonstra engenharia de software aplicada a presença digital: cena 3D gerada em runtime sem assets externos, animação dirigida por scroll e código 100% tipado.",
stack: [
"Next.js 16",
"React 19",
"Three.js",
"TypeScript",
"Tailwind CSS 4",
"shadcn/ui",
],
"Representar engenharia física e software na mesma narrativa visual sem depender de um modelo 3D pronto.",
stack: ["Next.js 16", "Three.js", "TypeScript"],
demoUrl: "https://asservicos.com",
githubUrl: "https://github.com/evandrodevbr/as-servicos",
detail: {
answer: {
text: "Site institucional em Next.js 16 que representa quatro frentes de engenharia com uma 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",
},
],
},
},
];
@@ -488,3 +710,9 @@ export function getServicePage(slug: string) {
export function getRelatedCases(caseIds: string[]) {
return caseStudies.filter((caseStudy) => caseIds.includes(caseStudy.id));
}
export function getRelatedServices(caseId: string): ServicePageContent[] {
return servicePages.filter(({ relatedCaseIds }) =>
relatedCaseIds.includes(caseId),
);
}
-99
View File
@@ -1,99 +0,0 @@
import { useState, useEffect } from "react";
interface PortfolioData {
personal: {
name: string;
title: string;
location: string;
email: string;
phone: string;
linkedin: string;
github: string;
summary: string;
};
experiences: Array<{
id: string;
title: string;
company: string;
period: string;
location: string;
type: string;
description: string;
achievements: string[];
technologies: string[];
featured: boolean;
}>;
projects: Array<{
id: string;
title: string;
subtitle: string;
description: string;
longDescription: string;
technologies: string[];
features: string[];
status: string;
demoUrl: string;
githubUrl: string;
imageUrl: string;
badges: string[];
featured: boolean;
category: string;
metrics?: {
complexity: number;
teamSize?: string;
};
}>;
skills: {
technical: Array<{
category: string;
icon: string;
color: string;
bgColor: string;
skills: string[];
}>;
soft: Array<{
skill: string;
level: number;
}>;
};
contact: {
email: string;
phone: string;
whatsapp: string;
linkedin: string;
github: string;
location: string;
};
social: Array<{
name: string;
url: string;
icon: string;
}>;
}
export function usePortfolioData() {
const [data, setData] = useState<PortfolioData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadData = async () => {
try {
const response = await fetch("/portfolio-data.json");
if (!response.ok) {
throw new Error("Failed to load portfolio data");
}
const portfolioData: PortfolioData = await response.json();
setData(portfolioData);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setLoading(false);
}
};
loadData();
}, []);
return { data, loading, error };
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { clsx, type ClassValue } from "clsx";
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {