Querying Content

On this page

EmDash provides query functions to retrieve content in your Astro pages and components. These functions follow Astro’s live content collections pattern, returning structured results with error handling.

Query Functions

EmDash exports two primary query functions:

FunctionPurposeReturns
getEmDashCollectionRetrieve all entries of a content type{ entries, error }
getEmDashEntryRetrieve a single entry by ID or slug{ entry, error, isPreview }

Import them from emdash:

import { getEmDashCollection, getEmDashEntry } from "emdash";

Get All Entries

Use getEmDashCollection to retrieve all entries of a content type:

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

const { entries: posts, error } = await getEmDashCollection("posts");

if (error) {
  console.error("Failed to load posts:", error);
}
---

<ul>
  {posts.map((post) => (
    <li>{post.data.title}</li>
  ))}
</ul>

Filter by Locale

When i18n is enabled, filter by locale to retrieve content in a specific language:

// French posts
const { entries: frenchPosts } = await getEmDashCollection("posts", {
	locale: "fr",
	status: "published",
});

// Use the current request locale
const { entries: localizedPosts } = await getEmDashCollection("posts", {
	locale: Astro.currentLocale,
	status: "published",
});

For single entries, pass locale as the third argument:

const { entry: post } = await getEmDashEntry("posts", "my-post", {
	locale: Astro.currentLocale,
});

When locale is omitted, it defaults to the request’s current locale. If no translation exists for the requested locale, the fallback chain is followed.

Filter by Status

Retrieve only published or draft content:

// Only published posts
const { entries: published } = await getEmDashCollection("posts", {
	status: "published",
});

// Only drafts
const { entries: drafts } = await getEmDashCollection("posts", {
	status: "draft",
});

Limit Results

Restrict the number of returned entries:

// Get the 5 most recent posts
const { entries: recentPosts } = await getEmDashCollection("posts", {
	status: "published",
	limit: 5,
});

Filter by Taxonomy

Filter entries by category, tag, or custom taxonomy terms:

// Posts in the "news" category
const { entries: newsPosts } = await getEmDashCollection("posts", {
	status: "published",
	where: { category: "news" },
});

// Posts with the "javascript" tag
const { entries: jsPosts } = await getEmDashCollection("posts", {
	status: "published",
	where: { tag: "javascript" },
});

// Posts matching any of multiple terms
const { entries: featuredNews } = await getEmDashCollection("posts", {
	status: "published",
	where: { category: ["news", "featured"] },
});

The where filter uses OR logic when multiple values are provided for a single taxonomy.

Error Handling

Always check for errors when reliability matters:

const { entries: posts, error } = await getEmDashCollection("posts");

if (error) {
	// Log and handle gracefully
	console.error("Failed to load posts:", error);
	return new Response("Server error", { status: 500 });
}

Get a Single Entry

Use getEmDashEntry to retrieve one entry by its ID or slug:

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

const { slug } = Astro.params;
const { entry: post, error } = await getEmDashEntry("posts", slug);

if (error) {
  return new Response("Server error", { status: 500 });
}

if (!post) {
  return Astro.redirect("/404");
}
---

<article>
  <h1>{post.data.title}</h1>
  <PortableText value={post.data.content} />
</article>

Entry Return Type

getEmDashEntry returns a result object:

interface EntryResult<T> {
	entry: ContentEntry<T> | null; // null if not found
	error?: Error; // Only set for actual errors (not "not found")
	isPreview: boolean; // true if viewing preview/draft content
}

interface ContentEntry<T> {
	id: string;
	data: T;
	edit: EditProxy; // Visual editing annotations
}

The data object within entry contains all fields defined for the content type. The edit proxy provides visual editing annotations (see below).

Preview Mode

EmDash handles preview automatically via middleware. When a URL contains a valid _preview token, the middleware verifies it and sets up the request context. Your query functions then serve draft content without any special parameters:

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

const { slug } = Astro.params;

