Saltar a contenido

RF-011 — Metadata y SEO: origen del dato por ruta

Estado La metadata de las páginas, implementada y verificada. robots.txt, sitemap.xml y noindex, desactivados a la espera del análisis
Tipo Estándar transversal — aplica a toda ruta del storefront
Alcance Un único país publicado: /py. El mercado es local
Ubicación src/lib/util/metadata.ts · src/app/layout.tsx · src/app/[countryCode]/**/page.tsx · src/proxy.ts · desactivado en src/app/_seo-en-revision/
Depende de RF-000 · RF-001 · RF-003 · grupo meta de Payload · catálogo de Medusa

Requisito

Publicar en cada ruta del sitio el título, la descripción, la URL canónica y la tarjeta para compartir en redes, de modo que buscadores y aplicaciones de mensajería muestren el contenido correcto; y dejar establecido, para cada ruta, de dónde sale ese dato: si lo edita el cliente en el CMS o si lo arma la página con lo que ya tiene.

Solución adoptada

Clasificar cada ruta en uno de tres orígenes, según tenga o no valor editorial.

flowchart TD
    R[Ruta del storefront] --> Q{¿El texto es una<br/>decisión de contenido?}
    Q -->|Sí — la escribe el cliente| CMS[Editable en Payload<br/>grupo meta]
    Q -->|No — se deriva del dato| AUTO[Automática<br/>Medusa · la propia página]
    Q -->|No — es una pantalla funcional| FIJA[Fija en el código<br/>+ noindex]
    CMS --> F[Respaldo del código si el campo viene vacío]
    AUTO --> F

La regla: una ruta es editable en Payload solo cuando su contenido también lo es. La home y las páginas de contenido son piezas de comunicación, y ahí el título y la descripción son una decisión del cliente. La ficha de producto, la categoría y la colección publican un dato del catálogo: su metadata se deriva de ese mismo dato y no se edita en ningún lado, porque son cientos de páginas y mantenerlas a mano es inviable. Las pantallas funcionales —carrito, checkout, cuenta, retornos de pago— no tienen contenido: título fijo y fuera del índice.

Estado al 2026-09-22. Lo que decide qué ve un buscador —robots.txt, sitemap.xml y el noindex de las pantallas funcionales— quedó desactivado y comentado a pedido, para analizarlo antes de publicarlo. El código está escrito y probado, en src/app/_seo-en-revision/, con su propio README. Ver Qué quedó desactivado. Todo el resto de este documento está activo.

Contrato por ruta

Toda ruta cuelga del país, y el único país publicado es /py.

