Inhalte abfragen

Auf dieser Seite

EmDash-Seiten lesen Inhalte zur Anfragezeit mit getEmDashCollection() und getEmDashEntry(). Die erste Funktion liefert eine Liste, die zweite einen einzelnen Eintrag über seinen Slug oder seine Content-ID. Beide liefern Fehler als Daten zurück, sodass die Seite selbst entscheiden kann, wie sie reagiert.

Die mitgelieferten Templates verwenden Astros Server-Ausgabe. Ein Besucher erhält daher beim nächsten Rendern, nachdem ein Editor veröffentlicht hat, die neueste veröffentlichte Revision. Ein gespeicherter Entwurf bleibt privat, bis er veröffentlicht oder über eine gültige Vorschau-URL angefordert wird.

Eine Sammlung abfragen

Die folgende Seite lädt die sieben zuletzt veröffentlichten Beiträge. Öffentliche Sammlungsabfragen verwenden standardmäßig status: "published", sodass der Filter das nicht wiederholen muss.

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

Sortieren und Begrenzen finden in der Datenbank statt, bevor EmDash die Einträge aufbereitet. So wird vermieden, eine ganze Sammlung zu laden und sie anschließend in der Seite zu sortieren oder zu kürzen.

Das Ergebnis enthält:

  • entries, ein leeres Array, wenn es keine Treffer gibt oder die Abfrage fehlgeschlagen ist;
  • error, das bei einer fehlgeschlagenen Abfrage gesetzt wird, nicht aber bei einem leeren Ergebnis;
  • cacheHint, der die Tags und den Zeitpunkt der letzten Änderung für Astros Cache enthält;
  • nextCursor, der gesetzt wird, wenn eine begrenzte Cursor-Seite weitere Einträge hat; und
  • hasMore, das angibt, ob auf eine begrenzte Cursor- oder Offset-Seite eine weitere Seite folgt.

Eintrags-Identifikatoren

Jedes Ergebnis hat zwei Identifikatoren mit unterschiedlichen Aufgaben:

  • entry.id ist der URL-seitige Identifikator, den der Content-Loader erzeugt. Normalerweise ist es der Slug. Wenn die Internationalisierung ein Locale voranstellt, ist dieses Präfix enthalten. Verwenden Sie diesen Wert, wenn Sie einen Link aus einem Sammlungsergebnis bauen.
  • entry.data.id ist die stabile, in der Datenbank gespeicherte Content-ID. Sie ändert sich nicht, wenn ein Editor den Slug ändert. Verwenden Sie sie, wenn eine API, ein Taxonomie-Helper, ein Seitenkontext oder eine Relation eine Content-ID erwartet.

getEmDashEntry() akzeptiert entweder den Slug oder die stabile Content-ID. Eine Slug-Suche ist auf ein Locale beschränkt, wenn die Internationalisierung aktiviert ist; eine Content-ID identifiziert die Zeile direkt.

Eine Sammlung filtern

Übergeben Sie Filter im zweiten Argument. Die folgende Abfrage liefert veröffentlichte Beiträge in der Kategorie news, deren Feld series den Wert engineering hat:

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

Schlüssel, die eine Taxonomie benennen, passen auf zugewiesene Term-Slugs. Andere Schlüssel passen auf Sammlungsfelder. Mehrere Schlüssel werden mit AND-Logik kombiniert. Ein Array bei einem Schlüssel passt auf jeden der aufgeführten Werte, sodass { category: ["news", "updates"] } auf beide Kategorien passt.

Verwenden Sie locale, um eine Sprache explizit anzufordern:

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

Wenn locale weggelassen wird, verwendet EmDash das Locale der Anfrage und danach das konfigurierte Standard-Locale. Siehe Internationalisierung (i18n) für die Fallback-Regeln und übersetzte Routen.

status akzeptiert "published", "draft" oder "archived". Fordern Sie keine Entwürfe von einer öffentlichen Route an. Verwenden Sie den Vorschau-Ablauf, wenn ein Besucher vorübergehend Zugriff auf einen unveröffentlichten Eintrag benötigt.

