emdash-cms/emdash

wordpress-plugin-to-emdash

Port a WordPress plugin to EmDash CMS. Use this skill when asked to migrate, convert, or port a WordPress plugin, theme functionality, or custom post type to EmDash. Provides concept mapping and implementation patterns.

First seen Apr 2, 2026

Installation

$ npx skills add emdash-cms/emdash --skill wordpress-plugin-to-emdash

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from emdash-cms/emdash.

npx skills add emdash-cms/emdash

Browse all from emdash-cms/emdash

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 12.3K
License LICENSE
Default branch main
Open issues 168
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 15,168 B
  • docs SUMMARY.md 253 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 52 installs

SKILL.md

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

  1. Understand the plugin — What does it do, not how
  2. Identify concepts — Content types, admin pages, hooks, shortcodes
  3. Map to EmDash — Use the tables below
  4. Implement in TypeScript — Clean room, not line-by-line port. Use the creating-plugins skill for implementation details.
  5. Test behaviour — Same result, different implementation

Concept Mapping

Content & Data

WordPress EmDash Notes
registerposttype() SchemaRegistry.createCollection() Via Admin API or seed file
register_taxonomy() emdashtaxonomy_defs table Hierarchical or flat, attached to collections
register_meta() / ACF Collection fields via SchemaRegistry All become typed schema fields
getpostmeta() 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
wpinsertpost() POST /_emdash/api/content/{type} REST API
wpupdatepost() PUT /_emdash/api/content/{type}/{id} REST API
wpdeletepost() 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
registernavmenu() Create menu via admin or seed emdashmenus table
wpnavmenu() getMenu(name) Returns { items: MenuItem[] }
wpnavmenu_item emdashmenu_items table Type: custom, page, post, taxonomy
menuitemobjectid referenceid + referencecollection Links to content entries
Menu locations Query by name in templates No locations concept — direct query

Taxonomies

WordPress EmDash Notes
register_taxonomy() emdashtaxonomy_defs table Define via admin, seed, or API
get_terms() getTaxonomyTerms(name) Returns tree for hierarchical
gettheterms() getEntryTerms(collection, id, name) Terms for specific entry
wpsetpost_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() emdashwidget_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
addmenupage() admin.pages in definePlugin() Plugin config
addsubmenupage() Nested admin pages Parent determines hierarchy
addsettingssection() admin.settingsSchema Auto-generated settings page
addmetabox() Field groups in collection schema UI config in schema
wpenqueuescript() 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
addaction('savepost') content:afterSave hook Filter by event.collection
addaction('beforedelete_post') content:beforeDelete hook Return false to prevent
addaction('wphead') page:metadata / page:fragments hook Metadata is sandbox-safe; scripts need trusted plugin
addaction('restapi_init') definePlugin({ routes }) Trusted only
addfilter('thecontent') Portable Text components Custom block renderers
addfilter('thetitle') Template logic Handle in Astro component

Frontend Output

WordPress EmDash Notes
add_shortcode() Portable Text custom block Content → block. Template → component. Trusted only.
registerblocktype() PT block + componentsEntry Block data → Astro component props. Trusted only.
Template tags Astro expressions getthetitle(){post.data.title}
Widgets Widget area + components Query with getWidgetArea()

Plugin Storage

WordPress EmDash Notes
getoption('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 getoption/updateoption 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 wpinsertterm(), registernavmenu(), 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 wpnavmenu)

// 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:

  1. Deep WP integration — Hooks into WP core features not in EmDash
  2. Theme dependencies — Assumes specific theme structure
  3. Multisite features — Not supported
  4. Complex WP_Query — Meta queries may need custom implementation
  5. Direct SQL — Schema differs, use Kysely or plugin storage
  6. Session/transient abuse — Needs proper caching layer
  7. User capability checks — Review role mapping (future)
  8. ob_start() buffering — PHP pattern, rethink for streaming
  9. Cron jobswpscheduleevent() has no direct equivalent; needs platform cron

Output Format

When porting a plugin, provide:

  1. Analysis — What the WP plugin does (concepts, not code)
  2. Concept mapping — Which WP concepts map to which EmDash features
  3. Plugin codesrc/descriptor.ts and src/index.ts (use creating-plugins skill for structure)
  4. Seed data — If plugin needs default taxonomies/menus/widgets
  5. Astro components — For frontend output
  6. Flags — Anything needing human decision