Ruta Origen Título Descripción Imagen para compartir Canónica Índice
/py (home) Payload homepage.meta, salvo la descripción meta.title → respaldo Automática: las categorías de primer nivel de Medusa, hasta 160 caracteres (ver nota) → respaldo meta.image tamaño og → logo /py Sí
/py/[slug] (páginas de contenido) Payload page.meta meta.title → title de la página meta.description → respaldo meta.image tamaño og → imagen del home → logo /py/[slug] Sí
/py/products/[handle]/[[...variantId]] Automática (Medusa) Producto, más las opciones de la variante si la URL la trae De la variante si la URL la trae; si no, del producto — ver Variantes Foto de la variante, o miniatura del producto, en variante og Siempre al producto base Sí
/py/categories/[...category] Automática (Medusa) Nombre de la categoría Descripción de la categoría → Productos de {categoría} en Compulandia. Imagen del home → logo /py/categories/[ruta] Sí
/py/collections/[handle] Automática (Medusa) Título de la colección Colección {título} en Compulandia. Imagen del home → logo /py/collections/[handle] Sí
/py/store Fija Tienda Fija Logo (no declara tarjeta propia) Pendiente: el código no la declara — ver Pendientes Sí
/py/cart, /py/checkout Fija Carrito, Checkout Fija — — No
/py/account/** Fija Por pantalla Fija — — No
/py/order/[id]/**, /py/payment/**, /py/auth/** Fija Por pantalla Fija — — No
not-found Fija 404 Fija — — No

Descripción del home (2026-09-30). Por pedido del negocio es sólo dinámica: el campo meta.description de la homepage en Payload ya no se usa. Se arma como "Tienda de tecnología: . Envíos a todo el Paraguay." Si no entran en 160 caracteres, se nombran las que entran y se cierra con "y más". Si Medusa no responde, queda el texto de respaldo. Es la única excepción a la regla de que lo editable en Payload se edita en Payload.

El sufijo de marca lo agrega el template %s | Compulandia del layout raíz: ninguna ruta lo concatena por su cuenta, salvo la home, que usa absolute porque su título ya nombra la marca.

Reglas del estándar

  1. Cadena de resolución. Todo campo se resuelve en este orden: valor del CMS → valor derivado del dato → respaldo escrito en el código. Ninguna página puede quedar sin título ni sin descripción, aunque el CMS devuelva el campo vacío o no responda.
  2. Un solo texto vacío no es un texto. Los valores del CMS se comparan ya recortados: un campo con espacios cuenta como ausente y cae al respaldo.
  3. Toda ruta indexable declara su canónica, con el país incluido y empezando por barra: /py/.... El país no se omite aunque sea uno solo — la URL real lo lleva, y una canónica que no coincide con la URL que se sirve no la respeta ningún buscador.
  4. El dominio sale de NEXT_PUBLIC_BASE_URL, y de ahí salen las canónicas, el og:url, el robots.txt y el sitemap.xml. Es NEXT_PUBLIC_*: se congela en el build. La imagen de producción tiene que construirse con el dominio de producción; si se construyera con el de desarrollo, cada página de la tienda real declararía como canónica la de dev-storefront, que es decirle al buscador que el sitio bueno es el de pruebas.
  5. Un solo país publicado. El sitio se sirve en /py y no se declara relación entre versiones de país: no hay otras. Mientras siga así, ninguna ruta necesita alternates.languages, y el sitemap.xml sólo lista URLs bajo /py. Abrir un segundo mercado obliga a volver sobre esta regla antes de publicarlo, o las dos versiones compiten como contenido duplicado.
  6. La ficha de producto apunta siempre al producto base, aunque la URL traiga variante: las variantes no compiten entre sí en el buscador.
  7. Las imágenes para compartir se piden en tamaño og (1200×630): en Payload es el tamaño og de la colección de medios; en las fotos del catálogo, la variante og de Cloudflare Images. Sin imagen propia se usa la del home en Payload (homepage.meta.image, tamaño og) y, si tampoco está, el logo del sitio, app/opengraph-image.jpg (2026-09-30). Las pantallas que no declaran tarjeta propia, como /py/store, muestran directamente el logo.
  8. No se declaran alto y ancho de la imagen: las variantes de Cloudflare Images no los informan.
  9. Las pantallas funcionales llevan noindex y no declaran canónica ni tarjeta. (Regla escrita y desactivada: ver Qué quedó desactivado.)
  10. Con una variante en la URL manda el dato de la variante, y sólo se cae al producto padre cuando la variante no trae nada — decisión temporal, ver Variantes.
  11. Las opciones de la variante se ordenan como el producto las declara, no como las devuelve la base: si no, dos variantes del mismo producto leen distinto.
  12. Un texto que al limpiarlo no llega a una frase, no es un texto. Vale tanto para el "" que devuelve Medusa en las categorías como para el punto suelto que deja el integrador en algunas short_description: los dos caen al respaldo.
  13. Una consulta por render. Si generateMetadata() y la página piden el mismo dato, la consulta se memoiza (cache() de React, como getHomepagePublicada()), no se duplica.
  14. Nada bloquea a los robots. Ninguna redirección ni cookie puede ser condición para leer una página: no guardan cookies y quedan en bucle.

Variantes: por qué el dato sale de la variante

Decisión del 2026-09-22, y es temporal. En la ficha, la metadata se arma con lo que tiene la variante seleccionada —su descripción corta, su foto, sus opciones en el título— y sólo cae al producto padre cuando la variante no trae nada. No es lo que recomienda el manual: es lo que permite el dato que hay hoy.

Medido sobre una muestra de 500 productos del catálogo (15.782 en total):

Productos padre con una sola imagen o ninguna 77%
Productos padre sin descripción utilizable 11%
Variantes con metadata.short_description cargada 85%
Variantes con foto propia 28%

El padre casi siempre tiene una descripción, pero es prosa genérica que vale para todas sus variantes; la ficha técnica concreta —capacidad, color, memoria— vive en la short_description de la variante, y está cargada en el 85%. Publicar la del padre en la página de una variante sería prometer algo que la página no dice: el resultado de búsqueda hablaría del producto en general mientras la persona aterriza en el de 256GB negro.

Lo recomendado, para cuando el dato del padre esté enriquecido: que la página indexable sea la del producto y que su metadata salga del padre —descripción propia, galería propia—, dejando a la variante sólo lo que hace falta para la tarjeta al compartir: su foto y su título. Eso da un resultado de búsqueda estable por producto y evita que el texto cambie según por qué variante entró el buscador.

Qué destraba el cambio: que los productos padre tengan descripción propia y galería propia. Cuando eso pase, se revisa esta sección y se invierte la cadena de resolución en generateMetadata() de la ficha. La canónica ya apunta al producto base, así que ese cambio no mueve nada de lo que el buscador tiene indexado.

Dependencia de datos

El grupo meta (title, description, image) existe hoy en las colecciones homepage y pages de Payload, provisto por el plugin de SEO, y la colección de medios genera el tamaño og. Cualquier campo nuevo en el CMS corresponde al equipo que lo mantiene: desde este repositorio el CMS es de solo lectura.

Para el catálogo, la metadata se deriva de lo que Medusa ya publica —título, descripción, miniatura y fotos de variante— sin campos nuevos.

Criterios de aceptación

Verificado con la respuesta del servidor quiere decir: se pidió la página y se leyeron sus etiquetas. Verificado en pantalla, que falta en varios, quiere decir pegar el enlace en WhatsApp o en el validador de la red y mirar la tarjeta.

# Criterio Estado
1 Cargar título e imagen de la home desde el CMS; la descripción se arma con las categorías (2026-09-30) Implementado · título e imagen verificados en pantalla; descripción verificada en la respuesta del servidor local
2 Vaciar esos campos en el CMS y comprobar que la home sigue publicando los textos de respaldo Implementado · no se prueba: está previsto que el meta de la home esté siempre cargado. El mismo respaldo se verificó en las páginas de contenido con el meta vacío
3 Compartir el enlace de una variante y ver su propia foto, y el título con las opciones de la variante Implementado · verificado en pantalla
4 Comprobar que la canónica de la ficha apunta al producto base, con variante en la URL o sin ella Implementado · verificado en pantalla
5 Leer cualquier página del sitio sin cookies (como un robot) y recibir contenido, no una redirección Implementado · verificado
6 Publicar título, descripción, imagen y canónica en las páginas de contenido del CMS Implementado · verificado en pantalla, con el meta cargado y vacío
7 Formar bien la canónica de categoría, con país y barra inicial Implementado · verificado en pantalla
8 Declarar la canónica de colección Implementado · verificado en pantalla
9 Servir robots.txt y sitemap.xml, con URLs bajo /py únicamente Verificado y desactivado
10 Mantener carrito, checkout, cuenta y retornos de pago fuera del índice Verificado en pantalla y desactivado
11 Entrar al sitio sin país en la URL y aterrizar en /py Implementado · verificado (307 a /py)
12 Publicar una imagen en toda tarjeta: la propia, la del home o el logo Implementado · verificado en la respuesta del servidor local el 2026-09-30 (categoría → imagen del home; página con imagen → la suya; carrito → logo)
13 No indexar las páginas de los países de la región "Europe" Verificado y desactivado
14 Publicar la descripción de la variante en la ficha de una variante, y la del producto en la base Implementado · verificado en pantalla
15 Leer igual las opciones de dos variantes del mismo producto, en el orden que declara el producto Implementado · verificado en pantalla
16 Publicar descripción en la categoría aunque Medusa la devuelva vacía Implementado · verificado en pantalla

Lo que se construyó

Pieza Para qué Estado
src/lib/util/metadata.ts El país publicado y la tarjeta para compartir, en un solo lugar Activo
generateMetadata() de home, [slug], ficha, categoría y colección El contrato de arriba Activo
src/app/_seo-en-revision/robots.ts /robots.txt: permitía el sitio, bloqueaba /api/, /py/auth/, /py/payment/ y /py/order/, y declaraba el sitemap Desactivado
src/app/_seo-en-revision/sitemap.ts + datos-del-sitemap.ts /sitemap.xml: 15.926 URLs bajo /py —15.782 productos, 135 categorías, las colecciones y las páginas del CMS—, revalidado cada hora Desactivado
listPublishedPages() en src/lib/payload.ts Las páginas publicadas del CMS para el sitemap; los borradores no se ofrecen Sin uso mientras el sitemap esté desactivado
SIN_INDEXAR en src/lib/util/metadata.ts El noindex de las 19 pantallas funcionales Desactivado
X-Robots-Tag en src/proxy.ts Dejaba fuera del índice los países de la región "Europe" sin romper el selector de país Desactivado

Qué quedó desactivado

El 2026-09-22 se comentó todo lo que decide qué ve un buscador del sitio, para analizarlo con el analista antes de publicarlo. La decisión no es técnica: define qué partes del sitio entran al índice y cuáles no.

El código está escrito, probado y verificado en pantalla. Vive en src/app/_seo-en-revision/, con un README que detalla qué hacía cada pieza, cómo reactivarlo en cuatro pasos y qué conviene llevar a esa conversación. Next no enruta las carpetas que empiezan con _, así que esos archivos no publican nada. El noindex y la cabecera del proxy, que no se pueden mover de lugar, quedaron comentados donde estaban: grep -rn "SIN_INDEXAR" src/.

Lo que esto significa mientras siga así:

  • No existen /robots.txt ni /sitemap.xml. Los 15.782 productos se descubren sólo siguiendo enlaces, que es más lento.
  • El carrito, el checkout, la cuenta y los retornos de pago vuelven a ser indexables.
  • Las siete copias del catálogo de la región "Europe" (it, dk, fr, de, es, se, gb) también.

Lo que no se tocó: toda la metadata de las páginas —títulos, descripciones, canónicas y tarjetas de home, páginas del CMS, ficha, categoría y colección— sigue activa. Y sigue activo el arreglo del bucle de redirección en src/proxy.ts, que vive en el mismo archivo pero es otra cosa: es lo que permite que un buscador o una vista previa de enlace puedan leer cualquier página.

Decisiones que conviene conocer

  • Las páginas privadas llevan noindex, no Disallow. Si se bloquearan en el robots.txt el buscador no entraría, y por lo tanto nunca leería el noindex: la URL podría aparecer igual en los resultados, sin título ni descripción. Disallow queda para lo que no tiene sentido ni leer.
  • El Medusa local y el del integrador (192.168.0.220) tienen una región "Europe" heredada de la demo (it, dk, fr, de, es, se, gb; en producción no está confirmada) y esos países sirven las mismas páginas con otro precio. Se los marca noindex por cabecera en vez de redirigirlos a /py: el selector de país sigue funcionando y el buscador entiende la cabecera igual que la etiqueta. Si el negocio confirma que esas regiones no van, corresponde borrarlas en Medusa, que es donde están de verdad.
  • La imagen global se nombra a mano. Next completa la tarjeta con app/opengraph-image.jpg sólo cuando la página no declara openGraph; en cuanto lo declara —aunque sea sin imágenes— el archivo deja de aplicarse. Sin nombrarla, toda página con tarjeta propia y sin imagen propia se publicaba sin ninguna.
  • El sitemap se arma con pedidos de 2.000 productos en tandas de a cuatro. Paginando de a cien tardaba más de un minuto y el build lo daba por fallido.

Pendientes conocidos

  • El listado /py/store no declara canónica, y sus filtros viajan en la URL (?category=, ?brand=, ?product_tag=, ?collection=, ?index=, ?q=). El menú del sitio enlaza a varias de esas combinaciones. Hoy cada una es una página distinta para el buscador, con el mismo título "Tienda" y la misma descripción. Lo propuesto:
  • canónica /py/store sin parámetros para el listado y sus filtros;
  • noindex, follow para la búsqueda (?q=). Va en la misma revisión con el analista que el resto de lo desactivado;
  • que el menú enlace a las páginas reales cuando existen: "Celulares" a /py/categories/celulares, no a /py/store?category=Celulares.

  • NEXT_PUBLIC_DEFAULT_REGION vale py sólo en el .env.local. Es una variable que se inlinea en el build, y el valor de los servidores vive en Secret Manager: hay que cambiarlo ahí y reconstruir la imagen. Mientras valga us —que no es una región de Medusa— entrar sin país en la URL aterriza en la primera región que Medusa devuelva, en el orden que venga.

  • Las 135 categorías tienen la descripción vacía en Medusa ("", no null). El respaldo las cubre, pero publica Productos de {nombre} en Compulandia. en las 135, cambiando sólo el nombre. Para las categorías con más tráfico conviene cargar una descripción real; es trabajo de contenido.
  • Los textos alternativos de las imágenes del CMS vienen con el nombre del archivo (la de la home dice memoria-ram-pnb-ddr4-...). Es carga de contenido, no código.
  • Las páginas del CMS no tienen descripción cargada, así que se publican sin og:description. No hay respaldo escrito a propósito: un texto genérico repetido en todas es peor que ninguno.
  • Falta probar las tarjetas en pantalla: pegar un enlace de la home, de una ficha con variante y de una página del CMS en WhatsApp y ver qué muestra.