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, et la seconde renvoie une entrée à partir de son slug ou de son ID de contenu. Toutes deux renvoient les erreurs sous forme de données, afin que la page décide comment réagir.

Les modèles fournis utilisent la sortie serveur d’Astro. Un visiteur reçoit donc la dernière révision publiée au rendu suivant, une fois qu’un éditeur l’a publiée. Un brouillon enregistré reste privé tant qu’il n’est pas publié ou demandé via une URL d’aperçu valide.

Interroger une collection

La page suivante charge les sept articles publiés le plus récemment. Les requêtes publiques sur une collection 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 limite s’appliquent dans la base de données avant qu’EmDash n’assemble les entrées. Cela évite de charger toute une collection puis de la trier ou de la découper dans la page.

Le résultat contient :

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

Identifiants d’entrée

Chaque résultat comporte deux identifiants aux rôles différents :

  • entry.id est l’identifiant destiné aux URL, produit par le chargeur de contenu. C’est normalement le slug. Lorsque l’internationalisation ajoute un préfixe de locale, ce 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 modifie le slug. Utilisez-le lorsqu’une API, un assistant de taxonomie, le 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 directement la ligne.

Filtrer une collection

Passez les filtres dans le deuxième argument. La requête suivante renvoie les articles publiés de 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 des termes attribués. Les autres clés correspondent aux champs de la collection. Plusieurs clés sont combinées avec une logique AND. Un tableau sur une clé correspond à n’importe laquelle des valeurs listées, de sorte que { category: ["news", "updates"] } correspond à l’une ou l’autre catégorie.

Utilisez locale pour demander explicitement une langue :

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 (i18n) 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 associe des noms de champ à "asc" ou "desc". Utilisez les noms de champ stockés, et non 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 leur nom de base de données, comme created_at, updated_at et published_at. Les champs personnalisés utilisent leur slug de collection, comme title ou priority. Les données renvoyées associent les dates système à createdAt, updatedAt et publishedAt, mais ces noms de propriété 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 critère de départage stable. Sans orderBy, les collections sont triées par défaut par 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 balayage complet de la 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, qui ne peuvent pas être combinés dans une même requête typée.

Pagination par curseur

La pagination par curseur reprend après la dernière entrée renvoyée par la requête précédente. Gardez le tri inchangé d’une requête à l’autre 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 le nombre total de pages et ne fournit pas de curseur de page précédente ; conservez les URL précédentes 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.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 doit être un entier positif ou nul. 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 deux requêtes peuvent décaler les pages suivantes. Utilisez des curseurs lorsque ce décalage prêterait à confusion.

Interroger et afficher une entrée

La route d’exécution suivante lit un article par son slug, affiche son image mise en avant et son corps en 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.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 fournit des moteurs de rendu pour les blocs standard d’EmDash, notamment les images, les vidéos, les galeries, le code, les tableaux, les blocs HTML et les blocs iframe. Passez des composants personnalisés lorsqu’un site ajoute ses propres blocs Portable Text.

Un bloc HTML avec isolated: true affiche son html, son css et son js dans une iframe en bac à sable (sandbox) qui ajuste sa taille à son contenu. Les autres blocs HTML affichent leur html, nettoyé, directement dans la page. Si vous remplacez le moteur de rendu htmlBlock, transmettez les blocs isolés à HtmlBlock depuis emdash/ui ou affichez-les dans une iframe dont l’attribut sandbox omet allow-same-origin. Nettoyez vous-même le HTML en ligne et n’autorisez que les hôtes d’iframe auxquels le site souhaite faire confiance.

Un bloc iframe est affiché via Iframe depuis emdash/ui sous la forme d’une iframe adaptative à chargement différé, avec un attribut sandbox et la politique de référent strict-origin-when-cross-origin. Les pages situées sur l’hôte de votre propre site perdent l’accès same-origin, et seules les sources https sont affichées.

Un bloc vidéo est rendu via Video de emdash/ui. Une vidéo de la médiathèque est lue dans le lecteur natif du navigateur, avec ses commandes et sa légende, depuis l’URL publique de votre stockage lorsqu’elle est configurée. Sans URL publique, les vidéos sont servies via /_emdash/api/media/file/. Une vidéo d’un fournisseur de médias est rendue par ce fournisseur. Un bloc vidéo vide, sans asset, n’affiche rien.

Lorsque la politique de sécurité du contenu d’Astro est activée, le navigateur bloque les styles et les scripts des blocs HTML isolés, qui s’affichent alors sans leur CSS, leur JavaScript ni leur hauteur automatique. Les blocs iframe perdent également leur taille personnalisée, et la directive frame-src de la politique doit autoriser les hôtes qu’ils intègrent. Les blocs vidéo dont le média se trouve sur un autre hôte, comme l’URL publique d’un stockage ou un fournisseur de médias, nécessitent cet hôte dans media-src.

PortableText affiche les tableaux sous forme d’espaces réservés 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 des tableaux publiés.

Lire les champs de référence

Un champ reference relie une entrée à des entrées d’une autre collection, et Relations explique comment en définir un. Sa valeur ne fait pas partie de data. Passez à getEmDashEntry une option references nommant les champs que la page affiche, avec le slug du champ comme clé, et chacun revient sous forme de 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.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 demande la première page avec la limite par défaut de 50 entrées. Utilisez { limit, cursor } pour un champ qui en contient davantage, jusqu’à 100 par page.

Chaque entrée référencée est un ContentEntry de même forme qu’une entrée chargée directement : un id, un objet data dont les dates sont des objets Date et dont les valeurs média sont résolues, et un proxy edit limité à l’entrée référencée, de sorte qu’un clic sur une carte dans l’édition visuelle ouvre l’entrée dont parle la carte. Les bylines et les termes de taxonomie font exception : EmDash ne les assemble 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 défini par l’éditeur lorsque le champ se trouve du côté parent de sa relation. Un champ du côté enfant liste les entrées qui pointent vers lui, et celles-ci n’ont pas d’ordre propre.

L’option est opt-in dans les deux sens. Un appel qui ne passe pas references n’exécute aucune requête supplémentaire, et un champ absent de la sélection n’est pas lu. Un appel qui sélectionne des champs coûte une requête de liens par champ, plus une requête d’entrées par collection cible distincte, quel que soit le nombre d’entrées que contient 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 commencé en aperçu continue de voir la sélection en attente. Un id de page porte sa locale lorsque i18n en ajoute une (fr/about), et c’est cet id qu’il faut renvoyer.

Sur une route qui met sa sortie en cache, fusionnez le cacheHint renvoyé par la page avec celui de la route, afin qu’une écriture sur l’une des entrées qu’il a nommées fasse expirer 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 affiche 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 du SEO, getSeoMeta() résout le titre SEO, la description, l’image, l’URL canonique et le choix no-index de l’éditeur, avec des valeurs de repli issues de l’entrée. Le modèle de blog actuel transmet ce résultat à son layout 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>

Un layout qui inclut <EmDashHead> peut appliquer les mêmes champs SEO et les contributions des 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 ni 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 distincte. Le middleware valide le jeton _preview, et getEmDashEntry() renvoie le brouillon correspondant avec isPreview: true. Le guide de l’aperçu et de l’é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 afin 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 les types pour ces options.

Chaque collection qui possède 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 modèles 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 requête suivante. 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 nouveau 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 des mises à jour différé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.