How to Set Up Redirects in EmDash CMS

How to Set Up Redirects in EmDash CMS

Redirects come up in two real scenarios: migrating from another platform where old URLs need to keep working, and general URL cleanup on an existing EmDash site. EmDash's documented mechanism for the first is a proper, validated schema in the seed file. This covers both cases honestly.

Table of Contents
  1. The Redirects Schema
  2. Choosing the Right Status Code
  3. Redirects From a WordPress Migration
  4. When You Add Redirects Matters
  5. A Realistic Migration Checklist
  6. Frequently Asked Questions
  7. Do redirects apply automatically on every deploy, or just the first one?
  8. Can I point a redirect at an external domain?
  9. Should I use EmDash's redirect schema or Astro's redirects config?
  10. What if I have hundreds of redirects from a large migration?
  11. The Bottom Line

The Redirects Schema

A seed file's `redirects` array defines rules that preserve legacy URLs:

{
  "redirects": [
    { "source": "/old-about", "destination": "/about" },
    { "source": "/legacy-feed", "destination": "/rss.xml", "type": 308 },
    {
      "source": "/category/news",
      "destination": "/categories/news",
      "groupName": "migration"
    }
  ]
}
emdashkits.com

The full property reference:

  • source (required) — the old path, must start with /.
  • destination (required) — the new path, must start with /.
  • type (optional) — HTTP status: 301, 302, 307, or 308. Omit for the platform default.
  • enabled (optional) — whether the rule is active; defaults to true.
  • groupName (optional) — a label for filtering/searching redirects in the admin, useful for tagging "all the redirects from the March migration" as one group.
source and destination must be local paths. External URLs, protocol-relative paths, path traversal segments, and newline characters are rejected by seed validation.

That validation is a real, deliberate constraint — redirects are for reorganizing your own site's URL structure, not for pointing at arbitrary external destinations, which closes off a potential open-redirect vector by design.

Choosing the Right Status Code

  • 301 (Moved Permanently) — the standard choice for a page that's permanently moved; search engines transfer ranking signal to the new URL.
  • 302 (Found) — temporary redirect; use when the old URL might come back (a seasonal page, an A/B test).
  • 307/308 — method-preserving equivalents of 302/301, relevant mainly for non-GET requests.

For most migration and URL-cleanup scenarios, 301 is the right default.

Read also:

Redirects From a WordPress Migration

EmDash's WordPress import generates a redirect map automatically, covering the URL patterns that change during migration:

{
  "redirects": [
    { "from": "/?p=123", "to": "/posts/hello-world" },
    { "from": "/2024/01/hello-world/", "to": "/posts/hello-world" },
    { "from": "/category/news/", "to": "/categories/news" }
  ],
  "feeds": [
    { "from": "/feed/", "to": "/rss.xml" }
  ]
}
emdashkits.com

That generated map uses `from`/`to` rather than the seed schema's `source`/`destination` — reconcile the field names when converting it into your seed file's `redirects` array, or apply it directly through your hosting platform's own redirect rules (Cloudflare redirect rules, or Astro's own `redirects` config option) if you'd rather not route it through the seed mechanism at all.

When You Add Redirects Matters

Seed files apply automatically only when a database is empty and setup hasn't been completed — that's the mechanism for a fresh install or a from-scratch migration, not for adding one new redirect to an already-live site. For redirects needed after initial launch (you renamed a page, restructured a URL), the two practical options are:

  • Astro's own redirects config in astro.config.mjs, for redirects your codebase should own directly.
  • Your hosting or CDN's redirect rules (Cloudflare Redirect Rules, or your Node.js host's equivalent), for redirects you want to manage outside your codebase entirely.
// astro.config.mjs
export default defineConfig({
  redirects: {
    "/old-about": "/about",
    "/legacy-page": { status: 301, destination: "/new-page" },
  },
});
emdashkits.com

A Realistic Migration Checklist

  1. During migration, capture every generated or known old-URL-to-new-URL mapping.
  2. Convert them into the seed file's `redirects` array (source/destination/type/groupName) if bootstrapping a fresh site.
  3. Tag them with a `groupName` (e.g. "2026-migration") so you can find and audit them later.
  4. For anything discovered after the initial launch, add it via Astro's `redirects` config or your hosting platform's rules instead.
  5. Spot-check a sample of old URLs post-launch to confirm they actually redirect rather than 404.

Frequently Asked Questions

Do redirects apply automatically on every deploy, or just the first one?

The seed file's redirects apply when bootstrapping a fresh, empty database — not on every subsequent deploy of an already-running site, the same rule that governs the rest of the seed file's content and schema.

Can I point a redirect at an external domain?

No — seed validation explicitly rejects external URLs in `source` or `destination`, restricting redirects to local path-to-path rules within your own site.

Should I use EmDash's redirect schema or Astro's redirects config?

For a fresh migration or initial launch, the seed file's redirects array is the documented, purpose-built mechanism, especially useful with `groupName` for tracking a batch. For ongoing, post-launch redirect additions, Astro's own config or your hosting platform's rules are the more practical path.

What if I have hundreds of redirects from a large migration?

The seed schema handles this fine as a large array with no documented limit, and `groupName` becomes genuinely useful at that scale for filtering the admin view down to a specific migration batch rather than scrolling through everything.

The Bottom Line

EmDash's redirect schema — source, destination, status type, and an optional group label — is a real, validated feature purpose-built for migrations, most useful when bootstrapping a fresh site from a seed file. For redirects added after a site is already live, Astro's own `redirects` config or your hosting platform's rules are the more practical, always-available path. See our guide to migrating from WordPress for where the generated redirect map in that flow comes from.

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)