Consultar contenido

En esta página

Las páginas de EmDash leen contenido en el momento de la solicitud con getEmDashCollection() y getEmDashEntry(). La primera función devuelve una lista, y la segunda una entrada por su slug o ID de contenido. Ambas devuelven 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 render después de que un editor la publique. Un borrador guardado permanece privado hasta que se publique o se solicite mediante una URL de vista previa válida.

Consultar una colección

La siguiente página carga las siete publicaciones más recientemente publicadas. Las consultas públicas de colección usan por defecto status: "published", por lo 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>

El ordenamiento y el límite ocurren en la base de datos antes de que EmDash hidrate las entradas. Esto evita cargar una colección completa 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 para una consulta fallida pero no para un resultado 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 de cursor limitada tiene más entradas; y
  • hasMore, que indica si una página de cursor u offset limitada tiene otra página.

Identificadores de entrada

Cada resultado tiene dos identificadores con trabajos distintos:

  • entry.id es el identificador orientado a la URL producido por el cargador de contenido. Normalmente es el slug. Cuando la internacionalización antepone una configuración regional, se incluye el prefijo. Use este valor al crear un enlace desde 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. Úselo cuando una API, un helper de taxonomía, un contexto de página o una relación espera un ID de contenido.

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

Filtrar una colección

Pase filtros en el segundo argumento. La siguiente consulta devuelve publicaciones publicadas en 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 slugs de términos asignados. Otras claves coinciden con campos de colección. Varias claves se combinan con lógica AND. Un array en una clave coincide con cualquier valor listado, de modo que { category: ["news", "updates"] } coincide con cualquiera de las dos categorías.

Use locale para solicitar un idioma explícitamente:

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

Si se omite locale, EmDash usa la configuración regional de la solicitud y luego la predeterminada configurada. Consulte Internacionalización para las reglas de respaldo y las rutas traducidas.

status acepta "published", "draft" o "archived". No solicite borradores desde una ruta pública. Use el flujo de vista previa cuando un visitante necesite acceso temporal a una entrada no publicada.

Ordenar resultados en la base de datos

orderBy asigna nombres de campo a "asc" o "desc". Use 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 orderBy válidos.

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

Paginar una colección

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

Paginación por cursor

La paginación por cursor continúa después de la última entrada devuelta por la consulta anterior. Mantenga el orden sin cambios entre solicitudes y pase nextCursor de vuelta 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 está ausente en la página final. La paginación por cursor no calcula un recuento total de páginas ni proporciona un cursor de página anterior; conserve 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 conviene a rutas como /posts/page/3. Convierta el número de página en un offset y use 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.redirect("/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, lo que significa «empezar en la primera entrada». La paginación por offset es fácil de abordar por número de página, pero las entradas añadidas entre solicitudes pueden desplazar páginas posteriores. Use cursores cuando ese movimiento 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 el cuerpo Portable Text, y distingue un fallo de consulta de una entrada faltante:

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

const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.redirect("/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.redirect("/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 suministra renderizadores para los bloques estándar de EmDash, incluidas imágenes, galerías, código, tablas y bloques HTML sanitizados. Pase componentes personalizados cuando un sitio añade sus propios bloques Portable Text. Si reemplaza el renderizador htmlBlock, sanee el HTML y permita solo los hosts de iframe en los que el sitio confía.

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

Leer campos de referencia

Un campo reference vincula una entrada con entradas de otra colección, y Relaciones cubre cómo definir una. Su valor no forma parte de data. Pase una opción references a getEmDashEntry con los campos que la página renderiza, claveados por slug de campo, 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.redirect("/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. Use { limit, cursor } para un campo que contiene 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 fechas como objetos Date y 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 de la que trata la tarjeta. Las firmas y los términos de taxonomía son la excepción: EmDash no los hidrata en las entradas referenciadas, así que lea data.bylines y data.terms de la propia entrada.

Las entradas llegan en el orden que el editor dispuso 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, que no tienen orden propio.

La opción es optativa en ambas direcciones. Una llamada sin references no ejecuta consultas extra, y un campo omitido de la selección no se lee. Una llamada que selecciona campos cuesta una consulta de enlace por campo, más una consulta de entradas por colección de destino distinta, independientemente de cuántas entradas contenga cada campo.

Paginar un campo de referencia

getEmDashReferences obtiene la página siguiente de un solo 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 borradores del mismo contexto de solicitud que getEmDashEntry, de modo que un recorrido iniciado en vista previa sigue viendo la selección pendiente. Un id de página lleva su configuración regional donde i18n antepone una (fr/about), y ese es el id que hay que devolver.

En una ruta que almacena en caché su salida, fusione el cacheHint que devuelve la página con el de la ruta, para que una escritura en una de las entradas nombradas expire la página.

Campos de referencia en vista previa

Un render público ve entradas publicadas y la selección publicada. Una vista previa de la entrada, o un editor en edición visual, ve entradas no publicadas 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 SEO, getSeoMeta() resuelve el título SEO, la descripción, la imagen, la URL canónica y la opción no-index del editor con respaldos desde la entrada. La plantilla de blog actual pasa ese resultado a su diseño 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 diseño que incluye <EmDashHead> puede aplicar los mismos campos SEO y contribuciones de plugins a 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 la opción no-index del editor.

Las URL de vista previa no necesitan una consulta separada. 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. Mantenga ese archivo incluido en la configuración 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 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. Consulte Generar tipos para 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 restringe su resultado a los campos nombrados por 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 de Node.js y Cloudflare establecen output: "server" en astro.config.mjs. Sus consultas de contenido se ejecutan en cada render del servidor, de modo que una revisión recién publicada es elegible para la siguiente solicitud. Si prerenderiza deliberadamente una ruta, su HTML contiene el contenido disponible en el momento de la compilación y solo cambia tras otra compilación.

Cuando la caché de Astro está habilitada, pase el cacheHint de la consulta a Astro.cache.set(). EmDash asocia la respuesta con las etiquetas de colección y entrada para que publicar pueda invalidar las páginas en caché afectadas. Evite sustituir esa integración por un tiempo de vida fijo y largo de Cache-Control a menos que las actualizaciones retrasadas sean una decisión de producto explícita.

Para firmas exactas y filtros menos comunes, consulte la referencia de la API de JavaScript. Para construir un ejemplo funcional en torno a estas consultas, continúe con Crear un blog.