How to Connect EmDash CMS with Nuxt.js

How to Connect EmDash CMS with Nuxt.js

Same honest starting point as evaluating any non-Astro frontend for EmDash: its own documentation states plainly that EmDash is "not a headless CMS — tightly integrated with Astro and runs in the same deployment, rather than as a separate service you call over an API." Running EmDash behind a separate Nuxt application is technically possible via its REST API, but it's working against the platform's actual design, not with it.

Table of Contents
  1. The REST API Path
  2. What You Give Up
  3. The Alternative Worth Considering First
  4. If You Still Want Nuxt Specifically
  5. Frequently Asked Questions
  6. Is there an official EmDash Nuxt module?
  7. Will published content appear in Nuxt instantly like it does in an Astro-native EmDash site?
  8. Is switching from Nuxt to Astro worth it just for EmDash?
  9. Does EmDash offer server-side rendering hooks Nuxt could plug into directly?
  10. The Bottom Line

The REST API Path

EmDash's REST API at `/_emdash/api/` is authenticated via Bearer token and supports CORS for browser requests, with allowed origins configurable in your deployment. A Nuxt app can call it the same way it would call any third-party content API:

GET /_emdash/api/content/posts?status=published
Authorization: Bearer YOUR_API_TOKEN
emdashkits.com

A minimal fetch inside a Nuxt server route or `useAsyncData` call:

// server/api/posts.get.ts
export default defineEventHandler(async () => {
  const res = await $fetch("https://your-emdash-site.com/_emdash/api/content/posts", {
    query: { status: "published" },
    headers: { Authorization: `Bearer ${process.env.EMDASH_API_TOKEN}` },
  });
  return res.data.items;
});
emdashkits.com
<script setup>
const { data: posts } = await useFetch("/api/posts");
</script>

<template>
  <ul>
    <li v-for="post in posts" :key="post.id">{{ post.data.title }}</li>
  </ul>
</template>
emdashkits.com

What You Give Up

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).

A separate Nuxt frontend puts you in that "headless CMS" row of EmDash's own comparison table, not its integrated one:

  • No live content updates by default — Nuxt needs its own revalidation strategy (ISR-style caching, ISG, ondemand webhook revalidation) since it won't know EmDash content changed until it re-fetches.
  • No generated TypeScript types from your content model — you're working against EmDash's generic REST JSON shape rather than typed query results.
  • Two separate deployments to run and keep synchronized instead of EmDash's single-deployment model.
  • You still run EmDash's own Astro-based admin panel somewhere — a separate Nuxt frontend adds a second application, it doesn't replace the first.
Read also:

The Alternative Worth Considering First

If Vue components specifically — not Nuxt's routing or server engine — are the actual requirement, Astro officially supports Vue components as islands inside an Astro-native site via `@astrojs/vue`, the same pattern this project uses for React via `@astrojs/react`:

import vue from "@astrojs/vue";

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

With that in place, `.vue` single-file components render as islands inside `.astro` pages — genuine Vue where you need it, while EmDash's live content queries and generated types keep working as documented, no separate deployment or revalidation strategy required.

If You Still Want Nuxt Specifically

  1. Deploy EmDash as its own Astro application — this remains your content backend and admin panel regardless of what consumes it.
  2. Generate an API token from the EmDash admin panel for your Nuxt app's server-side requests.
  3. Configure CORS in your EmDash deployment to allow your Nuxt app's origin, if you're fetching client-side rather than through a Nuxt server route.
  4. Proxy requests through a Nuxt server route (`server/api/`) to keep your API token off the client rather than exposing it in browser-side code.
  5. Pick a freshness strategy: short-TTL caching is simplest; a `content:afterSave` plugin hook calling a Nuxt revalidation endpoint is more precise.

Frequently Asked Questions

Is there an official EmDash Nuxt module?

No — EmDash's deep integration (query functions, generated types, live SSR updates) is Astro-specific. A Nuxt app talks to EmDash purely over its general-purpose REST API, the same way it would to any other CMS.

Will published content appear in Nuxt instantly like it does in an Astro-native EmDash site?

No, not without extra work — that immediacy is a property of Astro's Live Content Collections running in the same process. A separate Nuxt app needs to explicitly re-fetch or be told to revalidate.

Is switching from Nuxt to Astro worth it just for EmDash?

Depends on how deep your Nuxt-specific dependencies go. If your interactive components could work as Vue islands and you don't rely on Nuxt-only modules, Astro plus `@astrojs/vue` gets you EmDash's actual advantages without a decoupled-backend workaround. If you're deeply invested in Nuxt-specific tooling, that migration cost is real.

Does EmDash offer server-side rendering hooks Nuxt could plug into directly?

No — EmDash's SSR model is internal to its own Astro integration. A separate Nuxt app renders independently and simply fetches EmDash's data over HTTP like any external API.

The Bottom Line

EmDash's documentation is explicit that it isn't designed as a decoupled backend for a separate frontend framework — the REST API makes a Nuxt integration technically possible, but at the cost of live updates, generated types, and single-deployment simplicity. If Vue components are the real requirement, `@astrojs/vue` inside a native EmDash site is the better-aligned path for most teams. See our guide to connecting EmDash with Astro natively for the fully supported route.

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)