Saltar al contenido
Framework Ecosystems LabsUn contrato, muchos ecosistemas, la misma prueba.

Clase 101 — Metadatos y descubribilidad#

⬅️ Clase 100 · 📚 Parte 7 · 🎓 Clases · 102 ➡️ Parte 7 — Renderizado y full-stack · Nivel 🔴 avanzado · Pista fullstack (Renderizado y full-stack) ✅ Clase construida — 5 implementaciones verificadas contra contrato.json.

🎯 Objetivo#

Un rastreador de una red social no ejecuta JavaScript. Pide la página, lee la cabecera del documento y se va.

Así que todo lo que decide cómo se ve tu enlace cuando alguien lo comparte —título, descripción, imagen, tipo— tiene que estar en el primer HTML. Esta clase comprueba que está, en las cinco, y compara cinco formas de escribirlo que no son intercambiables.

📚 Resultados de aprendizaje#

Al terminar podrás:

🧩 La situación#

Dos rutas: una portada y un artículo. Cada una con su título, su descripción, su enlace canónico y sus etiquetas de Open Graph. El artículo, además, con su grafo de schema.org en JSON-LD.

Nada de eso se ve en la pantalla. Todo eso es lo único que ve quien comparte el enlace.

🧮 El contrato#

# Petición Qué comprueba
1 GET / título, descripción, og:* y canónico de la portada
2 GET /articulo/hola-mundo los suyos, que son otros
3 GET /articulo/hola-mundo y no arrastra el de la portada
4 GET /articulo/hola-mundo su grafo de schema.org, sin escapar
5 GET /metadatos.json los dos títulos salen del servidor y son distintos
6 GET /metadatos.json cómo se escriben y si esa forma evita duplicados

El caso 3 es el que encuentra el fallo real, y es un cuerpo_no_contiene:

        "cuerpo_no_contiene": ["<title>Tareas de Ada</title>"]

Sin él, una implementación con un título por omisión en la plantilla pasaría los casos 1 y 2 y estaría indexando las dos páginas con el mismo nombre. Pasó exactamente eso al construir esta clase, y está contado abajo.

<!-- generado: fichas -->

🧰 Las piezas de esta clase, una por una#

Antes del código: qué es cada framework, qué versión se está usando y qué hace falta para ejecutarlo. Todo lo de esta sección sale de los archivos reales del repositorio —el catálogo, la receta de arranque y el manifiesto de dependencias de cada ecosistema—, así que no puede quedarse desactualizado sin que la validación lo detecte.

Framework Qué es Desde Licencia Quién lo mantiene
Next.js react-metaframework de JavaScript/TypeScript (TypeScript) 2016 MIT Vercel
Nuxt vue-metaframework de JavaScript/TypeScript (TypeScript) 2016 MIT proyecto independiente
SvelteKit svelte-metaframework de JavaScript/TypeScript (TypeScript) 2022 MIT proyecto independiente
Remix react-metaframework de JavaScript/TypeScript (TypeScript) 2021 MIT proyecto independiente
Astro web-metaframework de JavaScript/TypeScript (TypeScript) 2021 MIT proyecto independiente

🔧 Next.js#

Convirtió el renderizado en servidor en la opción por omisión del ecosistema React. Su acoplamiento con una plataforma concreta es la dimensión que el módulo 11 obliga a puntuar.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts
pnpm exec next build

Arrancarla suelta, sin el verificador:

PORT=3000 pnpm exec next start -p 3000

Qué hay dentro de su directorio:

Archivo Qué es
app/articulo/hola-mundo/page.js código JavaScript
app/datos.js código JavaScript
app/layout.js código JavaScript
app/metadatos.json/route.js código JavaScript
app/page.js código JavaScript
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
next.config.mjs código JavaScript (módulo ES)
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión

🔧 Nuxt#

El equivalente de Next.js sobre Vue, con un motor de servidor propio reutilizable fuera del framework.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts
pnpm exec nuxt build

Arrancarla suelta, sin el verificador:

PORT=3000 node .output/server/index.mjs

Qué hay dentro de su directorio:

Archivo Qué es
datos.ts código TypeScript
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
nuxt.config.ts código TypeScript
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pages/articulo/hola-mundo.vue archivo del proyecto
pages/index.vue archivo del proyecto
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar

🔧 SvelteKit#

Enrutado por sistema de archivos y adaptadores de despliegue intercambiables, que es una estrategia de salida incorporada al diseño.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts
pnpm exec vite build

