How to Connect EmDash CMS with Astro

How to Connect EmDash CMS with Astro

Unlike connecting EmDash to Next.js or Nuxt — which means running it as a separate, decoupled backend — Astro is EmDash's actual, designed-for integration. There's no REST API round trip, no separate deployment, and no manual revalidation strategy to build. This is how the connection actually works under the hood, and how to query and render content once it's wired up.

Table of Contents
  1. The Integration Point: astro.config.mjs
  2. How Content Actually Flows
  3. Querying Content: The Two Functions You Need
  4. Get All Entries in a Collection
  5. Get a Single Entry by Slug
  6. Rendering Rich Content
  7. Using Astro Content Collections Alongside EmDash
  8. Adding React or Vue Components Inside Your EmDash Site
  9. Frequently Asked Questions
  10. Do I need a separate database server to develop locally?
  11. Does every page need to use getEmDashCollection or getEmDashEntry?
  12. What happens if a query fails?
  13. Can I get TypeScript autocomplete on the fields I define in the admin?
  14. The Bottom Line

The Integration Point: astro.config.mjs

EmDash registers as a standard Astro integration. A minimal local setup with SQLite and local file storage looks like this:

import { defineConfig } from "astro/config";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";

export default defineConfig({
  output: "server",
  integrations: [
    emdash({
      database: sqlite({ url: "file:./data/emdash.db" }),
      storage: local({
        directory: "./data/uploads",
        baseUrl: "/_emdash/api/media/file",
      }),
    }),
  ],
});
emdashkits.com

That's the entire connection — a database adapter and a storage adapter passed into the `emdash()` integration. Everything else (the admin panel at `/_emdash/admin`, the content model, the query API) becomes available to the rest of your Astro project automatically.

How Content Actually Flows

EmDash serves content through Astro's Live Collections at runtime, so changes an editor makes are visible immediately. Your pages read the same content through query functions as the admin panel writes.

Concretely: content lives in your configured database. Editors write to it through the admin UI. Your Astro pages read from the same database, in the same process, through two exported functions — no cache to invalidate, no webhook to configure, no build step between an edit and it appearing on the live site.

Read also:

Querying Content: The Two Functions You Need

Import both from the top-level `emdash` package:

import { getEmDashCollection, getEmDashEntry } from "emdash";
emdashkits.com

Get All Entries in a Collection

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

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

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

<ul>
  {posts.map((post) => (
    <li><a href={`/posts/${post.slug}`}>{post.data.title}</a></li>
  ))}
</ul>
emdashkits.com

Get a Single Entry by Slug

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

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

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

<h1>{post.data.title}</h1>
emdashkits.com

Rendering Rich Content

Post content stored in a `portableText` field is a structured JSON array, not raw HTML. Render it with EmDash's `<PortableText>` component, which handles images, code blocks, embeds, and formatting out of the box:

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

<PortableText value={post.data.content} />
emdashkits.com

To render a custom block type (a plugin-defined block, or an `htmlBlock` you want styled differently), pass a `components` map — this is exactly the pattern used elsewhere on this site to customize how external links render inside post bodies.

Using Astro Content Collections Alongside EmDash

You don't have to choose one or the other. Astro's file-based `astro:content` collections and EmDash's database-backed collections coexist cleanly — a common split is developer-managed content (docs, changelogs) in Astro collections, and editor-managed content (blog posts, marketing pages) in EmDash:

---
import { getCollection } from "astro:content";
import { getEmDashCollection } from "emdash";

// Developer-managed docs from files
const docs = await getCollection("docs");

// Editor-managed posts from the database
const { entries: posts } = await getEmDashCollection("posts", {
  status: "published",
  limit: 5,
});
---
emdashkits.com

Adding React or Vue Components Inside Your EmDash Site

If you want interactive components beyond what Astro's own templating covers, Astro's official framework integrations work exactly the same way inside an EmDash-powered site as any other Astro project — add `@astrojs/react` or `@astrojs/vue` alongside the `emdash()` integration, and use those components as islands in your `.astro` pages while EmDash content queries work unchanged around them.

Frequently Asked Questions

Do I need a separate database server to develop locally?

No — the local development setup uses a SQLite file by default, with no external database process to run. Production deployments can move to libSQL, Cloudflare D1, or PostgreSQL depending on your hosting target.

