# General Translation Knowledge Base: ¿Cómo creo un mapa del sitio para un sitio web multilingüe?
URL: https://generaltranslation.com/es/kb/multilingual-sitemap.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: Incluye cada versión de idioma de cada página como una entrada url propia. Si el mapa del sitio contiene tu hreflang, añade a cada entrada versiones alternativas xhtml:link para todas las versiones, incluida ella misma, además de un x-default opcional, y divide los archivos al llegar a 50.000 URL o 50 MB. gt-next de General Translation asigna a cada configuración regional su propia URL para que el mapa del sitio la incluya.
Category: SEO multilingüe
Last updated: 2026-10-28
Authors: General Translation Team
Published: 2026-10-28

Un mapa del sitio multilingüe incluye cada versión de idioma de una página como una entrada `<url>` independiente, de modo que `/en/pricing`, `/de/preise` y `/pt-BR/precos` son tres entradas distintas. Si el mapa del sitio también declara hreflang, cada entrada enumera todas las versiones de esa página, incluida ella misma, como versiones alternativas `xhtml:link`. Google trata el hreflang de un mapa del sitio igual que el declarado en el HTML, por lo que el mapa del sitio puede sustituir las etiquetas de tus páginas, siempre que esté completo y sea coherente con tus URL canónicas.

## ¿Hreflang debe ir en el mapa del sitio o en tu HTML?