Arrancarla suelta, sin el verificador:

PORT=3000 node build/index.js

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
src/app.html plantilla o marcado
src/lib/Cabeza.svelte componente de Svelte
src/lib/datos.js código JavaScript
src/routes/+page.server.js código JavaScript

🔧 Remix#

Apostó por los estándares de la plataforma web —formularios, respuestas, caché— frente a abstracciones propias. Su fusión con React Router es un ejemplo de convergencia entre proyectos.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts
pnpm exec remix vite:build

Arrancarla suelta, sin el verificador:

PORT=3000 pnpm exec remix-serve ./build/server/index.js

Qué hay dentro de su directorio:

Archivo Qué es
app/datos.js código JavaScript
app/root.jsx componente en JSX
app/routes/_index.jsx componente en JSX
app/routes/articulo.hola-mundo.jsx componente en JSX
app/routes/metadatos[.]json.js código JavaScript
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias

🔧 Astro#

Arquitectura de islas: por omisión no envía JavaScript y cada componente interactivo se declara explícitamente. Permite mezclar React, Vue y Svelte en la misma página, lo que lo hace un banco de pruebas ideal para comparar.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts
pnpm exec astro build

Arrancarla suelta, sin el verificador:

PORT=3000 node ./dist/server/entry.mjs

Qué hay dentro de su directorio:

Archivo Qué es
astro.config.mjs código JavaScript (módulo ES)
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
src/components/Cabeza.astro archivo del proyecto
src/datos.js código JavaScript
src/pages/articulo/hola-mundo.astro archivo del proyecto

Si alguna cadena de herramientas no está en tu máquina, node scripts/doctor.mjs dice cuál falta y con qué comando se instala. No hace falta tenerlas todas: el verificador ejecuta lo que encuentra y declara lo que omitió.

<!-- fin generado: fichas -->

🌐 Las implementaciones — el código a la vista#

Lo que hay que publicar, idéntico en las cinco#

astro/src/datos.js:

 * Un rastreador de una red social no ejecuta JavaScript. Si el título y la
 * descripción se ponen desde el navegador, el enlace compartido sale con el
 * título de la plantilla y sin imagen. Es el fallo de descubribilidad más caro
 * y el más fácil de no ver, porque en el navegador se ve bien.

Y el grafo, con la observación que la clase remata al final:

/** El grafo de la entidad, en el formato que leen los buscadores. Es lo mismo
 *  que las etiquetas de arriba dicho otra vez y en otro idioma, y esa
 *  duplicación es la parte que ningún framework ahorra. */