Ergebnisse in der Datenbank sortieren

orderBy ordnet Feldnamen "asc" oder "desc" zu. Verwenden Sie die gespeicherten Feldnamen, nicht die in entry.data zurückgegebenen camelCase-Namen:

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

Systemspalten verwenden ihre Datenbanknamen, etwa created_at, updated_at und published_at. Benutzerdefinierte Felder verwenden ihren Sammlungs-Slug, etwa title oder priority. Die zurückgegebenen Daten bilden Systemdaten auf createdAt, updatedAt und publishedAt ab, aber diese camelCase-Eigenschaftsnamen sind keine gültigen orderBy-Felder.

EmDash verwendet das erste gültige orderBy-Feld als Paginierungsschlüssel und die Content-ID als stabilen Tie-Breaker. Ohne orderBy sind Sammlungen standardmäßig nach created_at absteigend sortiert. Markieren Sie benutzerdefinierte skalare Felder als indiziert, wenn die Website regelmäßig danach sortiert oder filtert; der Index vermeidet einen vollständigen Tabellen-Scan, wenn die Sammlung wächst.

Eine Sammlung paginieren

Verwenden Sie einen Cursor für einen fortlaufenden Feed oder einen Offset für nummerierte Seiten. Das sind getrennte Paginierungs- Modelle, die sich in einer typisierten Abfrage nicht kombinieren lassen.

Cursor-Paginierung

Die Cursor-Paginierung setzt nach dem letzten Eintrag fort, den die vorherige Abfrage geliefert hat. Behalten Sie die Sortierung zwischen Anfragen unverändert bei und geben Sie nextCursor zurück, ohne ihn zu untersuchen oder zu verändern.

Die folgende Route rendert einen Link Older posts, wenn eine weitere Seite existiert:

---
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 fehlt auf der letzten Seite. Die Cursor-Paginierung berechnet keine Gesamtseitenzahl und liefert keinen Cursor für die vorherige Seite; bewahren Sie frühere URLs im Browserverlauf auf, wenn die Oberfläche eine Zurück-Navigation braucht.

Offset-Paginierung

Die Offset-Paginierung eignet sich für Routen wie /posts/page/3. Rechnen Sie die Seitenzahl in einen Offset um und verwenden Sie hasMore für den Link zur nächsten Seite:

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

Ein Offset muss eine nicht negative ganze Zahl sein. Seite 1 verwendet einen Offset von null, was „beim ersten Eintrag beginnen“ bedeutet. Die Offset-Paginierung lässt sich einfach über die Seitenzahl ansprechen, aber Einträge, die zwischen zwei Anfragen hinzukommen, können spätere Seiten verschieben. Verwenden Sie Cursor, wenn diese Verschiebung verwirrend wäre.

Einen Eintrag abfragen und rendern

Die folgende Laufzeit-Route liest einen Beitrag über den Slug, rendert sein Beitragsbild und den Portable-Text-Inhalt und unterscheidet einen Abfragefehler von einem fehlenden Eintrag:

---
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 liefert Renderer für die Standardblöcke von EmDash, darunter Bilder, Videos, Galerien, Code, Tabellen, HTML-Blöcke und iframe-Blöcke. Übergeben Sie eigene Komponenten, wenn eine Website eigene Portable-Text-Blöcke hinzufügt.

Ein HTML-Block mit isolated: true rendert sein html, css und js in einem iframe mit Sandbox, der seine Größe automatisch an den Inhalt anpasst. Andere HTML-Blöcke rendern ihr html bereinigt direkt in der Seite. Wenn Sie den htmlBlock-Renderer ersetzen, übergeben Sie isolierte Blöcke an HtmlBlock aus emdash/ui oder rendern Sie sie in einem iframe, dessen sandbox allow-same-origin nicht enthält. Bereinigen Sie Inline-HTML selbst und erlauben Sie nur die iframe-Hosts, denen die Website vertrauen möchte.

