wordpress-plugin-to-emdash
Porting WordPress Plugins to EmDash
This skill maps WordPress concepts to their EmDash equivalents for plugin porting. For general plugin authoring details (plugin structure, definePlugin(), hooks, storage, admin UI, etc.), use the creating-plugins skill.
Migration Approach
- Understand the plugin — What does it do, not how
- Identify concepts — Content types, admin pages, hooks, shortcodes
- Map to EmDash — Use the tables below
- Implement in TypeScript — Clean room, not line-by-line port. Use the creating-plugins skill for implementation details.
- Test behaviour — Same result, different implementation
Concept Mapping
Content & Data
| WordPress | EmDash | Notes |
|---|---|---|
register_post_type() |
SchemaRegistry.createCollection() |
Via Admin API or seed file |
register_taxonomy() |
_emdash_taxonomy_defs table |
Hierarchical or flat, attached to collections |
register_meta() / ACF |
Collection fields via SchemaRegistry | All become typed schema fields |
get_post_meta() |
entry.data.fieldName |
Direct typed access |
get_option() |
getSiteSetting() / ctx.kv |
Site settings or plugin-namespaced KV |
WP_Query |
getEmDashCollection() |
Runtime queries with filters |
get_post($id) |
getEmDashEntry(collection, slug) |
Returns entry or null |
wp_insert_post() |
POST /_emdash/api/content/{type} |
REST API |
wp_update_post() |
PUT /_emdash/api/content/{type}/{id} |
REST API |
wp_delete_post() |
DELETE /_emdash/api/content/{type}/{id} |
Soft delete |
| Custom tables | Plugin storage collections | ctx.storage.collectionName.put/get/query |
Site Configuration
| WordPress | EmDash | Notes |
|---|---|---|
get_bloginfo('name') |
getSiteSetting('title') |
From options table with site: prefix |
get_option('blogdesc') |
getSiteSetting('tagline') |
Site settings API |
| Theme Customizer | Site Settings admin page | /_emdash/admin/settings |
site_icon |
getSiteSetting('favicon') |
Media reference |
custom_logo |
getSiteSetting('logo') |
Media reference |
Navigation Menus
| WordPress | EmDash | Notes |
|---|---|---|
register_nav_menu() |
Create menu via admin or seed | _emdash_menus table |
wp_nav_menu() |
getMenu(name) |
Returns { items: MenuItem[] } |
wp_nav_menu_item |
_emdash_menu_items table |
Type: custom, page, post, taxonomy |
_menu_item_object_id |
reference_id + reference_collection |
Links to content entries |
| Menu locations | Query by name in templates | No locations concept — direct query |
Taxonomies
| WordPress | EmDash | Notes |
|---|---|---|
register_taxonomy() |
_emdash_taxonomy_defs table |
Define via admin, seed, or API |
get_terms() |
getTaxonomyTerms(name) |
Returns tree for hierarchical |
get_the_terms() |
getEntryTerms(collection, id, name) |
Terms for specific entry |
wp_set_post_terms() |
TaxonomyRepository.setTermsForEntry() |
Replace terms for entry |
| Hierarchical taxonomy | hierarchical: true in definition |
Categories-style |
| Flat taxonomy | hierarchical: false |
Tags-style |
Widgets & Sidebars
| WordPress | EmDash | Notes |
|---|---|---|
register_sidebar() |
_emdash_widget_areas table |
Create via admin or seed |
dynamic_sidebar() |
getWidgetArea(name) |
Returns { widgets: Widget[] } |
WP_Widget class |
Widget types: content, menu, component | Simplified — 3 types only |
| Text widget | type: 'content' + Portable Text |
Rich text widget |
| Nav Menu widget | type: 'menu' + menuName |
References a menu |
| Custom widgets | type: 'component' + componentId |
Plugin-registered components |
Admin UI
| WordPress | EmDash | Notes |
|---|---|---|
add_menu_page() |
admin.pages in definePlugin() |
Plugin config |
add_submenu_page() |
Nested admin pages | Parent determines hierarchy |
add_settings_section() |
admin.settingsSchema |
Auto-generated settings page |
add_meta_box() |
Field groups in collection schema | UI config in schema |
wp_enqueue_script() |
ESM imports in admin components | React (trusted) or Block Kit (sandboxed) |
| Admin notices | Toast notifications | Via admin UI framework |
Hooks
| WordPress | EmDash | Notes |
|---|---|---|
add_action('init') |
plugin:install hook |
Runs once on first install |
add_action('save_post') |
content:afterSave hook |
Filter by event.collection |
add_action('before_delete_post') |
content:beforeDelete hook |
Return false to prevent |
add_action('wp_head') |
page:metadata / page:fragments hook |
Metadata is sandbox-safe; scripts need trusted plugin |
add_action('rest_api_init') |
definePlugin({ routes }) |
Trusted only |
add_filter('the_content') |
Portable Text components | Custom block renderers |
add_filter('the_title') |
Template logic | Handle in Astro component |
Frontend Output
| WordPress | EmDash | Notes |
|---|---|---|
add_shortcode() |
Portable Text custom block | Content → block. Template → component. Trusted only. |
register_block_type() |
PT block + componentsEntry |
Block data → Astro component props. Trusted only. |
| Template tags | Astro expressions | get_the_title() → {post.data.title} |
| Widgets | Widget area + components | Query with getWidgetArea() |
Plugin Storage
| WordPress | EmDash | Notes |
|---|---|---|
get_option('plugin_*') |
ctx.kv.get(key) |
Namespaced to plugin automatically |
update_option() |
ctx.kv.set(key, value) |
Scoped KV storage |
delete_option() |
ctx.kv.delete(key) |
Delete single key |
| Custom tables | ctx.storage.collection |
Document collections with indexes |
| Transients | Plugin KV | No TTL yet |
Porting-Specific Patterns
These patterns cover WordPress-specific concepts that don't have a direct 1:1 mapping. For general plugin patterns (defining hooks, storage, routes, admin UI), see the creating-plugins skill.
Shortcodes → Portable Text Blocks
WordPress shortcodes ([youtube id="xxx"]) become Portable Text custom block types. The block data replaces shortcode attributes, and an Astro component replaces the shortcode render function. This is a trusted-only feature.
// WordPress
add_shortcode('youtube', function($atts) {
return '<iframe src="https://youtube.com/embed/' . $atts['id'] . '"></iframe>';
});
// EmDash — block type declaration in definePlugin()
admin: {
portableTextBlocks: [{
type: "youtube",
label: "YouTube Video",
icon: "video",
fields: [
{ type: "text_input", action_id: "id", label: "YouTube URL" },
{ type: "text_input", action_id: "title", label: "Title" },
],
}],
}
// EmDash — Astro component for rendering
// src/astro/YouTube.astro
const { id, title } = Astro.props.node;
const videoId = id?.match(/(?:v=|youtu\.be\/)([^&]+)/)?.[1] ?? id;
// <iframe src={`https://youtube-nocookie.com/embed/${videoId}`} ... />
Options API → Plugin KV
WordPress's get_option/update_option maps to the plugin KV store. The key difference: WordPress options are global, EmDash KV is automatically scoped to the plugin.
// WordPress
$count = get_option("myplugin_post_count", 0);
update_option("myplugin_post_count", $count + 1);
delete_option("myplugin_temp_data");
// EmDash — no prefix needed, automatically scoped
const count = (await ctx.kv.get<number>("post-count")) ?? 0;
await ctx.kv.set("post-count", count + 1);
await ctx.kv.delete("temp-data");
Custom Database Tables → Storage Collections
WordPress plugins that create custom tables with $wpdb->query("CREATE TABLE ...") should use EmDash's storage collections instead. No migrations needed — declare the schema in definePlugin() and it's automatically provisioned.
// WordPress
$wpdb->insert($table, ['form_id' => $id, 'data' => json_encode($data), 'created_at' => current_time('mysql')]);
$results = $wpdb->get_results("SELECT * FROM $table WHERE form_id = '$id' ORDER BY created_at DESC LIMIT 50");
// EmDash — declared in definePlugin()
storage: {
submissions: {
indexes: ["formId", "createdAt", ["formId", "createdAt"]],
},
},
// In a hook or route handler
await ctx.storage.submissions!.put(entryId, { formId, data, createdAt: new Date().toISOString() });
const result = await ctx.storage.submissions!.query({
where: { formId },
orderBy: { createdAt: "desc" },
limit: 50,
});
Seeding Data (replaces starter content, theme setup)
WordPress plugins that call wp_insert_term(), register_nav_menu(), or insert default content on activation should use a seed file:
{
"version": "1",
"settings": { "title": "My Site", "tagline": "Welcome" },
"taxonomies": [
{
"name": "category",
"label": "Categories",
"hierarchical": true,
"collections": ["posts"],
"terms": [
{ "slug": "news", "label": "News" },
{ "slug": "tutorials", "label": "Tutorials" }
]
}
],
"menus": [
{
"name": "primary",
"label": "Primary Navigation",
"items": [
{ "type": "custom", "label": "Home", "url": "/" },
{ "type": "page", "ref": "about", "collection": "pages" }
]
}
],
"redirects": [
{ "source": "/?p=123", "destination": "/about" },
{ "source": "/old-contact", "destination": "/contact", "type": 301 }
]
}
Save to .emdash/seed.json (or wire up via package.json#emdash.seed); the runtime applies it on the next first-boot when the database is empty.
Use redirects for legacy WordPress URLs that still receive traffic after migration.
Querying Content (replaces WP_Query)
// WordPress
$query = new WP_Query(['post_type' => 'post', 'category_name' => 'tech', 'posts_per_page' => 10]);
// EmDash — in Astro component frontmatter
import { getEmDashCollection, getEntryTerms } from "emdash";
const { entries } = await getEmDashCollection("posts", {
where: { category: "technology" },
limit: 10,
});
Menus (replaces wp_nav_menu)
// WordPress
wp_nav_menu(['theme_location' => 'primary']);
// EmDash — in Astro component
import { getMenu } from "emdash";
const nav = await getMenu("primary");
// nav.items[].label, nav.items[].url, nav.items[].children
Widget Areas (replaces dynamic_sidebar)
// WordPress
dynamic_sidebar("sidebar-1");
// EmDash — in Astro component
import { getWidgetArea } from "emdash";
const sidebar = await getWidgetArea("sidebar");
// sidebar.widgets[].type: "content" | "menu" | "component"
Red Flags (Need Human Decision)
Flag these for review — they may need architectural decisions:
- Deep WP integration — Hooks into WP core features not in EmDash
- Theme dependencies — Assumes specific theme structure
- Multisite features — Not supported
- Complex WP_Query — Meta queries may need custom implementation
- Direct SQL — Schema differs, use Kysely or plugin storage
- Session/transient abuse — Needs proper caching layer
- User capability checks — Review role mapping (future)
- ob_start() buffering — PHP pattern, rethink for streaming
- Cron jobs —
wp_schedule_event()has no direct equivalent; needs platform cron
Output Format
When porting a plugin, provide:
- Analysis — What the WP plugin does (concepts, not code)
- Concept mapping — Which WP concepts map to which EmDash features
- Plugin code —
src/descriptor.tsandsrc/index.ts(use creating-plugins skill for structure) - Seed data — If plugin needs default taxonomies/menus/widgets
- Astro components — For frontend output
- Flags — Anything needing human decision
More from emdash-cms/emdash
emdash-cli
Use the EmDash CLI to manage content, schema, media, and more. Use this skill when you need to interact with a running EmDash instance from the command line — creating content, managing collections, uploading media, generating types, or scripting CMS operations.
31building-emdash-site
Build and customize EmDash CMS sites on Astro. Use when creating pages, defining collections, writing seed files, querying content, rendering Portable Text, setting up menus/taxonomies/widgets, configuring deployment, or any task involving an EmDash-powered Astro site. Assumes basic Astro knowledge but provides all EmDash-specific patterns.
31creating-plugins
Create EmDash CMS plugins with hooks, storage, settings, admin UI, API routes, and Portable Text block types. Use this skill when asked to build, scaffold, or implement an EmDash plugin, or when creating plugin features like custom block types, admin pages, or content hooks.
23adversarial-reviewer
Adversarial code review that assumes bugs exist and hunts for them. Use when asked to review code, find bugs, audit for correctness, stress-test a PR, or when someone says "tear this apart" or "what's wrong with this". Give no benefit of the doubt — every line is guilty until proven innocent.
23agent-browser
Browser automation for testing and verification. Use when you need to interact with web UIs, verify visual changes, fill forms, or capture screenshots.
15wordpress-theme-to-emdash
Port WordPress themes to EmDash CMS. Use when asked to convert, migrate, or port a WordPress theme to EmDash, or when creating an EmDash site that should match an existing WordPress site's design. Handles design extraction, template conversion, and EmDash-specific features like menus, taxonomies, and widgets.
15