JavaScript API Reference

On this page

This page covers the public API used by Astro pages, layouts, and components to read and present an EmDash site. It does not inventory every export from the emdash package root: database repositories, API handlers, migration utilities, plugin-authoring APIs, and other server internals have separate references or are intended for framework integration code.

The site-template API contract in this reference lists every function imported from emdash by EmDash’s maintained site templates. Type-only imports such as MediaValue belong to the relevant data-model documentation. Related functions are documented in the same sections when they help a site author, but the page does not cover unrelated root exports.

Site-template API contract

The maintained templates call these runtime helpers:

FunctionUsed for
decodeSlugDecoding a dynamic route parameter before lookup
getEmDashCollectionReading and filtering entries in a collection
getEmDashEntryReading one entry by ID or slug
getMenuRendering a navigation menu and its resolved links
getSeoMetaResolving an entry’s SEO panel values and template fallbacks
getSiteSettingsReading public site identity and other global settings
getTaxonomyTermsRendering taxonomy navigation, filters, and term indexes
getTermReading one taxonomy term for an archive page
getTermsForEntriesBatch-loading one taxonomy for a list of entries
searchSearching published content across collections

Content queries

EmDash’s query functions follow Astro’s live content collections pattern, returning { entries, error } or { entry, error } for graceful error handling.

getEmDashCollection()

Fetch all entries from a collection. The following example loads all posts and checks for an error:

import { getEmDashCollection } from "emdash";

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

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

Parameters

ParameterTypeDescription
collectionstringCollection slug
optionsCollectionFilterOptional filter options

Options

The options parameter accepts the following filter:

interface WhereRange {
	gt?: string;
	gte?: string;
	lt?: string;
	lte?: string;
}

interface CollectionFilter {
	status?: "draft" | "published" | "archived";
	limit?: number;
	cursor?: string; // Keyset pagination — pass a previous `nextCursor`
	offset?: number; // Offset pagination — skip N entries (use with `limit`)
	where?: Record<string, string | string[] | WhereRange>;
	orderBy?: Record<string, "asc" | "desc">;
	locale?: string;
}

cursor and offset are mutually exclusive. The where keys can name content fields, taxonomies, or byline; range objects are available for ordered comparisons.

Returns

The function resolves to a CollectionResult:

interface CollectionResult<T> {
	entries: ContentEntry<T>[]; // Empty array if error or none found
	error?: Error; // Set if query failed
	cacheHint: CacheHint; // Tags and last-modified time for Astro route caching
	nextCursor?: string; // Cursor for the next keyset page, if any
	hasMore?: boolean; // Whether more entries exist beyond this page (when `limit` is set)
}

Examples

The following examples filter by status and taxonomy, limit results, and handle errors:

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

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

// Filter by taxonomy
const { entries: newsPosts } = await getEmDashCollection("posts", {
	status: "published",
	where: { category: "news" },
});

// Numbered archive page (e.g. /page/3) with offset pagination
const perPage = 20;
const page = Number(Astro.params.page ?? 1);
const { entries: pagePosts, hasMore } = await getEmDashCollection("posts", {
	status: "published",
	limit: perPage,
	offset: (page - 1) * perPage,
	orderBy: { published_at: "desc" },
});

// Handle errors
const { entries, error } = await getEmDashCollection("posts");
if (error) {
	return new Response("Server error", { status: 500 });
}

getEmDashEntry()

Fetch a single entry by slug or ID. The following example loads a post and redirects when it is missing:

import { getEmDashEntry } from "emdash";

const { entry: post, error } = await getEmDashEntry("posts", "my-post-slug");

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

Parameters

ParameterTypeDescription
collectionstringCollection slug
slugOrIdstringEntry slug or ID
options{ locale?: string }Optional. Locale for slug resolution

Preview mode is handled automatically: when the request has a valid _preview token, the query serves draft content. The optional options parameter only accepts a locale for slug resolution; preview state requires no parameter.

Returns

The function resolves to an EntryResult:

interface EntryResult<T> {
	entry: ContentEntry<T> | null; // null if not found
	error?: Error; // Set only for actual errors, not "not found"
	isPreview: boolean; // true if draft content is being served
	fallbackLocale?: string; // Set when locale fallback returned another locale
	cacheHint: CacheHint; // Tags and last-modified time for Astro route caching
}

Examples

The following examples fetch by slug and ID, read preview state, and distinguish errors from not-found:

// Get by slug
const { entry: post } = await getEmDashEntry("posts", "hello-world");

// Get by ID
const { entry: post } = await getEmDashEntry("posts", "01HXK5MZSN0FVXT2Q3KPRT9M7D");

// Preview is automatic — isPreview is true when a valid _preview token is present
const { entry, isPreview, error } = await getEmDashEntry("posts", slug);

