How to Optimize EmDash CMS Sites for SEO

EmDash builds several SEO fundamentals in as platform behavior rather than something you bolt on — sitemap generation, robots.txt, and JSON-LD structured data are automatic. The catch is that all of them depend on one configuration value being set correctly, and it's easy to miss since your local dev server works fine without it.
Table of Contents
- The Setting That Breaks Everything If You Skip It
- Site-Wide SEO Defaults
- Per-Page Metadata: The page:metadata Hook
- What Each Metadata Kind Actually Renders
- A Realistic SEO Checklist for an EmDash Site
- Frequently Asked Questions
- Do I need a separate SEO plugin, the way WordPress relies on Yoast or Rank Math?
- How do I know if siteUrl is misconfigured right now?
- Can I set different JSON-LD schema types for different content types?
- Does EmDash handle image optimization for SEO (responsive sizes, lazy loading)?
- The Bottom Line
The Setting That Breaks Everything If You Skip It
siteUrl is the public browser-facing origin for the site. Behind a TLS-terminating reverse proxy, Astro.url returns the internal address instead of the public one. This breaks passkeys, CSRF origin matching, OAuth redirects, login redirects, MCP discovery, snapshot exports, sitemap, robots.txt, and JSON-LD structured data. Set siteUrl to fix all of these at once.
If your production deployment sits behind a reverse proxy or load balancer (genuinely common — Cloudflare, most managed Node hosts, most PaaS platforms), EmDash's internal view of its own URL can silently be `http://localhost:4321` or an internal container address instead of your real public domain. Every generated sitemap URL, every canonical tag, every JSON-LD `url` field inherits that mistake. Set it explicitly:
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({ directory: "./uploads", baseUrl: "/_emdash/api/media/file" }),
siteUrl: "https://your-real-domain.com",
});Or set it via environment variable, useful for container deployments where the public URL is only known at runtime: `EMDASH_SITE_URL` or `SITE_URL`. This is the single highest-leverage SEO check for an EmDash site — verify it before anything else.
Site-Wide SEO Defaults
Under Settings in the admin panel (or via the MCP server / REST API), EmDash exposes a dedicated `seo` settings object:
- titleSeparator — the character between page title and site title (e.g. " | ").
- defaultOgImage — the fallback Open Graph image used when a specific piece of content has none set.
- robotsTxt — custom robots.txt body; omit to use EmDash's generated default.
- googleVerification — Google Search Console verification token.
- bingVerification — Bing Webmaster Tools verification token.
Updating these via the AI assistant, as a real documented example:
"Set the default OG image to the new banner" or "Update the title separator to a vertical bar."
Per-Page Metadata: The page:metadata Hook
For anything beyond the site-wide defaults — a custom meta description on a specific page, per-post JSON-LD structured data, a noindex directive on a specific page type — EmDash's plugin system exposes a `page:metadata` hook. It's available to sandboxed plugins (no native plugin required) and covers meta tags, OpenGraph properties, an allowlisted set of `<link>` rels, and JSON-LD:
"page:metadata": async (event, ctx) => {
if (event.page.kind !== "content") return null;
return {
kind: "jsonld",
id: `schema:${event.page.content?.collection}:${event.page.content?.id}`,
graph: {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: event.page.pageTitle ?? event.page.title,
description: event.page.description,
},
};
},Your templates opt into rendering these contributions by including `<EmDashHead>` (from `emdash/ui`) in your layout — core handles validation and deduplication automatically, so multiple plugins contributing metadata for the same page don't produce duplicate tags.
What Each Metadata Kind Actually Renders
- meta — <meta name="..." content="..."> — descriptions, robots directives, Twitter cards.
- property — <meta property="..." content="..."> — OpenGraph and similar.
- link — <link rel="canonical|alternate|..." href="..."> — canonical URLs, hreflang alternates.
- jsonld — <script type="application/ld+json"> — structured data graphs.
The `link` rel is deliberately restricted to a security-locked allowlist — `canonical`, `alternate`, `author`, `license`, `nlweb`, `site.standard.document` — so a plugin can't inject a `stylesheet` or other resource-loading link through this path; that's intentionally reserved for native plugins with `page:fragments`, a separate, more privileged hook.
A Realistic SEO Checklist for an EmDash Site
- Set siteUrl explicitly in production — verify it matches your real public domain, not an internal address.
- Fill in the site-wide seo settings object (title separator, default OG image, search console verification tokens).
- Confirm the auto-generated sitemap and robots.txt are reachable at their expected URLs post-deploy.
- Add page:metadata contributions for content-type-specific JSON-LD (Article/BlogPosting schema for posts, Person schema for author pages, BreadcrumbList for hierarchical content).
- Set per-entry alt text on every image field — EmDash's image field type stores it, but it's only useful if editors actually fill it in.
- Verify canonical tags render correctly on any URL that's reachable more than one way (pagination, filtered views, tag/category archives).
Frequently Asked Questions
Do I need a separate SEO plugin, the way WordPress relies on Yoast or Rank Math?
Not for the fundamentals — sitemap, robots.txt, and site-wide SEO settings are built into EmDash core. You'd reach for the `page:metadata` hook (in a first-party or third-party plugin) for anything more specialized, but there's no dependency on an equivalent to WordPress's SEO plugin ecosystem for baseline coverage.
How do I know if siteUrl is misconfigured right now?
Check your live sitemap.xml and any page's JSON-LD output — if URLs point to localhost, an internal container address, or the wrong domain, siteUrl (or the SITE_URL / EMDASH_SITE_URL environment variable) needs to be set explicitly in your production deployment.
Can I set different JSON-LD schema types for different content types?
Yes — the `page:metadata` hook receives the current page's content type and collection in its event payload, so a single hook can branch and return BlogPosting schema for posts, Product schema for a products collection, and so on.
Does EmDash handle image optimization for SEO (responsive sizes, lazy loading)?
Yes, for images going through EmDash's own image rendering component — it generates responsive srcset output via Astro's image service where possible, with lazy loading and async decoding set by default.
The Bottom Line
Most of EmDash's SEO foundation is automatic — sitemap, robots.txt, and JSON-LD generation ship as platform behavior, not a plugin you have to install. The one thing that will quietly break all three if you miss it is `siteUrl` behind a reverse proxy, so verify that first. From there, the site-wide `seo` settings object and the `page:metadata` hook cover everything from search console verification to content-type-specific structured data. See our guide to customizing content models if you're still shaping the content types this metadata will describe.




Comments