Skip to content

Querying Content

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.

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

Use getEmDashCollection to retrieve all entries of a content type:

src/pages/posts.astro
---
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>

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.

Retrieve only published, draft, or archived content:

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

Restrict the number of returned entries:

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

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.

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 });
}

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

src/pages/posts/[slug].astro
---
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 Astro.redirect("/404");
}
---
<article>
<h1>{post.data.title}</h1>
<div set:html={post.data.content} />
</article>

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

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:

src/pages/posts/[...slug].astro
---
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>

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.

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),
);
// 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);

Generate TypeScript types for your collections:

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

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

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

src/pages/posts/[slug].astro
---
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);
---

For server-rendered pages, query content directly:

src/pages/posts/[slug].astro
---
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 });
}
---

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");
---

Query once and pass data to components:

src/pages/index.astro
---
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} />