Does every page need to use getEmDashCollection or getEmDashEntry?

Only pages rendering EmDash-managed content. Static marketing pages, layouts, and anything else in your Astro project work exactly as they would without EmDash at all.

What happens if a query fails?

Both query functions return an `error` field alongside their result rather than throwing — check it explicitly (as shown above) so a database hiccup doesn't produce an unhandled exception mid-render.

Can I get TypeScript autocomplete on the fields I define in the admin?

Yes — EmDash generates TypeScript types from your current content model, giving autocomplete on `post.data.whateverField` from query to template, regenerated whenever your schema changes.

The Bottom Line

Connecting EmDash to Astro isn't really a separate step — it's the platform's native mode. Two query functions, one `<PortableText>` component for rich content, and your admin edits show up on the live site with no rebuild in between. If you need React or Vue components inside that same site, Astro's own framework integrations handle that without any special EmDash configuration. See our 10-minute setup guide if you haven't scaffolded a project yet, or our guide to customizing content models to define what you're querying.

Share

Comments

Write a comment

Related Articles

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

July 16, 2026

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

July 16, 2026

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

July 16, 2026

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

TL;DR — A custom session-cookie login flow appeared to succeed on localhost (the OTP verified, the response looked fine) but every subsequent request to a login-gated page treated the visitor as logged out. Identical code worked fine on the live HTTPS site. The cookie's Secure attribute was hardcoded to true — and per the cookie spec, browsers never store or send a Secure cookie over a plain, non-HTTPS connection.

Table of Contents
  1. Diagnostic
  2. Root cause
  3. Fix
  4. Lessons learned

Diagnostic

Check the actual Set-Cookie response header and the browser's own cookie storage panel — on localhost over http://, the cookie is sent by the server but never actually stored by the browser.

Root cause

// before -- assumes the app is always served over HTTPS
setCookie("session", token, { secure: true, httpOnly: true });
emdashkits.com

A cookie config that quietly assumes "we're always on HTTPS" breaks the instant you test over plain HTTP, which local dev servers commonly are.

Read also:

Fix

// after -- derive secure from the actual request protocol
const isHttps = request.url.startsWith("https://");
setCookie("session", token, { secure: isHttps, httpOnly: true });
emdashkits.com

Lessons learned

  • Any Secure-flagged cookie needs to key off the real request scheme, not an assumption baked in once at cookie-creation time.
  • "Works in production, silently fails in local dev" is a strong signal to check cookie flags before anything else in an auth flow.
  • Check other cookies in the same codebase for the same hardcoded assumption — if one cookie has this bug, sibling cookies set the same way are worth auditing too.
Share
Previous Article

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Comments

Write a comment

Related Articles

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

July 16, 2026

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

July 16, 2026

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)

July 16, 2026

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

TL;DR — GA4's gtag.js snippet was installed correctly, the page loaded with no visible JavaScript error, and GA4's real-time report still showed zero activity. The CMS's default Content-Security-Policy had no allowance for analytics domains and no config option to add one — so every request to Google's tracking endpoints was blocked at the browser level before it could fail loudly.

Table of Contents
  1. Diagnostic
  2. Root cause
  3. Lessons learned

Diagnostic

Check the browser's dedicated CSP violation reporting, not the regular console error list — CSP blocks are reported through their own channel, not thrown as normal script errors, so "no console errors" doesn't mean nothing was blocked.

Root cause

The CSP's script-src and connect-src directives had no entry for googletagmanager.com or google-analytics.com, and the CMS exposed no configuration surface to add one — the only way in was patching the CSP directives directly.

// patch-package: add analytics domains to the existing CSP directives
scriptSrc.push("https://www.googletagmanager.com");
connectSrc.push("https://www.google-analytics.com", "https://www.googletagmanager.com");
emdashkits.com
Read also:

Lessons learned

  • "No console errors" is not proof nothing was blocked — CSP violations live in their own reporting surface and are easy to miss if you're only scanning for red error text.
  • Before adding any third-party script tag to a site with a CSP already in place, check the CSP's directives first rather than assuming a silently-empty analytics dashboard means a snippet-installation mistake.
Share
Previous Article

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Next Article

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Comments

Write a comment

Related Articles

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

July 16, 2026

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

July 16, 2026

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)

July 16, 2026

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)