Consultar contenido

En esta página

Las páginas de EmDash leen el contenido en el momento de la solicitud con getEmDashCollection() y getEmDashEntry(). La primera función devuelve una lista y la segunda devuelve una entrada por su slug o su ID de contenido. Ambas devuelven los errores como datos para que la página decida cómo responder.

Las plantillas incluidas usan la salida de servidor de Astro. Por tanto, un visitante recibe la última revisión publicada en el siguiente renderizado después de que un editor la publique. Un borrador guardado permanece privado hasta que se publica o se solicita mediante una URL de vista previa válida.

Consultar una colección

La siguiente página carga las siete publicaciones más recientes. Las consultas públicas de colecciones usan por defecto status: "published", así que el filtro no necesita repetirlo.

---
import { getEmDashCollection } from "emdash";

const { entries: posts, error, cacheHint } = await getEmDashCollection("posts", {
  orderBy: { published_at: "desc" },
  limit: 7,
});

if (error) {
  console.error("Failed to load posts:", error);
  return new Response("Unable to load posts", { status: 500 });
}

if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---

<h1>Posts</h1>
<ul>
  {posts.map((post) => (
    <li>
      <a href={`/posts/${post.id}`}>{post.data.title}</a>
    </li>
  ))}
</ul>

La ordenación y el límite ocurren en la base de datos antes de que EmDash hidrate las entradas. Esto evita cargar una colección entera y ordenarla o recortarla en la página.

El resultado contiene:

  • entries, que es un array vacío cuando no hay coincidencias o la consulta falló;
  • error, que se establece cuando la consulta falla, pero no cuando el resultado está vacío;
  • cacheHint, que lleva las etiquetas y la hora de última modificación para la caché de Astro;
  • nextCursor, que se establece cuando una página limitada por cursor tiene más entradas; y
  • hasMore, que indica si una página limitada por cursor u offset tiene otra página.

Identificadores de entrada

Cada resultado tiene dos identificadores con funciones distintas:

  • entry.id es el identificador de cara a la URL que produce el cargador de contenido. Normalmente es el slug. Cuando la internacionalización antepone un locale, el prefijo se incluye. Usa este valor cuando construyas un enlace a partir de un resultado de colección.
  • entry.data.id es el ID de contenido estable almacenado en la base de datos. No cambia cuando un editor cambia el slug. Úsalo cuando una API, un helper de taxonomías, el contexto de la página o una relación esperen un ID de contenido.

getEmDashEntry() acepta el slug o el ID de contenido estable. Una búsqueda por slug se limita a un locale cuando la internacionalización está habilitada; un ID de contenido identifica la fila directamente.

Filtrar una colección

Pasa los filtros en el segundo argumento. La siguiente consulta devuelve las publicaciones publicadas de la categoría news cuyo campo series es engineering:

const { entries } = await getEmDashCollection("posts", {
  where: {
    category: "news",
    series: "engineering",
  },
});

Las claves que nombran una taxonomía coinciden con los slugs de los términos asignados. Las demás claves coinciden con campos de la colección. Varias claves se combinan con lógica AND. Un array en una clave coincide con cualquiera de los valores listados, así que { category: ["news", "updates"] } coincide con cualquiera de las dos categorías.

Usa locale para solicitar un idioma de forma explícita:

const { entries: frenchPosts } = await getEmDashCollection("posts", {
  locale: "fr",
  orderBy: { published_at: "desc" },
});

Si se omite locale, EmDash usa el locale de la solicitud y después el locale predeterminado configurado. Consulta Internacionalización (i18n) para ver las reglas de reserva y las rutas traducidas.

status acepta "published", "draft" o "archived". No solicites borradores desde una ruta pública. Usa el flujo de vista previa cuando un visitante necesite acceso temporal a una entrada sin publicar.

Ordenar resultados en la base de datos

orderBy asigna nombres de campo a "asc" o "desc". Usa los nombres de campo almacenados, no los nombres en camelCase devueltos en entry.data:

const { entries } = await getEmDashCollection("posts", {
  orderBy: {
    published_at: "desc",
    title: "asc",
  },
});

