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>
This commit is contained in:
1 parent
47c55515bb
commit
0d5f8f9c5f
17 files changed
+151
-21
No files matched your search
@@ -27,6 +27,9 @@ Tudo o que muda de relevante no ESPDocs está aqui. O formato segue o
|
|||||||
- Internacionalização do link de acessibilidade "Pular para o conteúdo" (`skipToContent`) no layout raiz e trilhas de navegação (breadcrumbs).
|
- Internacionalização do link de acessibilidade "Pular para o conteúdo" (`skipToContent`) no layout raiz e trilhas de navegação (breadcrumbs).
|
||||||
- Conversão de layouts de páginas (`series`, `comparacao`, `catalogo`, `componentes`, `seletor`, `diagnostico`, `glossario`) para `generateMetadata` dinâmico por locale com títulos, descrições e dados estruturados JSON-LD estritamente em inglês.
|
- Conversão de layouts de páginas (`series`, `comparacao`, `catalogo`, `componentes`, `seletor`, `diagnostico`, `glossario`) para `generateMetadata` dinâmico por locale com títulos, descrições e dados estruturados JSON-LD estritamente em inglês.
|
||||||
- Implementação de checagem automatizada estrita em `scripts/check-i18n.mjs` que bloqueia strings idênticas entre `pt` e `en` fora da allowlist de termos técnicos.
|
- Implementação de checagem automatizada estrita em `scripts/check-i18n.mjs` que bloqueia strings idênticas entre `pt` e `en` fora da allowlist de termos técnicos.
|
||||||
|
- Correção de `headline`, propriedades, identificadores e âncoras `@id` (`#article`, `#about`, `#glossary`, `#organization`, `#author`) em todos os blocos de dados estruturados JSON-LD (`TechArticle`, `DefinedTermSet`, `AboutPage`, `WebSite`) nas páginas em inglês.
|
||||||
|
- Correção do `PageHero` em Server Components para suportar a propriedade `isEn` explicitamente, eliminando o texto "Início" no breadcrumb das rotas inglesas.
|
||||||
|
- Adição do script de verificação permanente `scripts/verifica-vazamentos.py` ao `smoke-imagem.sh`, validando todas as 36 rotas públicas em inglês contra vazamentos em texto visível e blocos `ld+json`.
|
||||||
|
|
||||||
## [1.2.0] - 2026-11-01
|
## [1.2.0] - 2026-11-01
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ export default function PlacasPage() {
|
|||||||
|
|
||||||
const hero = (
|
const hero = (
|
||||||
<PageHero
|
<PageHero
|
||||||
|
isEn={isEn}
|
||||||
trail={[{ label: isEn ? "Board Catalog" : "Catálogo de placas" }]}
|
trail={[{ label: isEn ? "Board Catalog" : "Catálogo de placas" }]}
|
||||||
icon={ShoppingCart}
|
icon={ShoppingCart}
|
||||||
label={isEn ? "Real DevKits and modules" : "DevKits e módulos reais"}
|
label={isEn ? "Real DevKits and modules" : "DevKits e módulos reais"}
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ export default function Comparacao() {
|
|||||||
|
|
||||||
<main id="conteudo">
|
<main id="conteudo">
|
||||||
<PageHero
|
<PageHero
|
||||||
|
isEn={isEn}
|
||||||
trail={[{ label: isEn ? "Comparator" : "Comparador" }]}
|
trail={[{ label: isEn ? "Comparator" : "Comparador" }]}
|
||||||
icon={Scale}
|
icon={Scale}
|
||||||
label={isEn ? "Up to four series side by side" : "Até quatro séries lado a lado"}
|
label={isEn ? "Up to four series side by side" : "Até quatro séries lado a lado"}
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ export default function ComponentRegistryPage() {
|
|||||||
|
|
||||||
<main id="conteudo">
|
<main id="conteudo">
|
||||||
<PageHero
|
<PageHero
|
||||||
|
isEn={isEn}
|
||||||
trail={[{ label: isEn ? "Component Registry" : "Component Registry" }]}
|
trail={[{ label: isEn ? "Component Registry" : "Component Registry" }]}
|
||||||
icon={Box}
|
icon={Box}
|
||||||
label="ESP-IDF Component Manager"
|
label="ESP-IDF Component Manager"
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ export default function DiagnosticoPage() {
|
|||||||
|
|
||||||
<main id="conteudo">
|
<main id="conteudo">
|
||||||
<PageHero
|
<PageHero
|
||||||
|
isEn={isEn}
|
||||||
trail={[{ label: isEn ? "Diagnostics" : "Diagnóstico" }]}
|
trail={[{ label: isEn ? "Diagnostics" : "Diagnóstico" }]}
|
||||||
icon={AlertTriangle}
|
icon={AlertTriangle}
|
||||||
label={isEn ? "Firmware Debugging" : "Depuração de firmware"}
|
label={isEn ? "Firmware Debugging" : "Depuração de firmware"}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default async function FrameworkDetail({ params }) {
|
|||||||
{/* Ficha técnica do framework e trilha de navegação para o Google */}
|
{/* Ficha técnica do framework e trilha de navegação para o Google */}
|
||||||
<script
|
<script
|
||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.framework(key, framework)) }}
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.framework(key, framework, currentLocale)) }}
|
||||||
/>
|
/>
|
||||||
<script
|
<script
|
||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
@@ -47,6 +47,7 @@ export default async function FrameworkDetail({ params }) {
|
|||||||
|
|
||||||
<main id="conteudo">
|
<main id="conteudo">
|
||||||
<PageHero
|
<PageHero
|
||||||
|
isEn={isEn}
|
||||||
trail={[{ label: isEn ? "Frameworks & SDKs" : "Frameworks e SDKs", href: getLocalizedHref("/frameworks", currentLocale) }, { label: framework.nome }]}
|
trail={[{ label: isEn ? "Frameworks & SDKs" : "Frameworks e SDKs", href: getLocalizedHref("/frameworks", currentLocale) }, { label: framework.nome }]}
|
||||||
icon={Layers}
|
icon={Layers}
|
||||||
label={framework.tipo}
|
label={framework.tipo}
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ export default async function FrameworksLayout({ children, params }) {
|
|||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: JSON.stringify(
|
__html: JSON.stringify(
|
||||||
jsonLd.trilha([
|
jsonLd.trilha([
|
||||||
{ nome: isEn ? "Home" : "Início", caminho: isEn ? "/en" : "/pt" },
|
{ nome: isEn ? "Home" : "Início", caminho: isEn ? "/en" : "/" },
|
||||||
{ nome: "Frameworks", caminho: isEn ? "/en/frameworks" : "/pt/frameworks" },
|
{ nome: "Frameworks", caminho: isEn ? "/en/frameworks" : "/frameworks" },
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export default async function Frameworks({ params }) {
|
|||||||
|
|
||||||
<main id="conteudo">
|
<main id="conteudo">
|
||||||
<PageHero
|
<PageHero
|
||||||
|
isEn={isEn}
|
||||||
trail={[{ label: isEn ? "Frameworks & SDKs" : "Frameworks e SDKs" }]}
|
trail={[{ label: isEn ? "Frameworks & SDKs" : "Frameworks e SDKs" }]}
|
||||||
icon={Layers}
|
icon={Layers}
|
||||||
label={isEn ? `${frameworks.length} frameworks and SDKs` : `${frameworks.length} frameworks e SDKs`}
|
label={isEn ? `${frameworks.length} frameworks and SDKs` : `${frameworks.length} frameworks e SDKs`}
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ export default function GlossarioPage() {
|
|||||||
|
|
||||||
<main id="conteudo">
|
<main id="conteudo">
|
||||||
<PageHero
|
<PageHero
|
||||||
|
isEn={isEn}
|
||||||
trail={[{ label: isEn ? "Glossary" : "Glossário" }]}
|
trail={[{ label: isEn ? "Glossary" : "Glossário" }]}
|
||||||
icon={BookOpen}
|
icon={BookOpen}
|
||||||
label={isEn ? `${todosOsTermos.length} terms explained` : `${todosOsTermos.length} termos explicados`}
|
label={isEn ? `${todosOsTermos.length} terms explained` : `${todosOsTermos.length} termos explicados`}
|
||||||
|
|||||||
@@ -1369,6 +1369,7 @@ export default function Seletor() {
|
|||||||
|
|
||||||
<main id="conteudo">
|
<main id="conteudo">
|
||||||
<PageHero
|
<PageHero
|
||||||
|
isEn={isEn}
|
||||||
trail={isEn ? [{ label: "Chip selector" }] : [{ label: "Seletor de chip" }]}
|
trail={isEn ? [{ label: "Chip selector" }] : [{ label: "Seletor de chip" }]}
|
||||||
icon={Sparkles}
|
icon={Sparkles}
|
||||||
label={isEn ? "Smart selector" : "Seletor inteligente"}
|
label={isEn ? "Smart selector" : "Seletor inteligente"}
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ export default async function SerieDetail({ params }) {
|
|||||||
<div className="site-canvas min-h-screen">
|
<div className="site-canvas min-h-screen">
|
||||||
<script
|
<script
|
||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.serie(key, serie)) }}
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.serie(key, serie, currentLocale)) }}
|
||||||
/>
|
/>
|
||||||
<script
|
<script
|
||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
@@ -395,6 +395,7 @@ export default async function SerieDetail({ params }) {
|
|||||||
|
|
||||||
<main id="conteudo">
|
<main id="conteudo">
|
||||||
<PageHero
|
<PageHero
|
||||||
|
isEn={isEn}
|
||||||
trail={[{ label: isEn ? "Series" : "Séries", href: getLocalizedHref("/series", currentLocale) }, { label: key }]}
|
trail={[{ label: isEn ? "Series" : "Séries", href: getLocalizedHref("/series", currentLocale) }, { label: key }]}
|
||||||
icon={Cpu}
|
icon={Cpu}
|
||||||
label={serie.arquitetura}
|
label={serie.arquitetura}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export default function Series() {
|
|||||||
|
|
||||||
<main id="conteudo">
|
<main id="conteudo">
|
||||||
<PageHero
|
<PageHero
|
||||||
|
isEn={isEn}
|
||||||
trail={[{ label: isEn ? "Series" : "Séries" }]}
|
trail={[{ label: isEn ? "Series" : "Séries" }]}
|
||||||
icon={Compass}
|
icon={Compass}
|
||||||
label={isEn ? `${todas.length} Espressif families` : `${todas.length} famílias da Espressif`}
|
label={isEn ? `${todas.length} Espressif families` : `${todas.length} famílias da Espressif`}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export default async function Sobre({ params }) {
|
|||||||
|
|
||||||
<main id="conteudo">
|
<main id="conteudo">
|
||||||
<PageHero
|
<PageHero
|
||||||
|
isEn={!isPt}
|
||||||
trail={[{ label: isPt ? "Sobre" : "About" }]}
|
trail={[{ label: isPt ? "Sobre" : "About" }]}
|
||||||
icon={Info}
|
icon={Info}
|
||||||
label={isPt ? "Projeto independente" : "Independent project"}
|
label={isPt ? "Projeto independente" : "Independent project"}
|
||||||
|
|||||||
+17
-9
@@ -1,20 +1,28 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useLocale } from "next-intl";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abertura padrão das páginas internas: trilha, rótulo, título, frase de apoio
|
* Abertura padrão das páginas internas: trilha, rótulo, título, frase de apoio
|
||||||
* e ações. Mesma voz da /livro e da home, para que nenhuma página pareça
|
* e ações. Mesma voz da /livro e da home, para que nenhuma página pareça
|
||||||
* de outro produto.
|
* de outro produto.
|
||||||
*/
|
*/
|
||||||
export default function PageHero({ trail = [], icon: Icon, label, title, lede, align = "left", children }) {
|
export default function PageHero({
|
||||||
|
trail = [],
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
title,
|
||||||
|
lede,
|
||||||
|
align = "left",
|
||||||
|
children,
|
||||||
|
isEn: isEnProp,
|
||||||
|
locale: localeProp,
|
||||||
|
}) {
|
||||||
const center = align === "center";
|
const center = align === "center";
|
||||||
let isEn = false;
|
const isEn =
|
||||||
try {
|
isEnProp !== undefined
|
||||||
const locale = useLocale();
|
? isEnProp
|
||||||
isEn = locale === "en";
|
: localeProp
|
||||||
} catch {
|
? localeProp === "en"
|
||||||
// fallback
|
: trail.some((t) => t.isEn || (t.href && t.href.startsWith("/en")));
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="shell pt-8 sm:pt-12">
|
<section className="shell pt-8 sm:pt-12">
|
||||||
|
|||||||
+8
-8
@@ -106,11 +106,11 @@ export const jsonLd = {
|
|||||||
description: isEn
|
description: isEn
|
||||||
? "ESP32 ecosystem documentation: specifications, pinouts, comparison, and series selection."
|
? "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.",
|
: "Documentação do ecossistema ESP32 em português brasileiro: especificações, pinagem, comparação e seleção de séries.",
|
||||||
publisher: { "@id": `${SITE.url}/#organizacao` },
|
publisher: { "@id": `${SITE.url}/#${isEn ? "organization" : "organizacao"}` },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
"@id": `${SITE.url}/#organizacao`,
|
"@id": `${SITE.url}/#${isEn ? "organization" : "organizacao"}`,
|
||||||
name: "Ciência Embarcada",
|
name: "Ciência Embarcada",
|
||||||
url: SITE.blog,
|
url: SITE.blog,
|
||||||
logo: {
|
logo: {
|
||||||
@@ -124,7 +124,7 @@ export const jsonLd = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@type": "Person",
|
"@type": "Person",
|
||||||
"@id": `${SITE.url}/#autor`,
|
"@id": `${SITE.url}/#${isEn ? "author" : "autor"}`,
|
||||||
name: SITE.autor,
|
name: SITE.autor,
|
||||||
url: SITE.autorUrl,
|
url: SITE.autorUrl,
|
||||||
email: SITE.autorEmail,
|
email: SITE.autorEmail,
|
||||||
@@ -180,7 +180,7 @@ export const jsonLd = {
|
|||||||
return {
|
return {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "TechArticle",
|
"@type": "TechArticle",
|
||||||
"@id": `${SITE.url}${basePath}#artigo`,
|
"@id": `${SITE.url}${basePath}#${isEn ? "article" : "artigo"}`,
|
||||||
headline: isEn
|
headline: isEn
|
||||||
? `${serie.nome_completo || chave}: specifications and pinout`
|
? `${serie.nome_completo || chave}: specifications and pinout`
|
||||||
: `${serie.nome_completo || chave}: especificações e pinagem`,
|
: `${serie.nome_completo || chave}: especificações e pinagem`,
|
||||||
@@ -226,7 +226,7 @@ export const jsonLd = {
|
|||||||
return {
|
return {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "TechArticle",
|
"@type": "TechArticle",
|
||||||
"@id": `${SITE.url}${basePath}#artigo`,
|
"@id": `${SITE.url}${basePath}#${isEn ? "article" : "artigo"}`,
|
||||||
headline: isEn
|
headline: isEn
|
||||||
? `${framework.nome} for ESP32: Guide and Documentation`
|
? `${framework.nome} for ESP32: Guide and Documentation`
|
||||||
: `${framework.nome} para ESP32: Guia e Documentação`,
|
: `${framework.nome} para ESP32: Guia e Documentação`,
|
||||||
@@ -274,7 +274,7 @@ export const jsonLd = {
|
|||||||
return {
|
return {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "DefinedTermSet",
|
"@type": "DefinedTermSet",
|
||||||
"@id": `${SITE.url}${basePath}#glossario`,
|
"@id": `${SITE.url}${basePath}#${isEn ? "glossary" : "glossario"}`,
|
||||||
name: isEn ? "ESP32 Technical Glossary" : "Glossário Técnico do ESP32",
|
name: isEn ? "ESP32 Technical Glossary" : "Glossário Técnico do ESP32",
|
||||||
inLanguage: isEn ? "en" : "pt-BR",
|
inLanguage: isEn ? "en" : "pt-BR",
|
||||||
hasDefinedTerm: categorias.flatMap((cat) =>
|
hasDefinedTerm: categorias.flatMap((cat) =>
|
||||||
@@ -282,7 +282,7 @@ export const jsonLd = {
|
|||||||
"@type": "DefinedTerm",
|
"@type": "DefinedTerm",
|
||||||
name: t.termo,
|
name: t.termo,
|
||||||
description: t.definicao,
|
description: t.definicao,
|
||||||
inDefinedTermSet: `${SITE.url}${basePath}#glossario`,
|
inDefinedTermSet: `${SITE.url}${basePath}#${isEn ? "glossary" : "glossario"}`,
|
||||||
}))
|
}))
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@@ -400,7 +400,7 @@ export const jsonLd = {
|
|||||||
return {
|
return {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "AboutPage",
|
"@type": "AboutPage",
|
||||||
"@id": `${SITE.url}${basePath}#sobre`,
|
"@id": `${SITE.url}${basePath}#${isEn ? "about" : "sobre"}`,
|
||||||
name: isEn ? "About ESPDocs" : "Sobre o ESPDocs",
|
name: isEn ? "About ESPDocs" : "Sobre o ESPDocs",
|
||||||
description: isEn
|
description: isEn
|
||||||
? "ESPDocs is an independent platform organizing technical documentation of the ESP32 ecosystem."
|
? "ESPDocs is an independent platform organizing technical documentation of the ESP32 ecosystem."
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ if [[ " $ENABLED_LOCALES " =~ " en " ]]; then
|
|||||||
for marca in '<title>' 'rel="canonical"'; do
|
for marca in '<title>' 'rel="canonical"'; do
|
||||||
case "$head_en" in *"$marca"*) ;; *) echo "FALHOU /en/about: $marca fora do <head>"; falhas=$((falhas + 1));; esac
|
case "$head_en" in *"$marca"*) ;; *) echo "FALHOU /en/about: $marca fora do <head>"; falhas=$((falhas + 1));; esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# 7. Varredura estrita contra vazamento de português (texto visível e blocos ld+json) nas 36 rotas
|
||||||
|
python3 scripts/verifica-vazamentos.py "$base" || falhas=$((falhas + 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exit $((falhas > 0))
|
exit $((falhas > 0))
|
||||||
Executable
+105
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
scripts/verifica-vazamentos.py
|
||||||
|
Verificação permanente de ausência de vazamento de português em rotas inglesas (en).
|
||||||
|
Analisa tanto o texto visível da página quanto todos os blocos <script type="application/ld+json">.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import html
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
base = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:3998"
|
||||||
|
|
||||||
|
series = [
|
||||||
|
"ESP32", "ESP32-S2", "ESP32-S3", "ESP32-S31", "ESP32-C2", "ESP32-C3",
|
||||||
|
"ESP32-C5", "ESP32-C6", "ESP32-C61", "ESP32-P4", "ESP32-H2", "ESP32-H4"
|
||||||
|
]
|
||||||
|
|
||||||
|
frameworks = [
|
||||||
|
"ESP-IDF", "ESP-RainMaker", "ESP-Matter", "ESP-ADF", "ESP-SR", "ESP-IoT-Solution",
|
||||||
|
"ESP-NOW", "ESP-Zigbee-SDK", "ESP-WHO", "ESP-NN", "ESP-DSP", "Rust-ESP",
|
||||||
|
"Arduino", "MicroPython"
|
||||||
|
]
|
||||||
|
|
||||||
|
fixas = [
|
||||||
|
"/en", "/en/series", "/en/compare", "/en/diagnostics", "/en/selector",
|
||||||
|
"/en/frameworks", "/en/components", "/en/glossary", "/en/catalog", "/en/about"
|
||||||
|
]
|
||||||
|
|
||||||
|
urls = fixas + [f"/en/series/{s}" for s in series] + [f"/en/frameworks/{f}" for f in frameworks]
|
||||||
|
|
||||||
|
# Regex rigoroso para detecção de vazamentos em português
|
||||||
|
pt_regex = re.compile(
|
||||||
|
r"\b(não|você|para o|para a|com o|uma|são|também|então|código|sobre|pinagem|"
|
||||||
|
r"série|séries|livro|ção|ções|mais|muito|já|está|estão|é um|que|dos|das|"
|
||||||
|
r"nos|nas|pelo|pela|especificações|especificação|guia|comparativo|catálogo|"
|
||||||
|
r"seletor|recursos|projeto|independente|núcleos|frequência máxima|fabricante|"
|
||||||
|
r"arquitetura|encapsulamento|início|artigo)\b",
|
||||||
|
re.I
|
||||||
|
)
|
||||||
|
|
||||||
|
# Termos técnicos legítimos aceitos em links/IDs/nomes
|
||||||
|
TECHNICAL_EXEMPT_PATHS = {"@id", "@type", "@context", "url", "image", "logo", "sameAs"}
|
||||||
|
|
||||||
|
def check_jsonld(obj, path=""):
|
||||||
|
problems = []
|
||||||
|
if isinstance(obj, str):
|
||||||
|
key = path.split(".")[-1].split("[")[0]
|
||||||
|
if key in TECHNICAL_EXEMPT_PATHS:
|
||||||
|
# Garante que âncoras de id não vazem português
|
||||||
|
if (path.endswith("@id") or path.endswith("url")) and ("#artigo" in obj or "#sobre" in obj):
|
||||||
|
problems.append((path, f"Anchor leak in {obj}"))
|
||||||
|
return problems
|
||||||
|
if pt_regex.search(obj):
|
||||||
|
problems.append((path, obj))
|
||||||
|
elif isinstance(obj, list):
|
||||||
|
for i, val in enumerate(obj):
|
||||||
|
problems.extend(check_jsonld(val, f"{path}[{i}]"))
|
||||||
|
elif isinstance(obj, dict):
|
||||||
|
for k, val in obj.items():
|
||||||
|
problems.extend(check_jsonld(val, f"{path}.{k}" if path else k))
|
||||||
|
return problems
|
||||||
|
|
||||||
|
falhas = 0
|
||||||
|
|
||||||
|
print(f"ESPDocs · Varredura de i18n nas 36 rotas em inglês ({base})")
|
||||||
|
|
||||||
|
for u in urls:
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(base + u, headers={"User-Agent": "ESPDocs-SmokeTest/1.0"})
|
||||||
|
raw = urllib.request.urlopen(req, timeout=10).read().decode("utf-8")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✖ ERRO ao acessar {u}: {e}")
|
||||||
|
falhas += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 1. Verifica blocos ld+json
|
||||||
|
scripts = re.findall(r"<script[^>]*type=[\"']application/ld\+json[\"'][^>]*>(.*?)</script>", raw, re.DOTALL)
|
||||||
|
for i, s in enumerate(scripts):
|
||||||
|
try:
|
||||||
|
data = json.loads(s)
|
||||||
|
issues = check_jsonld(data, f"script[{i}]")
|
||||||
|
for p, v in issues:
|
||||||
|
print(f" ✖ JSON-LD vazamento em {u} -> {p}: \"{v}\"")
|
||||||
|
falhas += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✖ Erro no parse de JSON-LD em {u}: {e}")
|
||||||
|
falhas += 1
|
||||||
|
|
||||||
|
# 2. Verifica texto visível
|
||||||
|
clean = re.sub(r"<(script|style)[\s\S]*?</\1>", "", raw)
|
||||||
|
clean = html.unescape(re.sub(r"<[^>]+>", "\n", clean))
|
||||||
|
hits = [l.strip() for l in clean.split("\n") if l.strip() and pt_regex.search(l)]
|
||||||
|
for h in hits:
|
||||||
|
print(f" ✖ Texto visível vazamento em {u}: \"{h[:120]}\"")
|
||||||
|
falhas += 1
|
||||||
|
|
||||||
|
if falhas == 0:
|
||||||
|
print(f" ✔ Todas as {len(urls)} rotas em inglês verificadas com ZERO vazamentos (texto e JSON-LD).")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print(f" ✖ Total de falhas detectadas: {falhas}")
|
||||||
|
sys.exit(1)
|
||||||
Reference in new issue
Block a user