How to Connect EmDash CMS with Next.js

How to Connect EmDash CMS with Next.js

Worth stating plainly before anything else: EmDash's documentation is direct about this. "Not a headless CMS — EmDash is tightly integrated with Astro and runs in the same deployment, rather than as a separate service you call over an API." If you already have a Next.js codebase and want EmDash as a decoupled backend behind it, that's not the use case EmDash is built around. This is the honest version of how to do it anyway, and what it costs you.

Table of Contents
  1. What EmDash Actually Offers a Non-Astro Frontend
  2. What You Give Up Going This Route
  3. The Alternative Worth Considering First
  4. If You Still Want Next.js Specifically
  5. Frequently Asked Questions
  6. Is there an official EmDash Next.js SDK or adapter?
  7. Will content changes show up in Next.js immediately, like they do in Astro?
  8. Should I migrate my Next.js app to Astro instead?
  9. Does EmDash support GraphQL for non-Astro frontends?
  10. The Bottom Line

What EmDash Actually Offers a Non-Astro Frontend

EmDash exposes a REST API at `/_emdash/api/` for content, media, and schema operations, authenticated via Bearer token, with CORS support for browser requests (configurable allowed origins in your deployment). That means a separate Next.js application genuinely can fetch content from an EmDash instance — it's just not the integrated, single-deployment experience EmDash is designed around.

GET /_emdash/api/content/posts
Authorization: Bearer YOUR_API_TOKEN
emdashkits.com

A minimal fetch from a Next.js Server Component:

async function getPosts() {
  const res = await fetch("https://your-emdash-site.com/_emdash/api/content/posts?status=published", {
    headers: { Authorization: `Bearer ${process.env.EMDASH_API_TOKEN}` },
  });
  const json = await res.json();
  return json.data.items;
}

export default async function BlogPage() {
  const posts = await getPosts();
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.data.title}</li>
      ))}
    </ul>
  );
}
emdashkits.com

What You Give Up Going This Route

EmDash's own comparison table is direct about the trade-off between its integrated model and a traditional decoupled headless setup:

Headless CMS: Frontend — bring your own. Deployment — CMS + frontend, separately. Content updates — webhook/rebuild. EmDash: Frontend — Astro components. Deployment — single deployment. Content updates — immediate (SSR).

Running EmDash behind a separate Next.js app puts you squarely in that "headless CMS" row, not the integrated one. Concretely, that means:

  • No automatic live updates — Next.js won't know content changed until you rebuild, use ISR with a revalidation interval, or wire up an on-demand revalidation webhook yourself.
  • No generated TypeScript types flowing from your content model into your templates — you're working with EmDash's generic REST response shape instead.
  • Two deployments to manage and keep in sync (the EmDash/Astro instance and the Next.js app), instead of one.
  • You still need to run EmDash's own Astro-based admin somewhere — this doesn't remove that piece, it just adds a second application in front of it.
Read also:

The Alternative Worth Considering First

If the actual reason you want Next.js is React components — not Next.js's routing, data fetching, or specific framework features — Astro supports rendering React components directly inside an Astro-native EmDash site via the official `@astrojs/react` integration. This project's own `astro.config.mjs` uses exactly that pattern:

import react from "@astrojs/react";

export default defineConfig({
  integrations: [
    react(),
    emdash({ /* ... */ }),
  ],
});
emdashkits.com

With that integration in place, you write `.tsx` React components and drop them into `.astro` pages as islands — getting real React where you need interactivity, while EmDash's content queries, live updates, and generated types keep working exactly as documented. For most teams whose actual requirement is "I want to write React," this satisfies that without the deployment split and lost integration benefits of a separate Next.js app.

If You Still Want Next.js Specifically

  1. Deploy EmDash as its own Astro application (Node.js or Cloudflare Workers) — this is your content backend and admin panel.
  2. Generate an API token from the EmDash admin panel for your Next.js app to authenticate with.
  3. Configure CORS in your EmDash deployment to allow your Next.js app's origin.
  4. Fetch content in Next.js via the REST API — Server Components with `fetch`, or a data-fetching library of your choice.
  5. Handle content freshness yourself: ISR with a short revalidation window is the simplest approach; a `content:afterSave` plugin hook calling your Next.js revalidation webhook is the more precise one.

Frequently Asked Questions

Is there an official EmDash Next.js SDK or adapter?

No — EmDash's integration surface (query functions, generated types, live updates) is Astro-specific. A Next.js app talks to EmDash the same way it would talk to any REST API, with no framework-specific tooling provided.

Will content changes show up in Next.js immediately, like they do in Astro?

Not automatically. EmDash's "immediate at runtime" behavior is specific to Astro's Live Content Collections within the same deployment. A separate Next.js app needs its own revalidation strategy — ISR, on-demand revalidation, or polling.

Should I migrate my Next.js app to Astro instead?

If EmDash's specific benefits (single deployment, live updates, generated types) matter enough to your project, and you can render your interactive components as React islands within Astro via `@astrojs/react`, that's a more aligned path than bolting EmDash onto Next.js as a decoupled backend. If your Next.js app has deep framework-specific dependencies (App Router-specific patterns, Next-only libraries), that migration cost is real and worth weighing honestly.

Does EmDash support GraphQL for non-Astro frontends?

No — the documented API surface is REST, not GraphQL. If your Next.js app specifically expects a GraphQL API the way Contentful or Hygraph provide, EmDash doesn't offer that today.

The Bottom Line

EmDash is explicit that it isn't built to be a decoupled headless backend for a separate frontend framework — its REST API makes that technically possible, but you lose the live updates, type safety, and single-deployment simplicity that are EmDash's actual selling points. If your real need is React components, `@astrojs/react` inside a native EmDash site is very likely the better answer. See our full guide to connecting EmDash with Astro the way it's designed to work for that path.

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)