Interroger le contenu

Sur cette page

Les pages EmDash lisent le contenu au moment de la requête avec getEmDashCollection() et getEmDashEntry(). La première fonction renvoie une liste, la seconde une entrée par son slug ou son ID de contenu. Les deux renvoient les erreurs sous forme de données pour que la page décide comment répondre.

Les templates fournis utilisent la sortie serveur d’Astro. Un visiteur reçoit donc la dernière révision publiée au prochain rendu après qu’un éditeur l’a publiée. Un brouillon enregistré reste privé jusqu’à sa publication ou une demande via une URL d’aperçu valide.

Interroger une collection

La page suivante charge les sept articles les plus récemment publiés. Les requêtes de collection publiques utilisent par défaut status: "published", le filtre n’a donc pas besoin de le répéter.

---
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>

Le tri et la limitation se font dans la base de données avant qu’EmDash n’hydrate les entrées. Cela évite de charger une collection entière et de la trier ou la découper dans la page.

Le résultat contient :

  • entries, un tableau vide lorsqu’il n’y a aucune correspondance ou que la requête a échoué ;
  • error, défini pour une requête échouée mais pas pour un résultat vide ;
  • cacheHint, qui porte les balises et l’heure de dernière modification pour le cache d’Astro ;
  • nextCursor, défini lorsqu’une page de curseur limitée a d’autres entrées ; et
  • hasMore, qui indique si une page de curseur ou d’offset limitée a une autre page.

Identifiants d’entrée

Chaque résultat a deux identifiants aux rôles distincts :

  • entry.id est l’identifiant orienté URL produit par le chargeur de contenu. C’est normalement le slug. Lorsque l’internationalisation préfixe une locale, le préfixe est inclus. Utilisez cette valeur pour construire un lien à partir d’un résultat de collection.
  • entry.data.id est l’ID de contenu stable stocké dans la base de données. Il ne change pas lorsqu’un éditeur change le slug. Utilisez-le lorsqu’une API, un helper de taxonomie, un contexte de page ou une relation attend un ID de contenu.

getEmDashEntry() accepte le slug ou l’ID de contenu stable. Une recherche par slug est limitée à une locale lorsque l’internationalisation est activée ; un ID de contenu identifie la ligne directement.

Filtrer une collection

Passez les filtres dans le second argument. La requête suivante renvoie les articles publiés dans la catégorie news dont le champ series vaut engineering :

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

Les clés qui nomment une taxonomie correspondent aux slugs de termes assignés. Les autres clés correspondent aux champs de collection. Plusieurs clés sont combinées avec une logique AND. Un tableau sur une clé correspond à n’importe quelle valeur listée, donc { category: ["news", "updates"] } correspond à l’une ou l’autre catégorie.

Utilisez locale pour demander une langue explicitement :

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

Si locale est omis, EmDash utilise la locale de la requête puis la locale par défaut configurée. Voir Internationalisation pour les règles de repli et les routes traduites.

status accepte "published", "draft" ou "archived". Ne demandez pas de brouillons depuis une route publique. Utilisez le flux d’aperçu lorsqu’un visiteur a besoin d’un accès temporaire à une entrée non publiée.

Trier les résultats dans la base de données

orderBy mappe les noms de champs à "asc" ou "desc". Utilisez les noms de champs stockés, pas les noms en camelCase renvoyés dans entry.data :

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

Les colonnes système utilisent leurs noms de base de données, tels que created_at, updated_at et published_at. Les champs personnalisés utilisent leur slug de collection, tel que title ou priority. Les données renvoyées mappent les dates système vers createdAt, updatedAt et publishedAt, mais ces noms de propriétés en camelCase ne sont pas des champs orderBy valides.

EmDash utilise le premier champ orderBy valide comme clé de pagination et l’ID de contenu comme départage stable. Sans orderBy, les collections sont triées par défaut selon created_at décroissant. Marquez les champs scalaires personnalisés comme indexés lorsque le site trie ou filtre régulièrement dessus ; l’index évite un scan complet de table à mesure que la collection grandit.

Paginer une collection

Utilisez un curseur pour un flux continu ou un offset pour des pages numérotées. Ce sont des modèles de pagination distincts et ne peuvent pas être combinés dans une requête typée.

Pagination par curseur

La pagination par curseur continue après la dernière entrée renvoyée par la requête précédente. Gardez le tri inchangé entre les requêtes et renvoyez nextCursor sans l’inspecter ni le modifier.

La route suivante affiche un lien Older posts lorsqu’une autre page existe :

---
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 absent sur la dernière page. La pagination par curseur ne calcule pas un nombre total de pages et ne fournit pas de curseur de page précédente ; conservez les URL antérieures dans l’historique du navigateur si l’interface a besoin d’une navigation arrière.

Pagination par offset

La pagination par offset convient aux routes telles que /posts/page/3. Convertissez le numéro de page en offset et utilisez hasMore pour le lien vers la page suivante :

---
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 doit être un entier non négatif. La page 1 utilise un offset de zéro, ce qui signifie « commencer à la première entrée ». La pagination par offset est facile à adresser par numéro de page, mais les entrées ajoutées entre les requêtes peuvent décaler les pages suivantes. Utilisez des curseurs lorsque ce déplacement serait déroutant.

Interroger et afficher une entrée

La route runtime suivante lit un article par slug, affiche son image à la une et le corps Portable Text, et distingue un échec de requête d’une entrée manquante :