Ein iframe-Block wird über Iframe aus emdash/ui als responsiver, verzögert ladender iframe gerendert, mit einer sandbox und der Referrer-Policy strict-origin-when-cross-origin. Seiten auf dem eigenen Host Ihrer Website verlieren den Same-Origin-Zugriff, und es werden nur https-Quellen gerendert.

Ein Video-Block wird über Video aus emdash/ui gerendert. Ein Video aus der Medienbibliothek wird im eigenen Player des Browsers abgespielt, mit Bedienelementen und seiner Beschriftung, und zwar von der öffentlichen URL Ihres Speichers, sofern eine konfiguriert ist. Ohne öffentliche URL werden Videos über /_emdash/api/media/file/ ausgeliefert. Ein Video eines Medienanbieters wird über diesen Anbieter gerendert. Ein leerer Video-Block ohne asset rendert nichts.

Wenn die Content Security Policy von Astro aktiviert ist, blockiert der Browser die Styles und Skripte in isolierten HTML-Blöcken. Diese werden dann ohne ihr CSS, ihr JavaScript und ihre automatische Höhe gerendert. iframe-Blöcke verlieren außerdem ihre benutzerdefinierte Größe, und das frame-src der Policy muss die Hosts erlauben, die sie einbetten. Video-Blöcke, deren Medien auf einem anderen Host liegen, etwa unter der öffentlichen URL eines Speichers oder bei einem Medienanbieter, benötigen diesen Host in media-src.

PortableText zeigt Tabellen im Bearbeitungsmodus als schreibgeschützte Platzhalter an. Um deren anfängliche Beschriftung zu lokalisieren, übergeben Sie tablePlaceholder={translatedLabel}. Der Standardwert ist "Table (edit in admin)"; er hat keinen Einfluss auf veröffentlichte Tabelleninhalte.

Referenzfelder lesen

Ein reference-Feld verknüpft einen Eintrag mit Einträgen einer anderen Sammlung; wie man eines definiert, steht unter Relationen. Sein Wert ist nicht Teil von data. Übergeben Sie getEmDashEntry eine references-Option, die die Felder benennt, die die Seite rendert, mit dem Feld-Slug als Schlüssel, und jedes davon kommt als Seite von Einträgen zurück:

---
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 fordert die erste Seite mit dem Standardlimit von 50 Einträgen an. Verwenden Sie { limit, cursor } für ein Feld, das mehr enthält, bis zu 100 pro Seite.

Jeder referenzierte Eintrag ist ein ContentEntry mit derselben Form wie ein direkt geladener: eine id, ein data-Objekt, in dem Datumswerte als Date-Objekte vorliegen und Medienwerte aufgelöst sind, und ein auf den referenzierten Eintrag beschränkter edit-Proxy, sodass ein Klick auf eine Karte in der visuellen Bearbeitung den Eintrag öffnet, um den es auf der Karte geht. Bylines und Taxonomie-Terms sind die Ausnahme — EmDash bereitet sie bei referenzierten Einträgen nicht auf, lesen Sie data.bylines und data.terms daher vom Eintrag selbst.

Die Einträge kommen in der vom Editor festgelegten Reihenfolge an, wenn das Feld auf der Eltern-Seite seiner Relation liegt. Ein Feld auf der Kind-Seite listet die Einträge auf, die auf es zeigen und die keine eigene Reihenfolge haben.

Die Option ist in beide Richtungen Opt-in. Ein Aufruf ohne references führt keine zusätzlichen Abfragen aus, und ein Feld, das in der Auswahl fehlt, wird nicht gelesen. Ein Aufruf, der Felder auswählt, kostet eine Link-Abfrage pro Feld sowie eine Eintrags-Abfrage pro unterschiedlicher Zielsammlung, egal wie viele Einträge jedes Feld enthält.

Ein Referenzfeld paginieren

getEmDashReferences ruft die nächste Seite eines einzelnen Feldes ab, wobei es den Cursor verwendet, den die vorherige Seite geliefert hat:

import { getEmDashReferences } from "emdash";

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

Es liest die Sichtbarkeit von Entwürfen aus demselben Anfragekontext wie getEmDashEntry, sodass ein in der Vorschau begonnener Durchlauf die ausstehende Auswahl weiterhin sieht. Eine Seiten-ID trägt ihr Locale, wo i18n eines voranstellt (fr/about), und genau diese ID ist zurückzugeben.

