Files

194 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`,
} 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<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,
},
};
}