JavaScript API Reference
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
Section titled “Site-template API contract”The maintained templates call these runtime helpers:
| Function | Used for |
|---|---|
decodeSlug |
Decoding a dynamic route parameter before lookup |
getEmDashCollection |
Reading and filtering entries in a collection |
getEmDashEntry |
Reading one entry by ID or slug |
getMenuWithCacheHint |
Rendering a navigation menu with cache invalidation |
getSeoMeta |
Resolving an entry’s SEO panel values and template fallbacks |
getSiteSettings |
Reading public site identity and other global settings |
getSiteSettingsWithCacheHint |
Reading global settings with cache invalidation |
getTaxonomyTermsWithCacheHint |
Rendering taxonomy filters with cache invalidation |
getTermsForEntries |
Batch-loading one taxonomy for a list of entries |
sanitizeHref |
Rejecting unsafe URL schemes before rendering stored links |
search |
Searching published content across collections |
Content queries
Section titled “Content queries”EmDash’s query functions follow Astro’s live content collections pattern, returning { entries, error } or { entry, error } for graceful error handling.
getEmDashCollection()
Section titled “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
Section titled “Parameters”| Parameter | Type | Description |
|---|---|---|
collection |
string |
Collection slug |
options |
CollectionFilter |
Optional filter options |
Options
Section titled “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
Section titled “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
Section titled “Examples”The following examples filter by status and taxonomy, limit results, and handle errors:
// Get all published postsconst { entries: posts } = await getEmDashCollection("posts", { status: "published",});
// Get latest 5 postsconst { entries: latest } = await getEmDashCollection("posts", { limit: 5, status: "published",});
// Filter by taxonomyconst { entries: newsPosts } = await getEmDashCollection("posts", { status: "published", where: { category: "news" },});
// Numbered archive page (e.g. /page/3) with offset paginationconst 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 errorsconst { entries, error } = await getEmDashCollection("posts");if (error) { return new Response("Server error", { status: 500 });}getEmDashEntry()
Section titled “getEmDashEntry()”Fetch a single entry by slug or ID. The following example loads a post and renders the site’s 404 page when it is missing:
import { getEmDashEntry } from "emdash";
const { entry: post, error } = await getEmDashEntry("posts", "my-post-slug");
if (!post) { return Astro.rewrite("/404");}Parameters
Section titled “Parameters”| Parameter | Type | Description |
|---|---|---|
collection |
string |
Collection slug |
slugOrId |
string |
Entry slug or ID |
options |
{ locale?: string; references?: ReferenceSelection } |
Optional. Locale for slug resolution, and the reference fields to load |
Preview mode is handled automatically: when the request has a valid _preview token, the query serves draft content. Preview state requires no parameter.
references names the reference fields to load, keyed by field slug. true requests the first page at the default limit of 50 entries; the object form takes a limit (at most 100) and the cursor from a previous page. Fields left out are not read, and a call that omits the option runs no reference queries.
Returns
Section titled “Returns”The function resolves to an EntryResult:
interface EntryResult<T, R> { entry: ContentEntry<T, R> | 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
Section titled “Examples”The following examples fetch by slug and ID, read preview state, and distinguish errors from not-found:
// Get by slugconst { entry: post } = await getEmDashEntry("posts", "hello-world");
// Get by IDconst { entry: post } = await getEmDashEntry("posts", "01HXK5MZSN0FVXT2Q3KPRT9M7D");
// Preview is automatic — isPreview is true when a valid _preview token is presentconst { entry, isPreview, error } = await getEmDashEntry("posts", slug);
// Handle errors vs not-foundif (error) { return new Response("Server error", { status: 500 });}if (!entry) { return Astro.rewrite("/404");}getTranslations()
Section titled “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()
Section titled “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()
Section titled “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
Section titled “Content types”ContentEntry
Section titled “ContentEntry”Query functions return entries in the following shape:
interface ContentEntry<T = Record<string, unknown>, R = ReferencePages> { id: string; data: T; references?: R; // One page per reference field requested; absent otherwise 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 identifierslug- URL-friendly identifierstatus- “draft” | “published” | “archived”createdAt- Creation time as aDateupdatedAt- Last update time as aDatepublishedAt- Publication time as aDate, ornull; retained when content is unpublished- Plus all custom fields defined in your collection schema
ReferencePage
Section titled “ReferencePage”entry.references holds one page per reference field that getEmDashEntry was asked for, keyed by field slug:
interface ReferencePage<T = Record<string, unknown>> { entries: ContentEntry<T>[]; nextCursor?: string; // Set when the field holds more entries than the limit}Each entry is mapped the way a directly loaded entry is, with one exception: bylines and taxonomy terms are not hydrated, so data.bylines and data.terms are absent.
getEmDashReferences()
Section titled “getEmDashReferences()”Fetch one page of a single reference field, without re-reading the entry it hangs off. Use it to walk past the first page with the nextCursor that page returned.
| Parameter | Type | Description |
|---|---|---|
collection |
string |
Collection slug of the entry that holds the field |
slugOrId |
string |
Entry slug or ID |
field |
string |
Reference field slug |
options |
{ limit?: number; cursor?: string; locale?: string } |
Optional. Defaults to 50 entries, at most 100 |
The following example loads the next 20 linked entries:
import { getEmDashReferences } from "emdash";
const { entries, nextCursor, error } = await getEmDashReferences( "posts", post.id, "related_posts", { cursor, limit: 20 },);error is set only for actual errors. An unknown field, a missing entry, or one the request may not see all resolve to an empty entries array.
The result also carries a cacheHint naming the rows the page read. A route that caches what it renders should merge it into its own hint, the way getEmDashEntry({ references }) folds in the first page’s, so that writing to one of those entries expires the page.
URL helpers
Section titled “URL helpers”decodeSlug() and slugify()
Section titled “decodeSlug() and slugify()”Use decodeSlug() on a dynamic route parameter before passing it to a content query. It returns undefined for a missing parameter or a malformed percent-encoded string, and otherwise returns the decoded slug:
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()
Section titled “sanitizeHref() and isSafeHref()”These helpers accept http:, https:, mailto:, tel:, site-relative paths, and fragments. They reject script-capable schemes, protocol-relative URLs, backslash-prefixed forms that browsers resolve to another site, and control characters. 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
Section titled “Preview system”generatePreviewToken()
Section titled “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()
Section titled “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()
Section titled “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()
Section titled “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()
Section titled “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
Section titled “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 ProseMirrorconst prosemirrorDoc = portableTextToProsemirror(portableText);Site settings
Section titled “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 settingsconst settings = await getSiteSettings();
// Get single settingconst 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.
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
Section titled “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 menusconst menus = await getMenus();
// Get specific menu with itemsconst 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
Section titled “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
Section titled “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 termconst news = await getTerm("category", "news");
// Get terms assigned to a content entryconst postCategories = await getEntryTerms("posts", "post-123", "category");
// Get entries with a specific termconst newsPosts = await getEntriesByTerm("posts", "category", "news");getTaxonomyDefs({ locale? }) lists one definition per taxonomy, and getTaxonomyDef(name, { locale? }) returns one definition, or null when no locale defines the taxonomy. Both resolve a taxonomy in every locale; Translate taxonomies and terms explains which locale’s label they use. 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
Section titled “Widget areas”Fetch widget areas and the widgets they contain:
import { getWidgetArea, getWidgetAreas } from "emdash";
// Get all widget areasconst areas = await getWidgetAreas();
// Get specific widget area with widgetsconst 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
Section titled “Sections”Fetch sections and filter them:
import { getSection, getSections } from "emdash";
// Get all sections (paginated)const { items, nextCursor } = await getSections();
// Filter sectionsconst { items: themeSections } = await getSections({ source: "theme" });const { items: results } = await getSections({ search: "newsletter" });
// Get a single section by slugconst 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.
Search
Section titled “Search”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; scope?: "all" | "title"; // Default: "all"}
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, });}With scope: "title" the query matches only each collection’s title field instead of the full indexed text – useful for pickers and autocomplete where body-text matches would surprise. Collections whose title field is not indexed for search return no results under this scope.
Error handling
Section titled “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.rewrite("/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.