Führen Sie auf einer Route, die ihre Ausgabe cacht, den cacheHint, den die Seite liefert, mit dem der Route selbst zusammen, damit ein Schreibzugriff auf einen der darin genannten Einträge die Seite ablaufen lässt.

Referenzfelder in der Vorschau

Ein öffentliches Rendern sieht veröffentlichte Einträge und die veröffentlichte Auswahl. Eine Vorschau des Eintrags oder ein Editor in der visuellen Bearbeitung sieht unveröffentlichte Einträge und die im Entwurf des Eintrags vorgemerkte Auswahl, sodass ein Vorschau-Link die Referenzen zeigt, die die Seite nach der Veröffentlichung haben wird.

SEO- und Editing-Funktionen anwenden

Für eine Sammlung mit SEO-Unterstützung löst getSeoMeta() den SEO-Titel, die Beschreibung, das Bild, die kanonische URL und die No-Index-Auswahl des Editors auf, mit Fallbacks aus dem Eintrag. Das aktuelle Blog-Template übergibt dieses Ergebnis an sein Basis-Layout:

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

Ein Layout, das <EmDashHead> enthält, kann dieselben SEO-Felder und Plugin-Beiträge auf serverseitig gerenderte Inhaltsseiten anwenden. Handgeschriebene Meta-Tags, die nur data.title oder data.excerpt lesen, übernehmen weder die kanonische URL noch die No-Index-Einstellung des Editors.

Vorschau-URLs brauchen keine eigene Abfrage. Die Middleware validiert das _preview-Token, und getEmDashEntry() liefert den passenden Entwurf mit isPreview: true. Der Leitfaden zu Vorschau und visueller Bearbeitung erklärt die URL-Erzeugung und die entry.edit- Annotationen für die Inline-Bearbeitung.

TypeScript-Typen generieren

Der Dev-Server generiert emdash-env.d.ts aus dem aktiven Schema. Lassen Sie diese Datei in der TypeScript-Konfiguration des Projekts eingebunden, damit ein Sammlungsname wie "posts" automatisch den generierten Datentyp Post auswählt.

Für eine entfernte EmDash-Instanz kann die CLI das Schema abrufen und Typen nach .emdash/types.ts schreiben:

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

Der Befehl akzeptiert ein API-Token oder eigene Authentifizierungs-Header. Siehe Typen generieren für diese Optionen.

Jede Sammlung mit einem an eine Relation gebundenen Referenzfeld erhält ein zweites Interface, {Collection}References, das unter demselben Slug registriert wird. getEmDashEntry grenzt sein Ergebnis auf die Felder ein, die die references-Option benannt hat, und die Einträge jeder Seite tragen das Interface der Zielsammlung:

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

Runtime-Rendering und Caching

Die Blog-Templates für Node.js und Cloudflare setzen output: "server" in astro.config.mjs. Ihre Inhalts- Abfragen laufen bei jedem Server-Render, sodass eine neu veröffentlichte Revision für die nächste Anfrage infrage kommt. Wenn Sie eine Route bewusst vorab rendern, enthält ihr HTML die zur Build-Zeit verfügbaren Inhalte und ändert sich erst nach einem weiteren Build.

Wenn Astros Cache aktiviert ist, übergeben Sie den cacheHint der Abfrage an Astro.cache.set(). EmDash verknüpft die Antwort mit den Sammlungs- und Eintrags-Tags, sodass eine Veröffentlichung betroffene gecachte Seiten ungültig machen kann. Ersetzen Sie diese Integration nicht durch eine lange feste Cache-Control-Lebensdauer, es sei denn, verzögerte Aktualisierungen sind eine ausdrückliche Produktentscheidung.

Genaue Signaturen und weniger gebräuchliche Filter finden Sie in der JavaScript-API-Referenz. Um ein funktionierendes Beispiel rund um diese Abfragen zu bauen, fahren Sie mit Blog erstellen fort.