La [guía de versiones localizadas](https://developers.google.com/search/docs/specialty/international/localized-versions) de Google describe tres formas equivalentes de declarar las versiones de idioma e indica que usar más de una no aporta ninguna ventaja en la Búsqueda. Todas las URL de idioma que quieras indexar deben seguir figurando en el mapa del sitio; lo único que hay que decidir es dónde colocar las anotaciones hreflang.

| Método                            | Dónde se ubica                           | Conviene cuando                                                                                                                       | Ten en cuenta                                                           |
| --------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Etiquetas HTML `<link>`           | El `<head>` de cada página               | Tu framework renderiza los metadatos por página, como `alternates.languages` de Next.js                                               | Cada plantilla tiene que emitir el conjunto completo                    |
| Cabecera HTTP `Link`              | Cabeceras de respuesta                   | Archivos que no son HTML, como los PDF                                                                                                | Configuración del servidor por tipo de archivo                          |
| `xhtml:link` en el mapa del sitio | Un mapa del sitio o un conjunto de ellos | Un CMS o un paso de compilación ya conoce todas las páginas y configuraciones regionales, o no puedes editar las plantillas de página | Las páginas que no están en el mapa del sitio se quedan sin anotaciones |

Usa etiquetas HTML cuando tu framework ya las renderice por página, y el mapa del sitio cuando un único punto de tu compilación conozca todas las páginas y sus configuraciones regionales. No mantengas ambos a mano: dos listas acaban desincronizándose. (Consulta [qué es hreflang](/kb/what-is-hreflang) para ver la sintaxis de la etiqueta y la cabecera HTTP).

## Cómo estructurar un mapa del sitio multilingüe

Estas reglas provienen de la [guía de versiones localizadas](https://developers.google.com/search/docs/specialty/international/localized-versions) de Google, de su [guía de mapas del sitio](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap) y del [protocolo de mapas del sitio](https://www.sitemaps.org/protocol.html):

* **Un `<url>` por versión.** Una página en tres idiomas equivale a tres entradas, cada una con su propio `<loc>`.
* **Cada entrada enumera todas las versiones, incluida ella misma.** El conjunto de `xhtml:link` es idéntico en todas las entradas de una página, y Google ignora cualquier par de páginas que no se enlacen recíprocamente.
* **`x-default` es opcional.** Agrégalo para un selector de idioma o para una versión predeterminada destinada a los visitantes cuyo idioma no ofreces.
* **Declara el espacio de nombres.** El `<urlset>` necesita un atributo `xmlns:xhtml` con el valor `http://www.w3.org/1999/xhtml`.
* **Usa URL finales y canónicas.** Deben ser absolutas (`https://example.com/de/preise`, no `/de/preise`), devolver `200` y no estar marcadas como `noindex`.
* **Códigos válidos.** Un idioma ISO 639-1, opcionalmente con una región ISO 3166-1 alfa-2 (`en-GB`) o un sistema de escritura ISO 15924 (`zh-Hant`).
* **Escapa y codifica.** El archivo debe estar en UTF-8, `&` se escribe `&amp;` y las rutas con caracteres no ASCII se codifican con porcentajes, de modo que `/de/über-uns` se convierte en `/de/%C3%BCber-uns`.
* **`lastmod` por versión.** Google solo lo tiene en cuenta si es fiable de forma sistemática, es decir, si refleja la fecha del último cambio significativo de esa URL. Una traducción al alemán actualizada un día distinto que la página en inglés lleva su propia fecha.

Cada archivo admite como máximo 50 000 URL y 50 MB sin comprimir. Los hijos `xhtml:link` no cuentan para el límite de URL, pero sí para el tamaño del archivo. Con URL de unos 45 caracteres, cada alternativa ocupa unos 100 bytes, y una entrada con 20 idiomas, unos 2,2 KB. Según nuestros cálculos, un archivo alcanza los 50 MB con unas 23 700 entradas de este tipo, mucho antes de llegar a las 50 000.

### Divide por configuración regional con un índice de mapas del sitio

Usar un archivo por configuración regional mantiene los archivos pequeños en la mayoría de los sitios, pero no garantiza que no se superen los límites. Según la estimación anterior, una configuración regional con más de unas 23 700 páginas en 20 idiomas necesita más de un archivo, así que divídela aún más; por ejemplo, en `sitemap-de-1.xml` y `sitemap-de-2.xml`. Basta con enviar el índice para que Google lea todos los archivos que incluye; enviar también cada archivo por separado es opcional y hace que cada uno tenga su propia fila en el informe Sitemaps de Search Console. Un [índice de mapas del sitio](https://developers.google.com/search/docs/crawling-indexing/sitemaps/large-sitemaps) enumera los archivos:

```xml title="sitemap.xml"
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://example.com/sitemap-en.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://example.com/sitemap-de.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://example.com/sitemap-pt-br.xml</loc>
  </sitemap>
</sitemapindex>
```

Cada archivo de configuración regional enumera solo sus propias URL, pero cada entrada sigue conteniendo el conjunto completo de versiones alternativas de la página:

```xml title="sitemap-de.xml"
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:xhtml="http://www.w3.org/1999/xhtml">
  <url>
    <loc>https://example.com/de/preise</loc>
    <lastmod>2026-10-03</lastmod>
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/pricing"/>
    <xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/preise"/>
    <xhtml:link rel="alternate" hreflang="pt-BR" href="https://example.com/pt-BR/precos"/>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/pricing"/>
  </url>
  <url>
    <loc>https://example.com/de/ueber-uns</loc>
    <lastmod>2026-09-12</lastmod>
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about"/>
    <xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/ueber-uns"/>
    <xhtml:link rel="alternate" hreflang="pt-BR" href="https://example.com/pt-BR/sobre"/>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about"/>
  </url>
</urlset>
```

`sitemap-en.xml` y `sitemap-pt-br.xml` tienen la misma estructura, cada uno con sus propios valores de `<loc>` y `lastmod`. Guarda los archivos en la raíz del sitio: según Google, un mapa del sitio solo abarca las URL ubicadas dentro de su propio directorio, salvo que lo envíes a través de Search Console, y un índice solo puede hacer referencia a mapas del sitio que estén en su mismo directorio o en uno inferior.

## Cómo generar uno en Next.js

A partir de Next.js 14.2, las versiones alternativas `xhtml:link` se generan desde el campo `alternates.languages` de cada entrada en [`app/sitemap.ts`](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap#generate-a-localized-sitemap). Esta implementación construye las versiones alternativas a partir de una única lista de configuraciones regionales y slugs localizados, de modo que cada entrada se incluye a sí misma y a `x-default`. `i18n/routing.ts` contiene esas listas e indica qué páginas sirve cada configuración regional; en [la sección sobre General Translation](#how-general-translation-fits) se muestra uno que lee `gt.config.json`.

```ts title="app/sitemap.ts"
import type { MetadataRoute } from 'next';
import {
  BASE_URL,
  DEFAULT_LOCALE,
  LOCALES,
  localeRoutes,
  pathConfig,
} from '@/i18n/routing';

// Rutas compartidas de las páginas que quieres indexar.
const PAGES = ['/', '/about', '/pricing'];

// Una configuración regional ofrece una página salvo que localeRoutes la excluya.
const serves = (locale: string, page: string) =>
  !localeRoutes[locale] || localeRoutes[locale].includes(page);

// La URL pública de una página en una configuración regional, con su slug localizado si existe.
function localeUrl(locale: string, page: string) {
  const path = pathConfig[page]?.[locale] ?? page;
  return `${BASE_URL}/${locale}${path === '/' ? '' : path}`;
}

export default function sitemap(): MetadataRoute.Sitemap {
  return PAGES.flatMap((page) => {
    const locales = LOCALES.filter((locale) => serves(locale, page));

    // Todas las versiones de la página, incluida la de <loc>, más x-default.
    const languages: Record<string, string> = Object.fromEntries(
      locales.map((locale) => [locale, localeUrl(locale, page)]),
    );
    languages['x-default'] = localeUrl(DEFAULT_LOCALE, page);

    // Un <url> por versión, cada uno con las mismas versiones alternativas.
    return locales.map((locale) => ({
      url: localeUrl(locale, page),
      alternates: { languages },
    }));
  });
}
```

Compilada con Next.js 16.4.3 y `gt-next` 11.4.16, la página `/about` generó cuatro entradas como esta, cada una con los mismos cinco enlaces:

```xml title="/sitemap.xml"
<url>
<loc>https://example.com/en/about</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/ueber-uns" />
<xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/qui-sommes-nous" />
<xhtml:link rel="alternate" hreflang="pt-BR" href="https://example.com/pt-BR/about" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />
</url>
```

Tres detalles que la documentación de Next.js no explica:

* **Agrega la autorreferencia.** El ejemplo localizado de la documentación de Next.js solo incluye las versiones alternativas `es` y `de` dentro de la URL en inglés y no asigna entradas propias a las URL en español y en alemán. Google espera que cada entrada se incluya a sí misma y que cada versión tenga su propio `<url>`.
* **Escapa las URL por tu cuenta.** En nuestra prueba, Next.js 16.4.3 escribió las URL en el XML tal cual: un `&` en una cadena de consulta generó un archivo que `xmllint` rechaza, y la `ü` quedó sin codificar. No incluyas cadenas de consulta en las URL del mapa del sitio y construye las rutas con `new URL(path, BASE_URL).href`, que aplica la codificación porcentual.
* **Dividir no crea un índice.** [`generateSitemaps`](https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps) puede devolver un id por configuración regional, y Next.js sirve cada archivo en `/sitemap/en.xml`, `/sitemap/de.xml`, etc. En nuestra prueba, `/sitemap.xml` pasó a devolver un 404, así que envía cada archivo en Search Console, lo que además elimina la restricción de directorio de Google para los archivos dentro de `/sitemap/`.

## Errores comunes

* **Faltan autorreferencias o enlaces de retorno.** Si `/en/pricing` incluye `/de/preise`, pero `/de/preise` no incluye `/en/pricing`, Google ignora el par. Esto suele ocurrir cuando se añade una configuración regional a algunas páginas, pero no a otras.
* **Códigos no válidos.** `en-UK` no tiene ningún efecto porque `UK` es un código reservado; usa `en-GB`. `es-419` es un código BCP 47 válido, pero hreflang no lo admite; usa `es` o códigos de países concretos, como `es-MX`.
* **URL que redirigen.** Incluye la URL para la que tu servidor responde con `200`. Con los slugs del ejemplo anterior, `/de/about` redirige a `/de/ueber-uns`, por lo que en el mapa del sitio solo debe figurar el slug localizado.
* **Páginas con `noindex` o sin traducir.** Un mapa del sitio es la lista de URL que quieres que aparezcan en los resultados de búsqueda. Omite las configuraciones regionales que aún no se hayan publicado y elimínalas también de las versiones alternativas de las demás entradas.
* **`lastmod` con la fecha de compilación.** Regenerar el mapa del sitio no supone un cambio de contenido. Usa la fecha de la última modificación de cada versión u omite el campo.

## Cómo comprobarlo antes de enviarlo

Realiza tres comprobaciones antes de enviar el mapa del sitio desde el informe Sitemaps de Search Console o de añadir una línea `Sitemap:` a `robots.txt`:

1. **El XML está bien formado:** `xmllint --noout sitemap-de.xml`.
2. **Cada grupo está completo y es recíproco.** Este script lee uno o varios archivos de mapa del sitio e informa de las entradas que no se incluyen a sí mismas, las versiones alternativas que no tienen entrada propia, los conjuntos que no coinciden y los códigos mal formados:

   ```python title="check_sitemap.py"
   import re
   import sys
   import xml.etree.ElementTree as ET

   NS = {
       's': 'http://www.sitemaps.org/schemas/sitemap/0.9',
       'x': 'http://www.w3.org/1999/xhtml',
   }
   # Idioma, sistema de escritura opcional, región opcional: detecta es-419, pero no todos los códigos no válidos.
   CODE = re.compile(r'^[a-z]{2}(-[a-z]{4})?(-[a-z]{2})?$', re.IGNORECASE)
   RESERVED = ('-uk', '-eu', '-un')

   # Lee cada <url> de cada archivo de mapa del sitio: loc -> {hreflang: href}.
   entries = {}
   for path in sys.argv[1:]:
       for url in ET.parse(path).getroot().findall('s:url', NS):
           loc = url.findtext('s:loc', namespaces=NS).strip()
           entries[loc] = {
               link.get('hreflang'): link.get('href')
               for link in url.findall('x:link', NS)
               if link.get('rel') == 'alternate'
           }

   problems = []
   for loc, alternates in entries.items():
       if alternates and loc not in alternates.values():
           problems.append(f'{loc} does not list itself')
       for lang, href in alternates.items():
           if lang == 'x-default':
               continue
           if not CODE.match(lang) or lang.lower().endswith(RESERVED):
               problems.append(f'{loc} uses an invalid hreflang code: {lang}')
           if href not in entries:
               problems.append(f'{loc} lists {href}, which has no <url> entry')
           elif entries[href] != alternates:
               problems.append(f'{href} does not list the same alternates as {loc}')

   print('\n'.join(problems) or f'{len(entries)} URLs, no problems found')
   sys.exit(1 if problems else 0)
   ```

   Ejecútalo con todos los archivos de configuración regional a la vez, `python3 check_sitemap.py sitemap-en.xml sitemap-de.xml sitemap-pt-br.xml`; con el ejemplo anterior, muestra `6 URLs, no problems found`.
3. **Todas las URL responden con `200`:**

   ```bash
   curl -s https://example.com/sitemap-de.xml | grep -o '<loc>[^<]*' | sed 's/<loc>//' |
     while read -r url; do echo "$(curl -s -o /dev/null -w '%{http_code}' "$url") $url"; done
   ```

## Cómo encaja General Translation

`gt-next` de General Translation asigna a cada configuración regional su propia URL, que es precisamente lo que recoge un mapa del sitio multilingüe. [`createNextMiddleware`](/docs/react/nextjs/reference/functions/create-next-middleware) sirve las rutas `/de/...` y `/pt-BR/...`; `prefixDefaultLocale: true` coloca también el idioma de origen bajo `/en/`; [`pathConfig`](/docs/react/nextjs/reference/functions/create-next-middleware#path-config) asocia una página a un slug localizado, y [`localeRoutes`](/docs/react/nextjs/reference/functions/create-next-middleware#locale-routes) limita las páginas que sirve cada configuración regional. Define estos ajustes en un único módulo e impórtalo tanto en el middleware como en el mapa del sitio, de modo que el mapa del sitio incluya exactamente las URL a las que responde el middleware:

```ts title="i18n/routing.ts"
import gtConfig from '../gt.config.json';

export const BASE_URL = 'https://example.com';
export const DEFAULT_LOCALE = gtConfig.defaultLocale;

// Todas las configuraciones regionales que ofreces, tomadas de gt.config.json, con el idioma de origen primero.
export const LOCALES = [...new Set([gtConfig.defaultLocale, ...gtConfig.locales])];

// Se comparte con proxy.ts para que el mapa del sitio incluya exactamente las URL que sirve el middleware.
export const pathConfig: Record<string, Record<string, string>> = {
  '/about': { de: '/ueber-uns', fr: '/qui-sommes-nous' },
};
export const localeRoutes: Record<string, string[]> = {
  'pt-BR': ['/', '/about'], // la página de precios aún no está traducida al portugués de Brasil
};
```

```ts title="proxy.ts"
import { createNextMiddleware } from 'gt-next/middleware';
import { localeRoutes, pathConfig } from './i18n/routing';

export default createNextMiddleware({
  prefixDefaultLocale: true,
  pathConfig,
  localeRoutes,
});

export const config = {
  matcher: ['/((?!api|static|.*\\..*|_next).*)'],
};
```

Con `en`, `de`, `fr` y `pt-BR` en `gt.config.json`, la compilación generó 11 entradas: tres páginas en cuatro configuraciones regionales, menos la página de precios en portugués de Brasil. Esa URL sigue funcionando para los visitantes, ya que el middleware redirige `/pt-BR/pricing` a `/en/pricing` con un `307`, y una URL que redirige no debe figurar en un mapa del sitio. Todas las URL incluidas devolvieron `200` y el verificador anterior no detectó ningún problema. La comprobación `serves` compara rutas exactas, así que, si tus `localeRoutes` usan patrones como `/blog/[[...slug]]`, desglósalos en las páginas concretas que incluyas.

Si enrutas una configuración regional mediante un alias, como `/cn` para el chino, usa el código canónico en la clave de hreflang y el alias en la URL; la [guía de SEO para alias de configuración regional](/docs/react/nextjs/locale-alias-seo#sitemap) hace esta conversión con [`resolveCanonicalLocale`](/docs/react/nextjs/reference/functions/resolve-canonical-locale). Para traducir los títulos y las descripciones de esas mismas páginas, consulta [¿Cómo traduzco los títulos SEO y las metadescripciones?](/kb/translate-seo-metadata).

## Conclusiones clave

* Asigna a cada versión de idioma su propia entrada `<url>` e incluye todas las URL que quieras indexar, sea cual sea el método de hreflang que utilices.
* En un mapa del sitio con hreflang, cada entrada enumera todas las versiones de la página, incluida ella misma, con el mismo conjunto en todas las entradas; `x-default` es opcional.
* Cada archivo admite como máximo 50.000 URL y 50 MB; las versiones alternativas no cuentan para el límite de URL, pero sí para el tamaño, así que con muchas configuraciones regionales se alcanza antes el límite de tamaño.
* Divide los mapas del sitio por configuración regional (y subdivide los de las configuraciones regionales más grandes) bajo un índice de mapas del sitio en la raíz del sitio; envía el índice después de comprobar la reciprocidad y los códigos de estado.
* Con `gt-next`, comparte `pathConfig` y `localeRoutes` entre el middleware y `app/sitemap.ts` para que el mapa del sitio incluya exactamente las URL que sirve tu aplicación.

## Related questions

- [¿Qué es hreflang y cómo se configura?](https://generaltranslation.com/es/kb/what-is-hreflang.mdx): Hreflang le indica a Google qué URL son versiones de la misma página en otros idiomas o regiones, para que pueda mostrar la adecuada en los resultados de búsqueda. Agrégalo con etiquetas link de HTML, cabeceras HTTP Link o un mapa del sitio, asegúrate de que todos los enlaces sean recíprocos y usa códigos válidos como en-GB, no en-UK.
- [¿Cómo configuro el SEO de una aplicación de Next.js multilingüe?](https://generaltranslation.com/es/kb/multilingual-nextjs-seo.mdx): Asigna a cada idioma su propia URL con prefijo, como /en/about y /es/about, y luego publica html lang, una URL canónica autorreferente, versiones alternativas hreflang con x-default, un título y una descripción traducidos, y una entrada en el mapa del sitio para cada idioma publicado. Marca con noindex las versiones no publicadas. gt-next de General Translation dirige cada idioma a su propia URL y traduce los títulos con getGT.
- [Redirecciones de idioma y SEO: ¿URL, cookie o Accept-Language?](https://generaltranslation.com/es/kb/locale-routing-seo.mdx): Deja que la URL determine el idioma de cada página que quieras indexar, porque Googlebot no envía la cabecera Accept-Language y rastrea sobre todo desde direcciones de EE. UU. Recurre a la preferencia guardada en una cookie y, en su defecto, a Accept-Language solo para decidir adónde redirige una raíz neutral como /, mediante una redirección temporal 302 o 307.
- [Subdirectorio vs. subdominio vs. ccTLD para SEO multilingüe](https://generaltranslation.com/es/kb/multilingual-url-structure.mdx): Usa subdirectorios como example.com/de/ cuando un mismo producto comparte sitio y proceso de lanzamiento. Solo un ccTLD indica un país, los subdominios permiten un alojamiento independiente, Google desaconseja los parámetros ?lang= y trata los dominios personalizados como .io y .ai como genéricos.
- [¿Cómo traduzco los títulos SEO y las metadescripciones?](https://generaltranslation.com/es/kb/translate-seo-metadata.mdx): Escribe el título y la metadescripción de cada versión lingüística en su propio idioma, con las palabras que busca ese mercado, que a menudo difieren de una traducción literal. Después, localiza las etiquetas que los acompañan, como Open Graph, la URL canónica, hreflang, html lang y los datos estructurados. gt-next de General Translation traduce títulos y descripciones dentro de generateMetadata con getGT.

## Previous and next questions

- Previous question: [¿Cómo agrego i18n a una aplicación de Next.js con Pages Router?](https://generaltranslation.com/es/kb/add-i18n-to-nextjs-pages-router.mdx)
- Next question: [¿Cómo localizo una app de Android con strings.xml?](https://generaltranslation.com/es/kb/localize-android-app.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/es/kb/sitemap.md) for every article.
