Case com mídia ao lado do título e play acessível; problema de cada cliente reescrito com base nos dados; serviços relacionados em 2 cards; FAQ antes do próximo passo e bloco 'Onde já apliquei isso'; Apolo citada em IoT e arquitetura. metaTitle até 43 caracteres e descrições até 155 com teste; JSON-LD CreativeWork; imagens de compartilhamento abaixo de 100 KB; 404 e robots corrigidos; server-only nos módulos com node:.
167 lines
4.2 KiB
TypeScript
167 lines
4.2 KiB
TypeScript
import type { Metadata } from "next";
|
|
import {
|
|
type CaseStudyContent,
|
|
isClientCase,
|
|
type ServicePageContent,
|
|
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",
|
|
},
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/** 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,
|
|
},
|
|
};
|
|
}
|