export function grafoDelArticulo(origen) {

Astro · etiquetas, y nada más#

astro/src/components/Cabeza.astro:

// Astro no tiene API de metadatos: tiene componentes, y las etiquetas de la
// cabecera son etiquetas como cualquier otra. Esto es a la vez su virtud y su
// límite. La virtud: no hay nada que aprender, se escribe HTML. El límite: nada
// impide que dos componentes escriban el mismo `<title>` dos veces, porque no
// hay quien lo mire.

Y el detalle del JSON-LD, que reaparece en los cinco — hola-mundo.astro:

      `set:html` escribe el contenido sin escapar, que es lo que hace falta aquí:
      un JSON escapado dentro de un `<script>` no lo lee nadie. Es la misma
      capacidad que en React se llama `dangerouslySetInnerHTML` y en Svelte
      `{@html}`, con tres nombres y un solo peligro — la clase 077.

Next.js · un objeto que el framework convierte en etiquetas#

nextjs/app/page.js:

 * No se escriben etiquetas: se devuelve un objeto, y Next lo convierte en
 * etiquetas. La diferencia con escribirlas a mano se nota en tres sitios:
 *
 *   - **No hay duplicados posibles.** Si una disposición y una página declaran
 *     título, el de la página gana; con etiquetas sueltas, saldrían las dos.
 *   - **Se puede heredar y completar.** Una disposición pone lo común y cada
 *     página sobrescribe lo suyo.
 *   - **Es asíncrona.** Puede consultar la base de datos para saber el título.

Y su límite, en el artículo — articulo/hola-mundo/page.js:

        El grafo de schema.org no cabe en el objeto de `generateMetadata`, así
        que se escribe como una etiqueta más. Es el recordatorio de que una API
        dedicada cubre lo previsto, y lo no previsto vuelve al método manual.

SvelteKit · etiquetas, pero dentro de un elemento que el framework mira#

sveltekit/src/lib/Cabeza.svelte:

  // Se escriben etiquetas, como en Astro, pero dentro de un elemento especial que
  // el framework reconoce: SvelteKit las recoge de todos los componentes del
  // árbol y las pone en la cabecera del documento.
  // Con una consecuencia práctica que la sintaxis no deja ver: si dos
  // componentes ponen `<title>`, el que gana es el del último renderizado. No es
  // una API que resuelva conflictos como la de Next, pero tampoco es el «cada
  // uno escribe lo que quiera» de Astro.

Y el fallo que esta clase encontró de verdad, ahora contado en el archivo donde estaba — src/app.html:

      La plantilla del documento es el sitio donde más veces se ha escrito un
      título por omisión, y donde más caro sale: `%sveltekit.head%` inserta el
      que declare la ruta DESPUÉS, así que el documento acaba con dos `<title>`.
      El navegador enseña el primero y los buscadores también.
      Es el fallo de descubribilidad más silencioso que existe: la pantalla se ve
      perfecta, la ruta declara su título, y lo que se indexa es «Mi aplicación».

Nuxt · la API que sabe cómo se llaman los metadatos#

nuxt/pages/index.vue:

// No recibe etiquetas ni un objeto genérico: recibe **los nombres de los
// metadatos que existen**, con tipos. `ogTitle`, `ogType`, `twitterCard`,
// `articlePublishedTime`. Escribir mal uno es un error de compilación en lugar
// de una etiqueta que nadie lee.
//
// Es la diferencia entre una API que sabe de qué va el problema y una que solo
// mueve cadenas de un sitio a otro.

Y la misma excepción de siempre — articulo/hola-mundo.vue:

// El grafo sí es una etiqueta suelta, también aquí: ninguna de las cinco APIs
// dedicadas lo cubre, porque schema.org es un vocabulario abierto y no cabe en
// una lista de nombres.

Y una decisión declarada en la configuración — nuxt.config.ts:

 * Nuxt permite poner un título global aquí, en `app.head`. No se usa: un título
 * por omisión escrito en la configuración es la forma más habitual de acabar
 * indexando «Nuxt App» en media aplicación, porque no falla nada cuando una ruta
 * se olvida de poner el suyo.

Remix · una lista de descriptores, y el hueco a la vista#

remix/app/routes/_index.jsx:

 * Cada elemento es un objeto y Remix decide qué etiqueta le corresponde: `title`
 * se convierte en `<title>`, `name` en `<meta name>`, `property` en
 * `<meta property>`, `tagName: "link"` en `<link>`. Es un punto intermedio entre
 * el objeto cerrado de Next y las etiquetas sueltas de Astro.
 * Y recibe `data`, que es lo que devolvió el `loader`: el título puede depender
 * del dato que se cargó, sin volver a pedirlo.

Y dónde se colocan, escrito a mano — app/root.jsx:

 * Que haya que escribirlo a mano en el documento raíz es coherente con el resto
 * del framework: aquí no hay un documento mágico, hay un componente que devuelve
 * HTML y dos huecos con nombre. Se ve dónde va todo.

🔬 Comparación#

Cómo se declara ¿API dedicada? ¿Evita duplicados? ¿Recibe los datos cargados?
Astro etiquetas en un componente sí, son variables del frontmatter
SvelteKit etiquetas dentro de <svelte:head> ❌ gana la última sí, por propiedades
Next.js objeto devuelto por generateMetadata sí, y puede ser async
Nuxt useSeoMeta con nombres tipados sí, en el setup
Remix lista de descriptores en meta ✅ recibe data del loader

Cuatro lecturas:

⚠️ Errores frecuentes#

✅ Verificación#

node scripts/run-class.mjs 101

Para hacerlo tú, la comprobación que encuentra el fallo de esta clase en cualquier proyecto:

curl -s http://127.0.0.1:4100/articulo/hola-mundo | grep -c "<title>"

Si sale más de 1, tienes dos títulos y el que se indexa no es el que crees.

🧪 Reto de transferencia#

  1. Cuenta tus títulos. Con el comando de arriba, en tres rutas distintas de tu aplicación. Es la prueba más barata de esta parte entera.
  2. Comparte un enlace tuyo. Pégalo en una red social y mira la vista previa. Si sale el nombre del proyecto en lugar del de la página, los metadatos no están en el HTML.
  3. Busca tus rutas sin canónico. Cualquiera que acepte parámetros de consulta los necesita, y son casi todas las que tienen filtros o paginación.

🔗 Enlaces#

Fuentes#