// Handle errors vs not-found
if (error) {
	return new Response("Server error", { status: 500 });
}
if (!entry) {
	return Astro.redirect("/404");
}

getTranslations()

Fetch the available translations of one entry by collection slug and database ID:

import { getTranslations } from "emdash";

const { translations, error } = await getTranslations("posts", post.data.id);

The result contains the shared translationGroup, a translations array, and an optional error. Each translation summary includes id, locale, slug, and status.

resolveEmDashPath()

Resolve a public pathname against the URL patterns configured for routable collections:

import { resolveEmDashPath } from "emdash";

const result = await resolveEmDashPath("/blog/hello-world");

if (result) {
	console.log(result.collection, result.entry.data.title);
}

The result contains the matched collection, entry, and route params. The function returns null when no configured URL pattern matches.

getEditMeta()

Read the non-enumerable visual-editing metadata attached to a Portable Text value:

import { getEditMeta } from "emdash";

const meta = getEditMeta(post.data.content);

It returns { collection, id, field } for an annotated value, or undefined when the value has no annotation.

Content types

ContentEntry

Query functions return entries in the following shape:

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

The edit proxy provides visual editing annotations. Spread it onto elements to enable inline editing: {...entry.edit.title}. Outside edit mode, it produces no output.

The data object contains all content fields plus system fields:

  • id - Unique identifier
  • slug - URL-friendly identifier
  • status - “draft” | “published” | “archived”
  • createdAt - Creation time as a Date
  • updatedAt - Last update time as a Date
  • publishedAt - Publication time as a Date, or null; retained when content is unpublished
  • Plus all custom fields defined in your collection schema

URL helpers

decodeSlug() and slugify()

Use decodeSlug() on a dynamic route parameter before passing it to a content query. It returns undefined for a missing parameter and otherwise applies decodeURIComponent(), which throws for malformed percent encoding:

function decodeSlug(raw: string | undefined): string | undefined;
import { decodeSlug, getEmDashEntry } from "emdash";

const slug = decodeSlug(Astro.params.slug);
const { entry } = slug ? await getEmDashEntry("posts", slug) : { entry: null };

slugify(value) converts text to a lowercase, hyphen-separated slug. Use it when a template needs to construct a slug from a label; stored entry slugs already come from EmDash.

sanitizeHref() and isSafeHref()

These helpers reject unsafe link schemes such as javascript:. isSafeHref(value) returns a boolean. sanitizeHref(value) returns the original safe URL or "#" when the value is empty or unsafe.

import { sanitizeHref } from "emdash";

const href = sanitizeHref(menuItem.url);

Preview system

generatePreviewToken()

Generate a preview token for draft content. The following example creates a token that expires in one hour:

import { generatePreviewToken } from "emdash";

const token = await generatePreviewToken({
	contentId: "posts:01HXK5MZSN...",
	secret: process.env.EMDASH_PREVIEW_SECRET!,
	expiresIn: 3600, // 1 hour
});

contentId must use the collection:id format. expiresIn accepts seconds or a duration ending in s, m, h, d, or w and defaults to "1h". Keep the signing secret on the server.

verifyPreviewToken()

Verify a preview token and read its payload:

import { verifyPreviewToken } from "emdash";

const result = await verifyPreviewToken({
	token,
	secret: process.env.EMDASH_PREVIEW_SECRET!,
});

if (result.valid) {
	const { cid, exp, iat } = result.payload;
	// cid is "collection:id" format, e.g. "posts:my-draft-post"
}

Pass either token or url with the signing secret. An invalid token returns { valid: false, error }, where error is "none", "malformed", "invalid", or "expired".

parseContentId()

Split a preview payload’s collection:id value into its two parts:

import { parseContentId } from "emdash";

const parsed = parseContentId(result.payload.cid);

It returns { collection, id } and throws when the value has no colon separator.

getPreviewUrl() and buildPreviewUrl()

getPreviewUrl() creates and signs a preview URL. It accepts collection, id, and secret, plus optional expiresIn, baseUrl, pathPattern, and locale values:

import { getPreviewUrl } from "emdash";

const previewUrl = await getPreviewUrl({
	collection: "posts",
	id: post.id,
	secret: process.env.EMDASH_PREVIEW_SECRET!,
	pathPattern: "/blog/{id}",
});

Without baseUrl, it returns a site-relative URL. Use buildPreviewUrl({ path, token, baseUrl? }) when a token already exists.

isPreviewRequest()

Check whether a request includes a preview token, then read it:

import { isPreviewRequest, getPreviewToken } from "emdash";

if (isPreviewRequest(Astro.url)) {
	const token = getPreviewToken(Astro.url);
	// Verify and show preview content
}