Las columnas del sistema usan sus nombres de base de datos, como created_at, updated_at y published_at. Los campos personalizados usan su slug de colección, como title o priority. Los datos devueltos asignan las fechas del sistema a createdAt, updatedAt y publishedAt, pero esos nombres de propiedad en camelCase no son campos válidos de orderBy.

EmDash usa el primer campo válido de orderBy como clave de paginación y el ID de contenido como desempate estable. Sin orderBy, las colecciones se ordenan por defecto por created_at descendente. Marca los campos escalares personalizados como indexados cuando el sitio ordene o filtre por ellos con regularidad; el índice evita un escaneo completo de la tabla a medida que crece la colección.

Paginar una colección

Usa un cursor para un feed continuo o un offset para páginas numeradas. Son modelos de paginación distintos y no se pueden combinar en una misma consulta tipada.

Paginación por cursor

La paginación por cursor continúa después de la última entrada devuelta por la consulta anterior. Mantén el orden sin cambios entre solicitudes y devuelve nextCursor sin inspeccionarlo ni modificarlo.

La siguiente ruta renderiza un enlace Older posts cuando existe otra página:

---
import { getEmDashCollection } from "emdash";

const cursor = Astro.url.searchParams.get("cursor") ?? undefined;
const { entries: posts, nextCursor, error } = await getEmDashCollection("posts", {
  limit: 10,
  cursor,
  orderBy: { published_at: "desc" },
});

if (error) return new Response("Unable to load posts", { status: 500 });
---

<ul>
  {posts.map((post) => <li><a href={`/posts/${post.id}`}>{post.data.title}</a></li>)}
</ul>

{nextCursor && (
  <a href={`/posts?cursor=${encodeURIComponent(nextCursor)}`}>Older posts</a>
)}

nextCursor no aparece en la última página. La paginación por cursor no calcula un recuento total de páginas ni proporciona un cursor de página anterior; conserva las URL anteriores en el historial del navegador si la interfaz necesita navegación hacia atrás.

Paginación por offset

La paginación por offset es adecuada para rutas como /posts/page/3. Convierte el número de página en un offset y usa hasMore para el enlace a la página siguiente:

---
import { getEmDashCollection } from "emdash";

const parsedPage = Number(Astro.params.page ?? "1");
if (!Number.isInteger(parsedPage) || parsedPage < 1) {
  return Astro.rewrite("/404");
}

const perPage = 10;
const { entries: posts, hasMore, error } = await getEmDashCollection("posts", {
  limit: perPage,
  offset: (parsedPage - 1) * perPage,
  orderBy: { published_at: "desc" },
});

if (error) return new Response("Unable to load posts", { status: 500 });
---

<ul>
  {posts.map((post) => <li><a href={`/posts/${post.id}`}>{post.data.title}</a></li>)}
</ul>

<nav aria-label="Post pages">
  {parsedPage > 1 && <a href={`/posts/page/${parsedPage - 1}`}>Newer posts</a>}
  {hasMore && <a href={`/posts/page/${parsedPage + 1}`}>Older posts</a>}
</nav>

Un offset debe ser un entero no negativo. La página 1 usa un offset de cero, que significa «empezar en la primera entrada». La paginación por offset es fácil de direccionar por número de página, pero las entradas añadidas entre solicitudes pueden desplazar las páginas posteriores. Usa cursores cuando ese desplazamiento resulte confuso.

Consultar y renderizar una entrada

La siguiente ruta en tiempo de ejecución lee una publicación por slug, renderiza su imagen destacada y su cuerpo en Portable Text, y distingue un fallo de la consulta de una entrada inexistente:

---
import { decodeSlug, getEmDashEntry } from "emdash";
import { Image, PortableText } from "emdash/ui";

const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.rewrite("/404");

const { entry: post, error, isPreview, cacheHint } = await getEmDashEntry("posts", slug);

if (error) {
  console.error("Failed to load post:", error);
  return new Response("Unable to load post", { status: 500 });
}

if (!post) return Astro.rewrite("/404");
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---

{isPreview && <p>This is an unpublished preview.</p>}

