feat(i18n): restrict public routes and metadata to enabled locales
- Add ENABLED_LOCALES configuration in i18n/links.js (currently ["pt"]) - Redirect inactive locale prefixes to /pt via 307 in middleware.js - Restrict root locale negotiation to active locales with fallback to pt - Hide LanguageSelector when only one locale is enabled - Generate sitemap, hreflang alternates, and og:locale only for active locales - Generate static SSG pages only for active locales in build - Adapt check:i18n and smoke-imagem.sh to validate active vs inactive locales Co-Authored-By: Antigravity <noreply@google.com>
This commit is contained in:
1 parent
3a1a095e97
commit
d479a8d361
9 files changed
+219
-79
No files matched your search
+12
-3
@@ -3,6 +3,7 @@ import { JetBrains_Mono, Manrope, Space_Grotesk } from "next/font/google";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages } from "next-intl/server";
|
||||
import { routing } from "@/i18n/routing";
|
||||
import { ENABLED_LOCALES } from "@/i18n/links";
|
||||
import "../globals.css";
|
||||
import Providers from "@/components/Providers";
|
||||
import { SITE, jsonLd } from "@/lib/seo";
|
||||
@@ -38,7 +39,7 @@ const LANG_MAP = {
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({ locale }));
|
||||
return ENABLED_LOCALES.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
@@ -52,6 +53,8 @@ export async function generateMetadata({ params }) {
|
||||
"zh-CN": "zh_CN",
|
||||
};
|
||||
|
||||
const alternateLocales = ENABLED_LOCALES.filter((l) => l !== locale).map((l) => ogLocaleMap[l]);
|
||||
|
||||
return {
|
||||
metadataBase: new URL(SITE.url),
|
||||
title: {
|
||||
@@ -71,10 +74,16 @@ export async function generateMetadata({ params }) {
|
||||
"comparar ESP32", "qual ESP32 escolher", "datasheet ESP32",
|
||||
"microcontrolador", "Espressif", "ESP-IDF", "IoT", "sistemas embarcados",
|
||||
],
|
||||
alternates: { canonical: `/${locale}` },
|
||||
alternates: {
|
||||
canonical: `/${locale}`,
|
||||
languages: Object.fromEntries(
|
||||
ENABLED_LOCALES.map((l) => [LANG_MAP[l] || l, `/${l}`])
|
||||
),
|
||||
},
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: ogLocaleMap[locale] || "pt_BR",
|
||||
alternateLocale: alternateLocales.length > 0 ? alternateLocales : undefined,
|
||||
url: `${SITE.url}/${locale}`,
|
||||
siteName: SITE.nome,
|
||||
title: "ESPDocs · Documentação do ESP32 em português",
|
||||
@@ -127,7 +136,7 @@ export const viewport = {
|
||||
|
||||
export default async function RootLayout({ children, params }) {
|
||||
const { locale } = await params;
|
||||
if (!routing.locales.includes(locale)) {
|
||||
if (!ENABLED_LOCALES.includes(locale)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
|
||||
+55
-28
@@ -1,51 +1,78 @@
|
||||
import seriesData from "@/public/series.json";
|
||||
import frameworksData from "@/public/frameworks.json";
|
||||
import { SITE } from "@/lib/seo";
|
||||
import { ENABLED_LOCALES, getLocalizedHref } from "@/i18n/links";
|
||||
|
||||
const LANG_MAP = {
|
||||
pt: "pt-BR",
|
||||
en: "en",
|
||||
es: "es-419",
|
||||
fr: "fr",
|
||||
de: "de",
|
||||
"zh-CN": "zh-CN",
|
||||
};
|
||||
|
||||
/**
|
||||
* Sitemap gerado a partir dos próprios dados do site.
|
||||
*
|
||||
* Antes não existia sitemap nenhum, então o Google só chegava às páginas de
|
||||
* série e de framework se topasse com um link. Como são geradas a partir de
|
||||
* JSON, uma série nova entra aqui sozinha.
|
||||
* Sitemap gerado a partir dos próprios dados do site e dos idiomas ativos.
|
||||
* Inclui URLs apenas para ENABLED_LOCALES, com hreflang/alternates correspondentes.
|
||||
* O livro só entra para a versão em português.
|
||||
*/
|
||||
export default function sitemap() {
|
||||
// Usar uma data de revisão estável evita que todo deploy force o Googlebot
|
||||
// a reprocessar páginas idênticas ao mesmo tempo.
|
||||
const dataRevisao = new Date("2026-08-25T00:00:00.000Z");
|
||||
|
||||
const fixas = [
|
||||
{ caminho: "", prioridade: 1.0, frequencia: "weekly", lastModified: dataRevisao },
|
||||
{ caminho: "/series", prioridade: 0.9, frequencia: "weekly", lastModified: dataRevisao },
|
||||
{ caminho: "/comparacao", prioridade: 0.9, frequencia: "weekly", lastModified: dataRevisao },
|
||||
{ caminho: "/diagnostico", prioridade: 0.9, frequencia: "weekly", lastModified: dataRevisao },
|
||||
{ caminho: "/seletor", prioridade: 0.8, frequencia: "monthly", lastModified: dataRevisao },
|
||||
{ caminho: "/frameworks", prioridade: 0.8, frequencia: "monthly", lastModified: dataRevisao },
|
||||
{ caminho: "/componentes", prioridade: 0.8, frequencia: "weekly", lastModified: dataRevisao },
|
||||
{ caminho: "/livro", prioridade: 0.7, frequencia: "monthly", lastModified: dataRevisao },
|
||||
{ caminho: "/glossario", prioridade: 0.7, frequencia: "monthly", lastModified: dataRevisao },
|
||||
{ caminho: "/catalogo", prioridade: 0.6, frequencia: "weekly", lastModified: dataRevisao },
|
||||
{ caminho: "/sobre", prioridade: 0.4, frequencia: "yearly", lastModified: dataRevisao },
|
||||
{ rota: "", prioridade: 1.0, frequencia: "weekly" },
|
||||
{ rota: "/series", prioridade: 0.9, frequencia: "weekly" },
|
||||
{ rota: "/comparacao", prioridade: 0.9, frequencia: "weekly" },
|
||||
{ rota: "/diagnostico", prioridade: 0.9, frequencia: "weekly" },
|
||||
{ rota: "/seletor", prioridade: 0.8, frequencia: "monthly" },
|
||||
{ rota: "/frameworks", prioridade: 0.8, frequencia: "monthly" },
|
||||
{ rota: "/componentes", prioridade: 0.8, frequencia: "weekly" },
|
||||
{ rota: "/livro", prioridade: 0.7, frequencia: "monthly", apenasPt: true },
|
||||
{ rota: "/glossario", prioridade: 0.7, frequencia: "monthly" },
|
||||
{ rota: "/catalogo", prioridade: 0.6, frequencia: "weekly" },
|
||||
{ rota: "/sobre", prioridade: 0.4, frequencia: "yearly" },
|
||||
];
|
||||
|
||||
const series = Object.keys(seriesData).map((chave) => ({
|
||||
caminho: `/series/${chave}`,
|
||||
rota: `/series/${chave}`,
|
||||
prioridade: 0.9,
|
||||
frequencia: "monthly",
|
||||
lastModified: dataRevisao,
|
||||
}));
|
||||
|
||||
const frameworks = Object.keys(frameworksData).map((chave) => ({
|
||||
caminho: `/frameworks/${encodeURIComponent(chave)}`,
|
||||
rota: `/frameworks/${encodeURIComponent(chave)}`,
|
||||
prioridade: 0.6,
|
||||
frequencia: "monthly",
|
||||
lastModified: dataRevisao,
|
||||
}));
|
||||
|
||||
return [...fixas, ...series, ...frameworks].map(({ caminho, prioridade, frequencia, lastModified }) => ({
|
||||
url: `${SITE.url}${caminho}`,
|
||||
lastModified,
|
||||
changeFrequency: frequencia,
|
||||
priority: prioridade,
|
||||
}));
|
||||
const todasRotas = [...fixas, ...series, ...frameworks];
|
||||
const entries = [];
|
||||
|
||||
for (const item of todasRotas) {
|
||||
for (const loc of ENABLED_LOCALES) {
|
||||
if (item.apenasPt && loc !== "pt") continue;
|
||||
|
||||
const href = getLocalizedHref(`/pt${item.rota}`, loc);
|
||||
const url = `${SITE.url}${href}`;
|
||||
|
||||
const languages = {};
|
||||
for (const l of ENABLED_LOCALES) {
|
||||
if (item.apenasPt && l !== "pt") continue;
|
||||
languages[LANG_MAP[l] || l] = `${SITE.url}${getLocalizedHref(`/pt${item.rota}`, l)}`;
|
||||
}
|
||||
|
||||
entries.push({
|
||||
url,
|
||||
lastModified: dataRevisao,
|
||||
changeFrequency: item.frequencia,
|
||||
priority: item.prioridade,
|
||||
alternates: {
|
||||
languages,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
Reference in new issue
Block a user