--- name: wordpress-plugin-to-emdash description: 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. --- # 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 | | ----------------------- | ----------------------------------------- | --------------------------------------------- | | `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. ```typescript // WordPress add_shortcode('youtube', function($atts) { return ''; }); // 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; //