<article>
  {post.data.featured_image && <Image image={post.data.featured_image} priority />}
  <h1>{post.data.title}</h1>
  <PortableText value={post.data.content} />
</article>

PortableText aporta renderizadores para los bloques estándar de EmDash, incluidos imágenes, galerías, código, tablas, bloques HTML y bloques iframe. Pasa componentes personalizados cuando un sitio añada sus propios bloques de Portable Text.

Un bloque HTML con isolated: true renderiza su html, css y js en un iframe en sandbox que ajusta su tamaño al contenido. Los demás bloques HTML renderizan su html sanitizado en la propia página. Si reemplazas el renderizador htmlBlock, pasa los bloques aislados a HtmlBlock de emdash/ui o renderízalos en un iframe cuyo sandbox no incluya allow-same-origin. Sanitiza tú mismo el HTML en línea y permite solo los hosts de iframe en los que el sitio pretenda confiar.

Un bloque iframe se renderiza mediante Iframe de emdash/ui como un iframe adaptable y de carga diferida, con un sandbox y la política de referrer strict-origin-when-cross-origin. Las páginas del propio host de tu sitio pierden el acceso de mismo origen, y solo se renderizan fuentes https.

Con la política de seguridad de contenido de Astro activada, el navegador bloquea los estilos y scripts de los bloques HTML aislados, por lo que se renderizan sin su CSS, su JavaScript ni su altura automática. Los bloques iframe también pierden su tamaño personalizado, y el frame-src de la política debe permitir los hosts que incrustan.

PortableText muestra las tablas como marcadores de posición de solo lectura en el modo de edición. Para localizar su etiqueta inicial, pasa tablePlaceholder={translatedLabel}. Por defecto es "Table (edit in admin)" y no afecta al contenido de las tablas publicadas.

Leer campos de referencia

Un campo reference enlaza una entrada con entradas de otra colección, y Relaciones explica cómo definir uno. Su valor no forma parte de data. Pasa una opción references a getEmDashEntry que nombre los campos que renderiza la página, con el slug del campo como clave, y cada uno vuelve como una página de entradas:

---
import { getEmDashEntry } from "emdash";

const { entry: post } = await getEmDashEntry("posts", Astro.params.slug, {
  references: { author: true, related_posts: { limit: 6 } },
});

if (!post) return Astro.rewrite("/404");

const author = post.references?.author.entries[0];
---

<article>
  <h1>{post.data.title}</h1>
  {author && <p>By {author.data.name}</p>}
  <ul>
    {post.references?.related_posts.entries.map((related) => (
      <li><a href={`/posts/${related.data.slug}`}>{related.data.title}</a></li>
    ))}
  </ul>
</article>

true solicita la primera página con el límite predeterminado de 50 entradas. Usa { limit, cursor } para un campo que contenga más, hasta 100 por página.

Cada entrada referenciada es un ContentEntry con la misma forma que una cargada directamente: un id, un objeto data con las fechas como objetos Date y los valores de medios resueltos, y un proxy edit limitado a la entrada referenciada, de modo que al hacer clic en una tarjeta en la edición visual se abre la entrada a la que se refiere la tarjeta. Los bylines y los términos de taxonomía son la excepción: EmDash no los hidrata en las entradas referenciadas, así que lee data.bylines y data.terms de la propia entrada.

Las entradas llegan en el orden que dispuso el editor cuando el campo está en el extremo padre de su relación. Un campo en el extremo hijo lista las entradas que apuntan a él, las cuales no tienen un orden propio.

La opción es opt-in en ambos sentidos. Una llamada que no pasa references no ejecuta consultas adicionales, y un campo omitido de la selección no se lee. Una llamada que selecciona campos cuesta una consulta de enlaces por campo, más una consulta de entradas por cada colección de destino distinta, sin importar cuántas entradas contenga cada campo.

Paginar un campo de referencia

getEmDashReferences obtiene la página siguiente de un único campo, usando el cursor que devolvió la página anterior:

import { getEmDashReferences } from "emdash";

const { entries, nextCursor } = await getEmDashReferences("posts", post.id, "related_posts", {
	cursor,
	limit: 20,
});

