Nineteen content widgets for EmDash posts and pages
Callouts, numbered steps, tabs, code blocks, video facades and more for the block editor. What ships in the plugin, how to install it, and where to see it working.

Articles on this site needed more than paragraphs. Callouts for warnings, numbered steps for walkthroughs, code blocks for commands, a table of contents on long posts. The stock block editor ships a lean set on purpose, so I built the Content Widgets plugin: nineteen block types for the EmDash editor, usable in post bodies and on regular pages.
The short version: install @bitdoze.com/emdashhq-widgets from the registry, allow the widget types on your blocks field, copy the renderers from the package's site/ folder into your project, and the editor's slash menu grows nineteen new entries. Everything renders on the server, picks up the site's type scale and colors through CSS variables, and ships no client-side bundle.

What actually ships
Nineteen blocks is a lot of names. Grouped by the job they do:
Writing and structure
- Prose: Portable Text paragraphs between the widgets
- Notice: info, success, warning and danger callouts for gotchas
- Checklist and steps: task lists and numbered walkthroughs
- Accordion and tabs: collapsed sections and side-by-side variants
- Divider: a styled rule between sections
Media and embeds
- Image: figure with caption, served through the media pipeline
- Code: syntax highlighting with filename and language label
- YouTube: a click-to-load facade, privacy-enhanced host
- Embed: a sandboxed iframe for anything else
Reference and calls to action
- Button: links with twelve built-in icons
- Cards: small link grids for related reading
- Quote: pull quotes with attribution
- Facts: a label/value fact sheet for specs and pricing
- Product: a review box with rating, pros/cons and an affiliate disclosure line
- Table of contents: built from the post's own headings
- Series: previous/next navigation for multi-part posts
- Latest posts: a recent-articles list that skips the current post
What the editor actually stores
This is a real notice block lifted from the demo post's stored body - the whole record, nothing trimmed:
{
"_type": "widget_notice",
"_version": 1,
"_key": "meywrdak",
"kind": "info",
"title": "Info notice",
"body": [
{
"_type": "block",
"style": "normal",
"children": [
{ "_type": "span", "text": "A neutral note...", "marks": [] }
]
}
]
}Three details matter. The _type is the contract between editor and renderer; _version versions the shape so a plugin update can migrate old blocks; and _key is immutable - the API rejects an update that reuses a key with a different type, which is worth knowing the first time you rewrite a body through the REST API. Rich text fields like body carry real Portable Text, so spans, marks and links survive intact.
Install it
Admin → Plugins → Browse → find Content Widgets → Install. The package is @bitdoze.com/emdashhq-widgets on plugins.emdashcms.com. This is the path for most sites: the registry handles versions and updates.
Clone the plugin into your site's plugins/ directory, register it under sandboxed: in astro.config.mjs, and add it to package.json as a file: dependency. Take this path when you want to edit the widgets themselves.
Allow the block types
New block types do not appear in the editor until a blocks field allows them. Open the collection, edit the blocks field, and tick the widget types under allowed types. In a seed file it looks like this:
{
"slug": "body",
"type": "blocks",
"validation": {
"allowedTypes": ["widget_prose", "widget_notice", "widget_steps", "widget_code"]
}
}Wire the renderers
Two ways to render them. Spread the shipped component map into your existing block renderer, or drop the standalone WidgetBlocks component on a widget-only field like a post body:
import { widgetComponents } from "./widgets/widget-components";
import WidgetBlocks from "./widgets/WidgetBlocks.astro";
// path 1: merge into the page's block map
const blockComponents = defineBlockComponents({
...widgetComponents,
});
// path 2: a widget-only field, like a post body
<WidgetBlocks value={post.data.body} />
Anatomy of one widget
Every renderer is the same small shape - props carry the stored block value, markup is plain HTML, and styling reads theme tokens instead of hardcoding colors. Skinned down to its skeleton, a widget looks like this:
---
const { value } = Astro.props;
---
<aside class={`wgt-notice wgt-notice-${value.kind}`}>
{value.title && <p class="wgt-notice-title">{value.title}</p>}
<PortableText value={value.body} />
</aside>
<style>
.wgt-notice {
border-left: 3px solid var(--color-brand);
background: var(--color-surface);
padding: var(--spacing-md);
}
</style>The wgt- class prefix keeps widget CSS from colliding with the theme, and var() tokens mean the same file renders correctly in a cream-paper theme or an ink-dark one. Copy a component, change the fields, and you have a custom widget that stores Portable Text like the built-ins.
Pages get them too
Nothing here is post-specific. The contact page's form block and this site's marketing pages use the same mechanism: any blocks field on any collection can allow the widget types, and the same WidgetBlocks component renders them. Write the field once, use the widgets everywhere.
See it working
Every block on the list is rendering in the demo post linked below, the same one you can open in this site's editor to inspect how it was put together - including the affiliate product card, which got a layout fix and an affiliate-card restyle in 1.0.3.
- Live demo post
All nineteen blocks in one article.
- Registry listing
Install button, changelog and screenshots.
- Source
The plugin lives in this site's repo.
- Version
- 1.0.3
- License
- MIT
- Blocks
- 19
- Price
- Free