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`, } 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", }, // perfis profissionais públicos e no ar; a conta do GitHub foi suspensa (404) e o Instagram fica fora por política 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; } /** * Área de atendimento de todo nó `Service` (as ofertas da home e as páginas de serviço): a mesma do texto visível, "Garuva e * Joinville (SC), atendimento remoto em todo o Brasil". Um valor só, para os dados estruturados não divergirem entre páginas. */ const serviceAreaServed = [ { "@type": "City", name: "Garuva" }, { "@type": "City", name: "Joinville" }, { "@type": "AdministrativeArea", name: "Santa Catarina" }, { "@type": "Country", name: "Brasil" }, ] as const; /** * As três ofertas da home como nós `Service`, ligados à pessoa (`provider`) e com a área de atendimento (`serviceAreaServed`). * Nada de `ProfessionalService`, `LocalBusiness` ou catálogo de ofertas: a política * de docs/seo-geo-operations.md os exclui sem entidade ou local comprovado. Sem endereço, 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. * O `url` é a página da oferta (a revisão abre /ia). O `@id` é da home, para não repetir o do nó que a página do serviço emite * com outra descrição. */ export function buildOfferServicesJsonLd() { return serviceOffers.map((offer) => ({ "@type": "Service", "@id": `${siteUrl}/#service-${offer.id}`, name: offer.name, description: offer.summary, url: `${siteUrl}${offer.href}`, provider: { "@id": SEO_IDS.person }, areaServed: serviceAreaServed, })); } 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: serviceAreaServed, } 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 { return { alternates: { canonical: url }, openGraph: { locale: "pt_BR", title, description, url, siteName, type, }, twitter: { card: "summary_large_image", title, description, creator: twitterHandle, }, }; }