---
title: Querying Content
description: Use getEmDashCollection and getEmDashEntry to retrieve content in your templates.
---

import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";

EmDash provides query functions to retrieve content in your Astro pages and components. These functions follow Astro's [live content collections](https://docs.astro.build/en/reference/experimental-flags/live-content-collections/) pattern, returning structured results with error handling.

## Query Functions

EmDash exports two primary query functions:

| Function                | Purpose                                | Returns                       |
| ----------------------- | -------------------------------------- | ----------------------------- |
| `getEmDashCollection` | Retrieve all entries of a content type | `{ entries, error }`          |
| `getEmDashEntry`      | Retrieve a single entry by ID or slug  | `{ entry, error, isPreview }` |

Import them from `emdash`:

```ts
import { getEmDashCollection, getEmDashEntry } from "emdash";
```

## Get All Entries

Use `getEmDashCollection` to retrieve all entries of a content type:

```astro title="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>
```

<Aside>
  The destructuring syntax `{ entries: posts }` renames `entries` to `posts` for cleaner code. Using `{ entries }` directly also works.
</Aside>

### Filter by Locale

When [i18n is enabled](/guides/internationalization/), filter by locale to retrieve content in a specific language:

```ts
// 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:

```ts
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](/guides/internationalization/#fallback-chain) is followed.

### Filter by Status

Retrieve only published or draft content:

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

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

<Aside type="tip">
	Always filter by `status: "published"` for public-facing pages. Draft content should only be
	accessible in the admin or preview mode.
</Aside>

### Limit Results

Restrict the number of returned entries:

```ts
// 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:

```ts
// 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:

```ts
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:

```astro title="src/pages/posts/[slug].astro"
---
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:

```ts
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:

```astro title="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>
```

<Aside>
	Preview works out of the box — EmDash auto-generates a per-site preview secret on first
	use. Set `EMDASH_PREVIEW_SECRET` in your environment if you need to share the secret across
	multiple processes (e.g. a separate preview Worker). The admin generates preview URLs that
	include an HMAC-signed `_preview` token either way.
</Aside>

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

```astro
<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.

<Aside type="tip">
	For string and text fields, inline editing uses `contenteditable`. For Portable Text fields, it
	injects a TipTap editor. For image fields, it opens a media library popover.
</Aside>

## Sorting Results

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

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

```ts
// 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:

```bash
npx emdash types
```

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

```ts
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:

```astro title="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);
---
```

### Server-Rendered

For server-rendered pages, query content directly:

```astro title="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 });
}
---
```

<Aside type="tip">
	Server rendering shows content changes immediately without rebuilding. Use it for frequently
	updated content.
</Aside>

## Performance Considerations

### Caching

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

```astro
---
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:

```astro title="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} />
```

## Next Steps

- [Create a Blog](/guides/create-a-blog/) - Build a complete blog
- [Taxonomies](/guides/taxonomies/) - Filter by categories and tags
- [Working with Content](/guides/working-with-content/) - Admin CRUD operations
- [Internationalization](/guides/internationalization/) - Multilingual content and translations