---
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 fournit des renderers pour les blocs standard d’EmDash, y compris images, galeries, code, tableaux et blocs HTML assainis. Passez des composants personnalisés lorsque un site ajoute ses propres blocs Portable Text. Si vous remplacez le renderer htmlBlock, assainissez le HTML et n’autorisez que les hôtes iframe auxquels le site fait confiance.

PortableText affiche les tableaux comme des placeholders en lecture seule en mode édition. Pour localiser leur libellé initial, passez tablePlaceholder={translatedLabel}. La valeur par défaut est "Table (edit in admin)" et n’affecte pas le contenu de tableau publié.

Lire les champs de référence

Un champ reference lie une entrée à des entrées d’une autre collection, et Relations couvre la définition. Sa valeur ne fait pas partie de data. Passez une option references à getEmDashEntry nommant les champs que la page affiche, indexés par slug de champ, et chacun revient comme une page d’entrées :

---
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 demande la première page à la limite par défaut de 50 entrées. Utilisez { limit, cursor } pour un champ qui en contient plus, jusqu’à 100 par page.

Chaque entrée référencée est un ContentEntry de la même forme qu’une entrée chargée directement : un id, un objet data avec des dates en objets Date et des valeurs média résolues, et un proxy edit limité à l’entrée référencée, de sorte qu’un clic sur une carte en édition visuelle ouvre l’entrée dont parle la carte. Les bylines et termes de taxonomie sont l’exception — EmDash ne les hydrate pas sur les entrées référencées, lisez donc data.bylines et data.terms depuis l’entrée elle-même.

Les entrées arrivent dans l’ordre fixé par l’éditeur lorsque le champ est du côté parent de sa relation. Un champ du côté enfant liste les entrées qui pointent vers lui, qui n’ont pas d’ordre propre.

L’option est opt-in dans les deux sens. Un appel sans references n’exécute aucune requête supplémentaire, et un champ omis de la sélection n’est pas lu. Un appel qui sélectionne des champs coûte une requête de lien par champ, plus une requête d’entrées par collection cible distincte, quel que soit le nombre d’entrées de chaque champ.

Paginer un champ de référence

getEmDashReferences récupère la page suivante d’un seul champ, en utilisant le curseur renvoyé par la page précédente :

import { getEmDashReferences } from "emdash";

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

Il lit la visibilité des brouillons depuis le même contexte de requête que getEmDashEntry, de sorte qu’un parcours démarré en aperçu continue de voir la sélection en attente. Un id de page porte sa locale là où i18n en préfixe une (fr/about), et c’est l’id à renvoyer.

Sur une route qui met en cache sa sortie, fusionnez le cacheHint renvoyé par la page avec celui de la route, afin qu’une écriture sur l’une des entrées nommées expire la page.

Champs de référence en aperçu

Un rendu public voit les entrées publiées et la sélection publiée. Un aperçu de l’entrée, ou un éditeur en édition visuelle, voit les entrées non publiées et la sélection préparée dans le brouillon de l’entrée, de sorte qu’un lien d’aperçu montre les références que la page aura une fois publiée.

Appliquer les fonctionnalités SEO et d’édition

Pour une collection avec prise en charge SEO, getSeoMeta() résout le titre SEO, la description, l’image, l’URL canonique et le choix no-index de l’éditeur avec des repli depuis l’entrée. Le template de blog actuel passe ce résultat à sa mise en page de 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>

Une mise en page qui inclut <EmDashHead> peut appliquer les mêmes champs SEO et contributions de plugins aux pages de contenu rendues côté serveur. Les balises meta écrites à la main qui ne lisent que data.title ou data.excerpt n’appliquent pas l’URL canonique ni le réglage no-index de l’éditeur.

Les URL d’aperçu n’ont pas besoin d’une requête séparée. Le middleware valide le jeton _preview, et getEmDashEntry() renvoie le brouillon correspondant avec isPreview: true. Le guide d’aperçu et d’édition visuelle explique la génération d’URL et les annotations entry.edit utilisées pour l’édition en ligne.

Générer des types TypeScript

Le serveur de développement génère emdash-env.d.ts à partir du schéma actif. Gardez ce fichier inclus dans la configuration TypeScript du projet pour qu’un nom de collection tel que "posts" sélectionne automatiquement le type de données Post généré.

Pour une instance EmDash distante, la CLI peut récupérer le schéma et écrire les types dans .emdash/types.ts :

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

La commande accepte un jeton d’API ou des en-têtes d’authentification personnalisés. Voir Générer des types pour ces options.

Chaque collection qui a un champ de référence lié à une relation obtient une seconde interface, {Collection}References, enregistrée sous le même slug. getEmDashEntry restreint son résultat aux champs nommés par l’option references, et les entrées de chaque page portent l’interface de la collection cible :

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

Rendu runtime et mise en cache

Les templates de blog Node.js et Cloudflare définissent output: "server" dans astro.config.mjs. Leurs requêtes de contenu s’exécutent à chaque rendu serveur, de sorte qu’une révision nouvellement publiée est éligible pour la prochaine requête. Si vous pré-rendez délibérément une route, son HTML contient le contenu disponible au moment du build et ne change qu’après un autre build.

Lorsque le cache d’Astro est activé, passez le cacheHint de la requête à Astro.cache.set(). EmDash associe la réponse aux balises de collection et d’entrée afin que la publication puisse invalider les pages en cache concernées. Évitez de remplacer cette intégration par une durée de vie Cache-Control fixe et longue, sauf si les mises à jour retardées sont une décision produit explicite.

Pour les signatures exactes et les filtres moins courants, voir la référence de l’API JavaScript. Pour construire un exemple fonctionnel autour de ces requêtes, continuez avec Créer un blog.