getPreviewToken() returns the _preview query parameter or null when it is absent. The EmDash middleware verifies normal preview requests and supplies preview state to getEmDashEntry() automatically; these helpers are for custom preview routes and tooling.

Content converters

Convert between Portable Text and ProseMirror formats:

import { prosemirrorToPortableText, portableTextToProsemirror } from "emdash";

// From ProseMirror (editor) to Portable Text (storage)
const portableText = prosemirrorToPortableText(prosemirrorDoc);

// From Portable Text to ProseMirror
const prosemirrorDoc = portableTextToProsemirror(portableText);

Site settings

Read site-wide settings with getSiteSettings and getSiteSetting:

function getSiteSettings(): Promise<Partial<SiteSettings>>;
function getSiteSetting<K extends SiteSettingKey>(key: K): Promise<SiteSettings[K] | undefined>;
import { getSiteSettings, getSiteSetting } from "emdash";

// Get all settings
const settings = await getSiteSettings();

// Get single setting
const title = await getSiteSetting("title");

Settings are read-only from the runtime API. Use the admin API to update them.

getSiteSettings() returns a partial object because unset keys are omitted. Media settings such as the site logo and favicon are resolved to media-reference objects before the function returns.

getSiteSettingsWithCacheHint() returns { data, cacheHint }. Pass the hint to Astro.cache.set() when an Astro route cache should be invalidated after site settings change.

SEO

Resolve the SEO panel values and content fallbacks for a page with getSeoMeta():

function getSeoMeta<T>(content: SeoContentInput<T>, options?: SeoMetaOptions): SeoMeta;

interface SeoMetaOptions {
	siteTitle?: string;
	siteUrl?: string;
	titleSeparator?: string; // Default: " | "
	path?: string;
	defaultOgImage?: string;
	defaultTitle?: string;
	defaultDescription?: string;
}

interface SeoMeta {
	title: string;
	description: string | null;
	ogTitle: string;
	ogDescription: string | null;
	ogImage: string | null;
	canonical: string | null;
	robots: string | null;
}
import { getSeoMeta } from "emdash";

const meta = getSeoMeta(post, {
	siteTitle: "Example Blog",
	siteUrl: "https://example.com",
	path: `/blog/${post.data.slug}`,
});

It returns the resolved title, description, Open Graph values, canonical URL, and robots value. getContentSeo(content) returns the raw SEO object without applying template fallbacks.

For translated content, getHreflangAlternates(collection, entryId, { siteUrl? }) returns published, routable locale variants as { hreflang, href } objects and adds an x-default entry. It returns an empty array when internationalization is disabled, the current entry is marked noindex, or no absolute site URL is available.

Comments

Fetch approved comments and their count for an entry:

import { getCommentCount, getComments } from "emdash";

const { items: comments, total } = await getComments({
	collection: "posts",
	contentId: post.data.id,
	threaded: true,
	reactions: true,
	sort: "best",
});

const count = await getCommentCount("posts", post.data.id);

threaded nests replies below their parent. The default sort is "oldest"; "best" ranks top-level comments by reactions and enables reaction counts automatically. Server-rendered comment queries return at most 500 approved comments; use the REST API when a client needs pagination.

Fetch navigation menus and iterate their items, including nested children:

function getMenu(name: string, options?: { locale?: string }): Promise<Menu | null>;
function getMenus(options?: { locale?: string }): Promise<MenuSummary[]>;
import { getMenu, getMenus } from "emdash";

// Get all menus
const menus = await getMenus();

// Get specific menu with items
const primaryMenu = await getMenu("primary");

if (primaryMenu) {
	primaryMenu.items.forEach(item => {
		console.log(item.label, item.url);
		// Nested items for dropdowns
		item.children.forEach(child => console.log("  -", child.label));
	});
}

getMenu(name, { locale? }) follows the configured locale fallback chain. getMenus({ locale? }) lists menu summaries for the resolved request or configured locale; without internationalization it lists every locale. getMenuWithCacheHint() returns { data, cacheHint } for routes using Astro’s cache.

Bylines

Fetch an author profile by ID or slug, or list entries credited to a byline:

import { getByline, getBylineBySlug, getEntriesByByline } from "emdash";

const profile = await getBylineBySlug("jane-doe", { locale: "en" });
const posts = profile
	? await getEntriesByByline("posts", profile.translationGroup ?? profile.id)
	: [];

getByline(id) returns one profile or null. The slug lookup accepts an optional locale and follows the locale fallback chain. Content queries already hydrate an entry’s ordered credits into entry.data.bylines; use these standalone helpers for author pages and byline archives.

Taxonomies

Fetch taxonomy terms, a single term, an entry’s terms, or entries by term:

function getTaxonomyTerms(
	taxonomyName: string,
	options?: { locale?: string; includeCounts?: boolean },
): Promise<TaxonomyTerm[]>;

function getTerm(
	taxonomyName: string,
	slug: string,
	options?: { locale?: string; includeCounts?: boolean },
): Promise<TaxonomyTerm | null>;

function getTermsForEntries(
	collection: string,
	entryIds: string[],
	taxonomyName: string,
	options?: { locale?: string },
): Promise<Map<string, TaxonomyTerm[]>>;
import { getTaxonomyTerms, getTerm, getEntryTerms, getEntriesByTerm } from "emdash";

// Get all terms for a taxonomy (tree structure for hierarchical)
const categories = await getTaxonomyTerms("category");

// Get single term
const news = await getTerm("category", "news");

// Get terms assigned to a content entry
const postCategories = await getEntryTerms("posts", "post-123", "category");

// Get entries with a specific term
const newsPosts = await getEntriesByTerm("posts", "category", "news");

getTaxonomyDefs({ locale? }) lists taxonomy definitions, while getTaxonomyDef(name, { locale? }) returns one definition or null. Locale-aware definition and term lookups follow the configured fallback chain.

getTaxonomyTerms(name, { locale?, includeCounts? }) returns a tree for hierarchical taxonomies and includes visible-entry counts by default. Pass includeCounts: false when the template does not render counts. Content queries also hydrate assigned terms into each entry’s data.terms; use getEntryTerms() when you only have a collection name and entry ID.

For archive pages that render terms beside many entries, batch the lookup instead of calling getEntryTerms() in a loop:

import { getAllTermsForEntries, getTermsForEntries } from "emdash";

const termsByPost = await getTermsForEntries(
	"posts",
	posts.map(post => post.data.id),
	"category",
);

const allTermsByPost = await getAllTermsForEntries(
	"posts",
	posts.map(post => post.data.id),
);

getTermsForEntries() returns a map from entry ID to the requested taxonomy’s terms. getAllTermsForEntries() returns a map from entry ID to terms grouped by taxonomy name. getTaxonomyTermsWithCacheHint() returns { data, cacheHint } for an Astro-cached route.

Widget areas

Fetch widget areas and the widgets they contain:

import { getWidgetArea, getWidgetAreas } from "emdash";

// Get all widget areas
const areas = await getWidgetAreas();

// Get specific widget area with widgets
const sidebar = await getWidgetArea("sidebar");

if (sidebar) {
	sidebar.widgets.forEach(widget => {
		console.log(widget.type, widget.title);
	});
}

getWidgetAreaWithCacheHint(name) returns { data, cacheHint } for routes using Astro’s cache. Widget areas and their widgets are ordered by their configured sort order.

Sections

Fetch sections and filter them:

import { getSection, getSections } from "emdash";

// Get all sections (paginated)
const { items, nextCursor } = await getSections();

// Filter sections
const { items: themeSections } = await getSections({ source: "theme" });
const { items: results } = await getSections({ search: "newsletter" });

// Get a single section by slug
const cta = await getSection("newsletter-cta");

getSections(options?) returns { items: Section[]; nextCursor?: string }. Options are source ("theme" | "user" | "import"), search, limit (default 50, max 100), and cursor.

Run a global search across collections. Results include highlighted snippets:

function search(query: string, options?: SearchOptions): Promise<SearchResponse>;

interface SearchOptions {
	collections?: string[]; // Default: every searchable collection
	status?: string; // Default: "published"
	locale?: string; // Default: all locales
	limit?: number; // Default: 20
	cursor?: string;
}

interface SearchResponse {
	items: SearchResult[];
	nextCursor?: string;
}
import { search } from "emdash";

const results = await search("hello world", {
	collections: ["posts", "pages"],
	status: "published",
	limit: 20,
});

// search() resolves to { items, nextCursor? }
results.items.forEach(result => {
	console.log(result.title);
	console.log(result.snippet); // Contains <mark> tags
	console.log(result.score);
});

// Paginate: pass the previous nextCursor back as `cursor` to get the next page.
// nextCursor is undefined once there are no more results.
if (results.nextCursor) {
	const next = await search("hello world", {
		collections: ["posts", "pages"],
		limit: 20,
		cursor: results.nextCursor,
	});
}

Error handling

Content queries return operational errors in their result instead of throwing. A missing entry is not an operational error: entry is null and error remains undefined.

const { entry, error } = await getEmDashEntry("posts", slug);

if (error) {
	return new Response("Content could not be loaded", { status: 500 });
}

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

Helpers without a result envelope can throw when their input is invalid or a database operation fails. Handle those errors at the route boundary when the page can provide a useful fallback. The repository and handler error classes used by lower-level server integrations are outside this site-template API.