An Emdash CMS plugin that lets AI agents (Claude, ChatGPT, etc.) assign taxonomy terms and set SEO metadata when creating content via the Emdash MCP server.
The Emdash MCP server is great for creating and updating content, but has two gaps that make it hard to use from an AI agent on mobile or in agentic workflows:
- No
content_set_termstool. There is no way for an agent to assign taxonomy terms (categories, tags, etc.) through the MCP server. - SEO metadata serialization bug. The
seoparameter incontent_updatedoes not persist correctly. (Discussion #1070)
This plugin works around both gaps by letting the agent embed a small metadata block directly in the post content. The plugin intercepts every save via content:afterSave, processes the block, and strips it before the content reaches readers.
-
When creating a post, the agent appends a metadata block to the Portable Text content field:
<!-- ebt-meta:{"categories":["history"],"tags":["dallas"],"seo_title":"My Title"} --> -
On
content:afterSave, this plugin scans all Portable Text fields for the block. -
It resolves taxonomy slugs to database IDs, assigns terms, and sets SEO — all in a single pass.
-
It strips the block from the content and saves the cleaned version.
The save pipeline sees only the final, clean content. The metadata block is never visible to site visitors.
npm install emdash-auto-metaIf you're referencing the plugin locally via a file: path during development, also run npm install inside the plugin directory so Vite can resolve its peer dependency:
cd path/to/emdash-auto-meta && npm installRequirements:
- Emdash
^0.12.0 - Cloudflare Workers (paid plan) — this plugin uses
getDb()fromemdash/runtimeand must run as a trusted plugin in your Workers environment - Must be registered in
plugins: [](notsandboxed: [])
// astro.config.mjs
import { emdashAutoMeta } from "emdash-auto-meta";
export default defineConfig({
integrations: [
emdash({
plugins: [
emdashAutoMeta({
taxonomyMap: {
categories: "category", // your Emdash taxonomy name
tags: "tag",
regions: "regions",
eras: "eras",
},
}),
],
}),
],
});Append this block to the end of any Portable Text content field when creating a post. The agent writes it; the plugin strips it.
<!-- ebt-meta:{
"categories": ["history-landmarks"],
"tags": ["dallas-texas", "new-tag"],
"regions": ["prairies-lakes"],
"eras": ["the-oil-boom"],
"seo_title": "Your SEO Title",
"seo_description": "Your meta description."
} -->
The block must be a single Portable Text block node containing the raw HTML comment. All fields are optional — include only what you need.
Both key styles are accepted for underscored fields. seo_title, seo_description, and content_types can be written with underscores or spaces ("seo title", "seo description", "content types") — both are read identically. Space-separated keys exist because Markdown parses a lone underscore as an italic delimiter; if a block has two or more underscored keys, the Markdown-to-PortableText conversion (which runs before this plugin sees the content) can split the block into multiple spans. As of v1.3.0 the plugin reconstructs the full block text from every span before parsing, so underscored keys work reliably too — but the space-separated style remains supported for content written under the old workaround, and as a defensive fallback.
interface AutoMeta {
/** Slugs of existing category terms to assign */
categories?: string[];
/** Slugs of tag terms to assign (auto-created if autoCreateTags is true) */
tags?: string[];
/** Slugs of existing region terms to assign */
regions?: string[];
/** Slugs of existing era terms to assign */
eras?: string[];
/** SEO title tag */
seo_title?: string;
/** SEO meta description */
seo_description?: string;
}emdashAutoMeta({
metaPrefix?: string;
autoCreateTags?: boolean;
logLevel?: "silent" | "info" | "debug";
taxonomyMap?: {
categories?: string;
tags?: string;
regions?: string;
eras?: string;
};
})| Option | Type | Default | Description |
|---|---|---|---|
metaPrefix |
string |
"<!-- ebt-meta:" |
The prefix string that identifies the metadata block. Change this if your agents use a different convention. |
autoCreateTags |
boolean |
true |
When true, tag slugs that don't exist in the database are created automatically. Set to false to require all tags to be pre-created. |
logLevel |
"silent" | "info" | "debug" |
"info" |
Controls log output. "debug" logs every step including successful updates. "silent" suppresses everything except errors. |
taxonomyMap.categories |
string |
"category" |
The Emdash taxonomy name that maps to the categories key in the metadata block. |
taxonomyMap.tags |
string |
"tag" |
The Emdash taxonomy name that maps to the tags key. |
taxonomyMap.regions |
string |
"regions" |
The Emdash taxonomy name that maps to the regions key. |
taxonomyMap.eras |
string |
"eras" |
The Emdash taxonomy name that maps to the eras key. |
categories,regions,eras— slugs must match terms that already exist in your Emdash database. Unknown slugs are logged as warnings and skipped.tags— whenautoCreateTags: true(the default), unknown slugs are created as new terms with a label derived from the slug ("dallas-texas"→"Dallas Texas"). SetautoCreateTags: falseto treat tags the same as other taxonomies.- Taxonomy names in
taxonomyMapmust match thenamefield in your Emdash seed exactly, not the display label.
Add something like this to your agent's system prompt or instructions:
When creating a post in Emdash, append a metadata block as the final
paragraph of the content field using this exact format:
<!-- ebt-meta:{"categories":["slug"],"tags":["slug"],"seo_title":"Title","seo_description":"Description."} -->
Only include the fields you have values for. Taxonomy slugs must be
lowercase and hyphenated (e.g. "history-landmarks", not "History Landmarks").
- Trusted plugin only. This plugin uses
getDb()fromemdash/runtimeto assign taxonomy terms, which requires direct database access. It cannot run insandboxed: []mode. - Cloudflare Workers (paid plan) required. The trusted plugin mode that provides
getDb()is only available on Cloudflare Workers with a paid plan. - Media upload not supported in v1.0. Attaching images via URL is planned for v1.1, pending a reliable solution for server-side image fetching in the Cloudflare Workers environment. For now, upload images manually through the Emdash admin after creating a post.
- Metadata block must be in a Portable Text field. The plugin scans only
_type: "block"nodes inside array fields. It will not find the block in plain text or other field types. - One metadata block per save. Only the first block found is processed. If the agent writes multiple blocks, only the first is consumed; the rest remain in the content.
- Unparseable meta block. If the block is found but still isn't valid JSON after span reconstruction, the plugin does not silently ignore it: it logs an error with the post id, the reconstructed raw string, and the parse error, and writes the same to KV under
metaParseError:<collection>:<id>so the failure is visible even thoughcontent:afterSaveruns after the MCP call has already returned success.
This plugin was built as a workaround for two gaps in the Emdash MCP server. If you're interested in seeing native MCP support for taxonomy assignment and SEO metadata, join the discussion:
github.com/emdash-cms/emdash/discussions/1070
Bug reports and pull requests welcome. Please open an issue before submitting a PR for anything beyond a small bug fix.
AI-assisted taxonomy and SEO metadata plugin for EmDash CMS. Designed by Marcus Shaw for Every Bit Texas. Coded by Claude Code.
Built for EmDash CMS — star the repo to support open-source CMS development.
MIT