Lee la visibilidad de los borradores del mismo contexto de solicitud que getEmDashEntry, de modo que un recorrido iniciado en la vista previa sigue viendo la selección pendiente. Un id de página lleva su locale cuando i18n antepone uno (fr/about), y ese es el id que hay que devolver.

En una ruta que almacena su salida en caché, combina el cacheHint que devuelve la página con el de la propia ruta, para que una escritura en una de las entradas que nombró haga caducar la página.

Campos de referencia en vista previa

Un renderizado público ve las entradas publicadas y la selección publicada. Una vista previa de la entrada, o un editor en la edición visual, ve las entradas sin publicar y la selección preparada en el borrador de la entrada, de modo que un enlace de vista previa muestra las referencias que tendrá la página una vez publicada.

Aplicar funciones de SEO y edición

Para una colección con soporte de SEO, getSeoMeta() resuelve el título SEO, la descripción, la imagen, la URL canónica y la opción de no-index del editor, con valores de reserva tomados de la entrada. La plantilla de blog actual pasa ese resultado a su layout base:

---
import { getSeoMeta } from "emdash";

const seo = getSeoMeta(post, {
  siteTitle: "My Blog",
  siteUrl: Astro.url.origin,
  path: Astro.url.pathname,
});
---

<Base
  title={seo.title}
  pageTitle={seo.ogTitle}
  description={seo.description}
  image={seo.ogImage}
  canonical={seo.canonical}
  robots={seo.robots}
>
  <!-- Post content -->
</Base>

Un layout que incluya <EmDashHead> puede aplicar los mismos campos de SEO y las contribuciones de plugins a las páginas de contenido renderizadas en el servidor. Las metaetiquetas escritas a mano que solo leen data.title o data.excerpt no aplican la URL canónica ni el ajuste de no-index del editor.

Las URL de vista previa no necesitan una consulta aparte. El middleware valida el token _preview y getEmDashEntry() devuelve el borrador correspondiente con isPreview: true. La guía de vista previa y edición visual explica la generación de URL y las anotaciones entry.edit usadas para la edición en línea.

Generar tipos TypeScript

El servidor de desarrollo genera emdash-env.d.ts a partir del esquema activo. Mantén ese archivo incluido en la configuración de TypeScript del proyecto para que un nombre de colección como "posts" seleccione automáticamente el tipo de datos Post generado.

Para una instancia remota de EmDash, la CLI puede obtener el esquema y escribir los tipos en .emdash/types.ts:

npx emdash types --url https://cms.example.com

El comando acepta un token de API o encabezados de autenticación personalizados. Consulta Generar tipos para ver esas opciones.

Cada colección que tiene un campo de referencia vinculado a una relación obtiene una segunda interfaz, {Collection}References, registrada bajo el mismo slug. getEmDashEntry acota su resultado a los campos que nombró la opción references, y las entradas de cada página llevan la interfaz de la colección de destino:

const { entry: post } = await getEmDashEntry("posts", "my-post", {
	references: { author: true },
});

// post?.references?.author.entries[0] carries the Author interface
// post?.references?.related_posts is a type error: it was not selected

Renderizado en tiempo de ejecución y caché

Las plantillas de blog para Node.js y Cloudflare establecen output: "server" en astro.config.mjs. Sus consultas de contenido se ejecutan en cada renderizado del servidor, por lo que una revisión recién publicada es apta para la siguiente solicitud. Si prerenderizas una ruta de forma deliberada, su HTML contiene el contenido disponible en el momento de la compilación y solo cambia después de otra compilación.

Cuando la caché de Astro está habilitada, pasa el cacheHint de la consulta a Astro.cache.set(). EmDash asocia la respuesta con las etiquetas de colección y de entrada para que la publicación pueda invalidar las páginas afectadas en caché. Evita reemplazar esa integración por una vida útil fija y larga de Cache-Control, a menos que las actualizaciones retrasadas sean una decisión de producto explícita.

Para las firmas exactas y los filtros menos comunes, consulta la Referencia de la API JavaScript. Para construir un ejemplo funcional en torno a estas consultas, continúa con Crear un Blog.