// No special preview handling needed — middleware does it automatically
const { entry, isPreview, error } = await getEmDashEntry("posts", slug);

if (error) {
  return new Response("Server error", { status: 500 });
}

if (!entry) {
  return Astro.redirect("/404");
}
---

{isPreview && (
  <div class="preview-banner">
    Viewing preview. This content is not published.
  </div>
)}

<article>
  <h1>{entry.data.title}</h1>
  <PortableText value={entry.data.content} />
</article>

Visual Editing

Every entry returned by query functions includes an edit proxy for annotating your templates. Spread it onto elements to enable inline editing for authenticated editors:

<article {...entry.edit}>
  <h1 {...entry.edit.title}>{entry.data.title}</h1>
  <div {...entry.edit.content}>
    <PortableText value={entry.data.content} />
  </div>
</article>

In edit mode, {...entry.edit.title} produces a data-emdash-ref attribute that the visual editing toolbar uses to enable inline editing. In production, the proxy spreads produce no output — zero runtime cost.

Sorting Results

getEmDashCollection does not guarantee sort order. Sort results in your template:

const { entries: posts } = await getEmDashCollection("posts", {
	status: "published",
});

// Sort by publication date, newest first
const sorted = posts.sort(
	(a, b) => (b.data.publishedAt?.getTime() ?? 0) - (a.data.publishedAt?.getTime() ?? 0),
);

Common Sort Patterns

// Alphabetical by title
posts.sort((a, b) => a.data.title.localeCompare(b.data.title));

// By custom order field
posts.sort((a, b) => (a.data.order ?? 0) - (b.data.order ?? 0));

// Random order
posts.sort(() => Math.random() - 0.5);

TypeScript Types

Generate TypeScript types for your collections:

npx emdash types

This creates .emdash/types.ts with interfaces for each collection. Use them for type safety:

import { getEmDashCollection, getEmDashEntry } from "emdash";
import type { Post } from "../.emdash/types";

// Type-safe collection query
const { entries: posts } = await getEmDashCollection<Post>("posts");
// posts is ContentEntry<Post>[]

// Type-safe entry query
const { entry: post } = await getEmDashEntry<Post>("posts", "my-post");
// post is ContentEntry<Post> | null

Static vs. Server Rendering

EmDash content works with both static and server-rendered pages.

Static (Prerendered)

For static pages, use getStaticPaths to generate routes at build time:

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

export async function getStaticPaths() {
  const { entries: posts } = await getEmDashCollection("posts", {
    status: "published",
  });

  return posts.map((post) => ({
    params: { slug: post.data.slug },
  }));
}

const { slug } = Astro.params;
const { entry: post } = await getEmDashEntry("posts", slug);
---

Server-Rendered

For server-rendered pages, query content directly:

---
export const prerender = false;

import { getEmDashEntry } from "emdash";

const { slug } = Astro.params;
const { entry: post, error } = await getEmDashEntry("posts", slug);

if (error) {
  return new Response("Server error", { status: 500 });
}

if (!post) {
  return new Response(null, { status: 404 });
}
---

Performance Considerations

Caching

EmDash uses Astro’s live content collections, which handle caching automatically. For server-rendered pages, consider adding HTTP cache headers:

---
const { entries: posts } = await getEmDashCollection("posts", {
  status: "published",
});

// Cache for 5 minutes
Astro.response.headers.set("Cache-Control", "public, max-age=300");
---

Avoid Redundant Queries

Query once and pass data to components:

---
import { getEmDashCollection } from "emdash";
import PostList from "../components/PostList.astro";
import Sidebar from "../components/Sidebar.astro";

// Query once
const { entries: posts } = await getEmDashCollection("posts", {
  status: "published",
});

const featured = posts.filter((p) => p.data.featured);
const recent = posts.slice(0, 5);
---

<PostList posts={featured} />
<Sidebar posts={recent} />

Next Steps