Files
ESPDocs/lib/seo.js
T
Lucas RayanandAntigravity 0d5f8f9c5f
CI / Lint e auditorias (push) Successful in 45s
CI / Trivy (dependências e segredos) (push) Successful in 11s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m23s
fix(i18n): localize json-ld metadata and eliminate breadcrumb leaks
- Pass currentLocale to jsonLd.serie and jsonLd.framework to ensure English headline, name, and properties
- Localize JSON-LD @id anchors to #article, #about, #glossary, #organization, and #author for English routes
- Pass isEn prop to PageHero across all internal pages to prevent breadcrumbs from defaulting to Portuguese
- Add scripts/verifica-vazamentos.py to smoke-imagem.sh to permanently test all 36 English URLs (visible text and ld+json)

Co-Authored-By: Antigravity <noreply@google.com>
2026-11-01 17:42:36 -03:00

424 lines
13 KiB
JavaScript

/**
* Fonte única das informações de SEO do ESPDocs.
*
* Centralizar aqui evita o problema clássico de metadata espalhado: a URL do
* site aparecendo com valores diferentes em cada arquivo, ou uma página nova
* nascendo sem título próprio.
*/
import { ENABLED_LOCALES, getLocalizedHref } from "@/i18n/links";
export const SITE = {
url: "https://espdocs.cienciaembarcada.com.br",
nome: "ESPDocs",
autor: "Lucas Rayan Guerra",
autorUrl: "https://lucasrguerra.dev.br",
autorLinkedin: "https://linkedin.com/in/lucasrguerra",
autorEmail: "contato@lucasrguerra.dev.br",
repositorio: "https://git.lucasrguerra.dev.br/lucasrguerra/ESPDocs",
blog: "https://cienciaembarcada.com.br",
livroTitulo: "Microcontroladores ESP32",
livroSubtitulo: "Conectividade, Eficiência e Desempenho",
livroIsbn: "978-65-02-43432-1",
livroCapa: "/livro/capa.jpg",
livroAmazon: "https://a.co/d/0fdCvzlg",
livroUiclap: "https://loja.uiclap.com/titulo/ua213708",
};
/**
* Monta o objeto `metadata` de uma página, já com canonical e Open Graph.
* Usar isto em vez de escrever o objeto à mão garante que nenhuma página fique
* sem canonical ou sem og:title.
*/
export function paginaMeta({ titulo, descricao, caminho, keywords = [], tipo = "website", locale = "pt" }) {
const currentLocale = locale || "pt";
const basePath = caminho.startsWith("/") ? caminho : `/${caminho}`;
const canonicalPath = getLocalizedHref(`/pt${basePath}`, currentLocale);
const url = `${SITE.url}${canonicalPath}`;
const languages = {};
for (const loc of ENABLED_LOCALES) {
if (basePath === "/livro" && loc !== "pt") continue;
const locHref = getLocalizedHref(`/pt${basePath}`, loc);
const langKey = loc === "pt" ? "pt-BR" : loc;
languages[langKey] = `${SITE.url}${locHref}`;
}
if (ENABLED_LOCALES.includes("en") && basePath !== "/livro") {
languages["x-default"] = `${SITE.url}${getLocalizedHref(`/pt${basePath}`, "en")}`;
}
const ogLocaleMap = {
pt: "pt_BR",
en: "en_US",
es: "es_419",
fr: "fr_FR",
de: "de_DE",
"zh-CN": "zh_CN",
};
const alternateLocales = ENABLED_LOCALES
.filter((l) => l !== currentLocale && (basePath !== "/livro" || l === "pt"))
.map((l) => ogLocaleMap[l])
.filter(Boolean);
return {
title: titulo,
description: descricao,
keywords,
alternates: {
canonical: canonicalPath,
languages: Object.keys(languages).length > 1 ? languages : undefined,
},
openGraph: {
type: tipo,
locale: ogLocaleMap[currentLocale] || "pt_BR",
alternateLocale: alternateLocales.length > 0 ? alternateLocales : undefined,
url,
siteName: SITE.nome,
title: `${titulo} · ${SITE.nome}`,
description: descricao,
},
twitter: {
card: "summary_large_image",
title: `${titulo} · ${SITE.nome}`,
description: descricao,
},
};
}
/* ------------------------------------------------------------------------
Dados estruturados (JSON-LD). São eles que rendem os resultados ricos:
trilha de navegação, box de busca e reconhecimento de autoria.
------------------------------------------------------------------------ */
export const jsonLd = {
siteESociedade(locale = "pt") {
const isEn = locale === "en";
return {
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebSite",
"@id": `${SITE.url}/#site`,
url: `${SITE.url}/${locale}`,
name: SITE.nome,
inLanguage: isEn ? "en" : "pt-BR",
description: isEn
? "ESP32 ecosystem documentation: specifications, pinouts, comparison, and series selection."
: "Documentação do ecossistema ESP32 em português brasileiro: especificações, pinagem, comparação e seleção de séries.",
publisher: { "@id": `${SITE.url}/#${isEn ? "organization" : "organizacao"}` },
},
{
"@type": "Organization",
"@id": `${SITE.url}/#${isEn ? "organization" : "organizacao"}`,
name: "Ciência Embarcada",
url: SITE.blog,
logo: {
"@type": "ImageObject",
url: `${SITE.url}/icon-512.png`,
width: 512,
height: 512,
},
image: `${SITE.url}/icon-512.png`,
sameAs: [SITE.autorLinkedin, SITE.repositorio, SITE.blog],
},
{
"@type": "Person",
"@id": `${SITE.url}/#${isEn ? "author" : "autor"}`,
name: SITE.autor,
url: SITE.autorUrl,
email: SITE.autorEmail,
sameAs: [SITE.autorLinkedin, SITE.repositorio, SITE.blog],
},
],
};
},
/** Trilha de navegação: aparece acima do título no resultado de busca. */
trilha(itens) {
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: itens.map((it, i) => ({
"@type": "ListItem",
position: i + 1,
name: it.nome,
item: `${SITE.url}${it.caminho}`,
})),
};
},
/** Documentação e ficha técnica da série como TechArticle para o Google. */
serie(chave, serie, locale = "pt") {
const isEn = locale === "en";
const prop = (nome, valor) =>
valor ? { "@type": "PropertyValue", name: nome, value: String(valor) } : null;
const propriedades = [
prop(isEn ? "Manufacturer" : "Fabricante", "Espressif Systems"),
prop(isEn ? "Architecture" : "Arquitetura", serie.arquitetura),
prop(isEn ? "Cores" : "Núcleos", serie.nucleos),
prop(isEn ? "Max frequency" : "Frequência máxima", serie.frequencia),
prop("SRAM", serie.memoria_sram),
prop("RTC SRAM", serie.memoria_sram_rtc),
prop(isEn ? "Supported Flash" : "Flash suportada", serie.flash_suportada),
prop(isEn ? "Supported PSRAM" : "PSRAM suportada", serie.psram_suportada),
prop("GPIO", serie.gpio),
prop("Wi-Fi", serie.wifi),
prop("Bluetooth", serie.bluetooth),
prop("Zigbee / Thread / 802.15.4", serie.zigbee_thread),
prop(isEn ? "Package" : "Encapsulamento", serie.encapsulamento),
prop(isEn ? "Minimum ESP-IDF" : "ESP-IDF mínimo", serie.esp_idf_minimo),
].filter(Boolean);
const imagem = serie.placa
? `${SITE.url}${serie.placa}`
: `${SITE.url}/opengraph-image.png`;
const basePath = isEn ? `/en/series/${chave}` : `/series/${chave}`;
return {
"@context": "https://schema.org",
"@type": "TechArticle",
"@id": `${SITE.url}${basePath}#${isEn ? "article" : "artigo"}`,
headline: isEn
? `${serie.nome_completo || chave}: specifications and pinout`
: `${serie.nome_completo || chave}: especificações e pinagem`,
name: isEn
? `${serie.nome_completo || chave} - Technical Documentation and Specifications`
: `${serie.nome_completo || chave} - Documentação Técnica e Especificações`,
description: serie.descricao,
image: [imagem],
inLanguage: isEn ? "en" : "pt-BR",
datePublished: "2026-08-25T00:00:00.000Z",
dateModified: new Date().toISOString(),
mainEntityOfPage: `${SITE.url}${basePath}`,
url: `${SITE.url}${basePath}`,
author: {
"@type": "Person",
name: SITE.autor,
url: SITE.autorUrl,
},
publisher: {
"@type": "Organization",
name: "Ciência Embarcada",
url: SITE.blog,
logo: {
"@type": "ImageObject",
url: `${SITE.url}/icon-512.png`,
width: 512,
height: 512,
},
},
about: {
"@type": "Thing",
name: serie.nome_completo || chave,
description: serie.descricao,
additionalProperty: propriedades,
},
};
},
/** Documentação técnica de framework. */
framework(chave, framework, locale = "pt") {
const isEn = locale === "en";
const basePath = isEn ? `/en/frameworks/${encodeURIComponent(chave)}` : `/frameworks/${encodeURIComponent(chave)}`;
return {
"@context": "https://schema.org",
"@type": "TechArticle",
"@id": `${SITE.url}${basePath}#${isEn ? "article" : "artigo"}`,
headline: isEn
? `${framework.nome} for ESP32: Guide and Documentation`
: `${framework.nome} para ESP32: Guia e Documentação`,
name: isEn
? `${framework.nome_completo || framework.nome} for ESP32`
: `${framework.nome_completo || framework.nome} para ESP32`,
description: framework.descricao,
image: [`${SITE.url}/opengraph-image.png`],
inLanguage: isEn ? "en" : "pt-BR",
datePublished: "2026-08-25T00:00:00.000Z",
dateModified: new Date().toISOString(),
mainEntityOfPage: `${SITE.url}${basePath}`,
url: `${SITE.url}${basePath}`,
author: {
"@type": "Person",
name: SITE.autor,
url: SITE.autorUrl,
},
publisher: {
"@type": "Organization",
name: "Ciência Embarcada",
url: SITE.blog,
logo: {
"@type": "ImageObject",
url: `${SITE.url}/icon-512.png`,
width: 512,
height: 512,
},
},
about: {
"@type": "SoftwareApplication",
name: framework.nome_completo || framework.nome,
applicationCategory: "DeveloperApplication",
operatingSystem: "Embedded",
description: framework.descricao,
url: framework.documentacao || `${SITE.url}${basePath}`,
},
};
},
/** Glossário: marca cada verbete como termo definido. */
glossario(categorias, locale = "pt") {
const isEn = locale === "en";
const basePath = isEn ? "/en/glossary" : "/glossario";
return {
"@context": "https://schema.org",
"@type": "DefinedTermSet",
"@id": `${SITE.url}${basePath}#${isEn ? "glossary" : "glossario"}`,
name: isEn ? "ESP32 Technical Glossary" : "Glossário Técnico do ESP32",
inLanguage: isEn ? "en" : "pt-BR",
hasDefinedTerm: categorias.flatMap((cat) =>
cat.termos.map((t) => ({
"@type": "DefinedTerm",
name: t.termo,
description: t.definicao,
inDefinedTermSet: `${SITE.url}${basePath}#${isEn ? "glossary" : "glossario"}`,
}))
),
};
},
/** Perguntas frequentes: pode render o bloco expansível no Google. */
perguntas(pares) {
return {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: pares.map(({ pergunta, resposta }) => ({
"@type": "Question",
name: pergunta,
acceptedAnswer: { "@type": "Answer", text: resposta },
})),
};
},
/** Aplicação web / ferramenta interativa (Seletor, Comparador, Catálogo). */
aplicacaoWeb({ nome, descricao, caminho, locale = "pt" }) {
const isEn = locale === "en";
return {
"@context": "https://schema.org",
"@type": "WebApplication",
"@id": `${SITE.url}${caminho}#app`,
name: nome,
description: descricao,
url: `${SITE.url}${caminho}`,
applicationCategory: "DeveloperApplication",
operatingSystem: "All",
browserRequirements: "Requires JavaScript",
inLanguage: isEn ? "en" : "pt-BR",
author: {
"@type": "Person",
name: SITE.autor,
url: SITE.autorUrl,
},
publisher: {
"@type": "Organization",
name: "Ciência Embarcada",
url: SITE.blog,
logo: {
"@type": "ImageObject",
url: `${SITE.url}/icon-512.png`,
width: 512,
height: 512,
},
},
offers: {
"@type": "Offer",
price: "0",
priceCurrency: "BRL",
},
};
},
/** Livro do autor: dados de obra para mecanismos de busca. */
livro() {
return {
"@context": "https://schema.org",
"@type": "Book",
"@id": `${SITE.url}/livro#livro`,
name: SITE.livroTitulo,
alternateName: `${SITE.livroTitulo}: ${SITE.livroSubtitulo}`,
description:
"Livro em português sobre o hardware e o firmware do ESP32: arquitetura, energia, rádio, periféricos e projeto de produto, em 12 partes e 78 capítulos.",
inLanguage: "pt-BR",
url: `${SITE.url}/livro`,
isbn: SITE.livroIsbn,
image: `${SITE.url}${SITE.livroCapa}`,
author: {
"@type": "Person",
name: SITE.autor,
url: SITE.autorUrl,
},
publisher: {
"@type": "Organization",
name: "Ciência Embarcada",
url: SITE.blog,
},
about: [
{ "@type": "Thing", name: "ESP32" },
{ "@type": "Thing", name: "Sistemas embarcados" },
{ "@type": "Thing", name: "Internet das Coisas" },
],
workExample: [
{
"@type": "Book",
"@id": `${SITE.url}/livro#digital`,
bookFormat: "https://schema.org/EBook",
url: SITE.livroAmazon,
potentialAction: {
"@type": "BuyAction",
target: SITE.livroAmazon,
},
},
{
"@type": "Book",
"@id": `${SITE.url}/livro#fisico`,
bookFormat: "https://schema.org/Paperback",
url: SITE.livroUiclap,
potentialAction: {
"@type": "BuyAction",
target: SITE.livroUiclap,
},
},
],
};
},
/** Página Sobre com metadados E-E-A-T de autoria e reputação. */
paginaSobre(locale = "pt") {
const isEn = locale === "en";
const basePath = isEn ? "/en/about" : "/sobre";
return {
"@context": "https://schema.org",
"@type": "AboutPage",
"@id": `${SITE.url}${basePath}#${isEn ? "about" : "sobre"}`,
name: isEn ? "About ESPDocs" : "Sobre o ESPDocs",
description: isEn
? "ESPDocs is an independent platform organizing technical documentation of the ESP32 ecosystem."
: "O ESPDocs é uma plataforma independente que reúne a documentação do ecossistema ESP32 em português brasileiro.",
url: `${SITE.url}${basePath}`,
inLanguage: isEn ? "en" : "pt-BR",
mainEntity: {
"@type": "Person",
name: SITE.autor,
url: SITE.autorUrl,
email: SITE.autorEmail,
sameAs: [
SITE.autorLinkedin,
SITE.repositorio,
SITE.blog,
],
},
};
},
};