Files
portifolio-evandrodevbr/lib/seo.ts
T
Evandro Fonseca Junior fc44a69fb3 feat(servicos): ofertas no topo de /servicos, capa por assunto e diagramas
/servicos abre com as 3 ofertas e depois outros serviços; H1 de criação de sites e de engenharia com o nome da oferta; capa de cada serviço pelo assunto (SEV fora do próprio case cai de ~11 para 2 imagens); diagrama de fluxo em SVG nos serviços sem case; cases com o que o cliente passou a poder fazer e contato logo depois de Como ficou; FAQ num componente só; lastmod por rota; ProfessionalService com areaServed.
2026-10-14 19:14:46 -03:00

206 lines
5.7 KiB
TypeScript

import type { Metadata } from "next";
import {
type CaseStudyContent,
isClientCase,
type ServicePageContent,
serviceOffers,
siteName,
siteUrl,
} from "./site-content";
export const twitterHandle = "@evandrodevbr";
export const SEO_IDS = {
website: `${siteUrl}/#website`,
person: `${siteUrl}/#person`,
profilePage: `${siteUrl}/curriculo#profile-page`,
professionalService: `${siteUrl}/#professional-service`,
} 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`,
// cargo e áreas vêm de public/portfolio-data.json e estão visíveis em /curriculo e na seção de skills
jobTitle: "Engenheiro da Computação e Desenvolvedor de Software",
knowsAbout: [
"Next.js",
"React",
"TypeScript",
"Node.js",
"Python",
"IoT",
"Computação em nuvem",
"Inteligência artificial",
],
homeLocation: {
"@type": "Place",
name: "Garuva, Santa Catarina, Brasil",
},
sameAs: ["https://www.linkedin.com/in/evandrodevbr/"],
} 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;
}
/**
* O negócio como `ProfessionalService`: onde atende (Garuva, Joinville, Santa Catarina e o Brasil, de forma remota) e as três ofertas
* da home como catálogo. Sem endereço de rua, preço, prazo, telefone ou e-mail: o que não está publicado no site não entra nos
* dados estruturados. Só o construtor: quem o coloca na página é a home.
*/
export function buildProfessionalServiceJsonLd() {
return {
"@type": "ProfessionalService",
"@id": SEO_IDS.professionalService,
name: siteName,
url: siteUrl,
description:
"Sites, sistemas web e aplicativos sob medida, e revisão de sistemas que já existem, inclusive feitos com IA. Garuva e Joinville (SC), remoto em todo o Brasil.",
founder: { "@id": SEO_IDS.person },
areaServed: [
{ "@type": "City", name: "Garuva" },
{ "@type": "City", name: "Joinville" },
{ "@type": "AdministrativeArea", name: "Santa Catarina" },
{ "@type": "Country", name: "Brasil" },
],
hasOfferCatalog: {
"@type": "OfferCatalog",
name: "Serviços de desenvolvimento de software",
itemListElement: serviceOffers.map((offer) => ({
"@type": "Offer",
itemOffered: {
"@type": "Service",
name: offer.name,
description: offer.summary,
url: `${siteUrl}${offer.href}`,
},
})),
},
} 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": "City", name: "Joinville" },
{
"@type": "AdministrativeArea",
name: "Norte de Santa Catarina",
},
],
} as const;
}
/** Imagem de compartilhamento do case: a mesma que as redes recebem em `og:image` (rota opengraph-image do case). */
export function getCaseImageUrl(id: string) {
return `${siteUrl}/cases/${id}/opengraph-image/${id}`;
}
/** Trabalho do case como `CreativeWork`: autor ligado à pessoa e, nos projetos de cliente, o cliente em nome de quem foi feito. */
export function buildCaseJsonLd(caseStudy: CaseStudyContent) {
const pageUrl = `${siteUrl}/cases/${caseStudy.id}`;
return {
"@type": "CreativeWork",
"@id": `${pageUrl}#creative-work`,
name: `${caseStudy.title}: ${caseStudy.subtitle}`,
description: caseStudy.metaDescription,
url: pageUrl,
image: getCaseImageUrl(caseStudy.id),
inLanguage: "pt-BR",
creator: { "@id": SEO_IDS.person },
...(isClientCase(caseStudy)
? {
sourceOrganization: {
"@type": "Organization",
name: caseStudy.title,
...(caseStudy.demoUrl ? { url: caseStudy.demoUrl } : {}),
},
}
: {}),
keywords: caseStudy.stack.join(", "),
};
}
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;
}
/**
* 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,
},
};
}