Porting WordPress Plugins
WordPress plugins extend the CMS with custom functionality. EmDash provides equivalent extension points through its plugin system. This guide shows how to translate common WordPress patterns.
Plugin Architecture Comparison
Section titled “Plugin Architecture Comparison”| WordPress | EmDash |
|---|---|
PHP files in wp-content/plugins/ | TypeScript modules registered in config |
add_action() / add_filter() | Hook functions |
| Admin menu pages | Admin panel routes |
| REST API endpoints | API route handlers |
Database via $wpdb | Storage via ctx.storage |
Options via wp_options | Key-value via ctx.kv |
| Post meta | Collection fields |
| Shortcodes | Portable Text custom blocks |
| Gutenberg blocks | Portable Text custom blocks |
Concept Mapping
Section titled “Concept Mapping”Actions and Filters → Hooks
Section titled “Actions and Filters → Hooks”WordPress uses add_action() and add_filter() for extensibility. EmDash uses typed hook functions.
// WordPress actionadd_action('save_post', function($post_id, $post) { if ($post->post_type !== 'product') return; update_post_meta($post_id, 'last_updated', time());}, 10, 2);
// WordPress filteradd_filter('the_content', function($content) {return $content . '<p>Read more articles</p>';});// EmDash hookexport const hooks = { 'content:beforeSave': async (ctx, entry) => { if (entry.collection !== 'products') return entry; return { ...entry, data: { ...entry.data, lastUpdated: new Date().toISOString() } }; },
'content:afterRender': async (ctx, html) => { return html + '<p>Read more articles</p>'; }};Available Hooks
Section titled “Available Hooks”| Hook | Equivalent WordPress Hook | Purpose |
|---|---|---|
content:beforeSave | wp_insert_post_data | Modify content before save |
content:afterSave | save_post | React after content saved |
content:beforeDelete | before_delete_post | Validate before deletion |
content:afterRender | the_content | Transform rendered output |
media:beforeUpload | wp_handle_upload_prefilter | Validate/transform uploads |
media:afterUpload | add_attachment | React after upload |
admin:init | admin_init | Admin panel initialization |
api:request | rest_pre_dispatch | Intercept API requests |
Database Access
Section titled “Database Access”WordPress uses $wpdb for direct database queries. EmDash provides ctx.storage for structured data access.
global $wpdb;
// Insert$wpdb->insert('custom_table', ['name' => 'Example','value' => 42]);
// Query$results = $wpdb->get_results("SELECT \* FROM custom_table WHERE value > 10");
// Update$wpdb->update('custom_table',['value' => 50],['name' => 'Example']);// Using ctx.storage (D1/SQLite)const db = ctx.storage;
// Insertawait db.prepare( 'INSERT INTO custom_table (name, value) VALUES (?, ?)').bind('Example', 42).run();
// Queryconst results = await db.prepare( 'SELECT * FROM custom_table WHERE value > ?').bind(10).all();
// Updateawait db.prepare( 'UPDATE custom_table SET value = ? WHERE name = ?').bind(50, 'Example').run();Options Storage
Section titled “Options Storage”WordPress uses get_option() / update_option(). EmDash uses ctx.kv for key-value storage.
// Get option$api_key = get_option('my_plugin_api_key', '');
// Set optionupdate_option('my_plugin_api_key', 'abc123');
// Delete optiondelete_option('my_plugin_api_key');// Get valueconst apiKey = await ctx.kv.get('my_plugin_api_key') ?? '';
// Set valueawait ctx.kv.put('my_plugin_api_key', 'abc123');
// Delete valueawait ctx.kv.delete('my_plugin_api_key');Custom Post Types → Collections
Section titled “Custom Post Types → Collections”WordPress registers post types with register_post_type(). EmDash uses collections defined in the admin UI or via API.
register_post_type('product', [ 'labels' => [ 'name' => 'Products', 'singular_name' => 'Product' ], 'public' => true, 'supports' => ['title', 'editor', 'thumbnail'], 'has_archive' => true]);
register_meta('post', 'price', ['type' => 'number','single' => true,'show_in_rest' => true]);// Create via APIawait fetch('/_emdash/api/schema/collections', { method: 'POST', body: JSON.stringify({ slug: 'products', label: 'Products', labelSingular: 'Product', fields: [ { slug: 'title', type: 'string', required: true }, { slug: 'content', type: 'portableText' }, { slug: 'featuredImage', type: 'media' }, { slug: 'price', type: 'number' } ] })});Collections are typically created through the admin UI at Content Types → New Content Type.
Shortcodes → Portable Text Blocks
Section titled “Shortcodes → Portable Text Blocks”WordPress shortcodes embed dynamic content. EmDash uses custom Portable Text blocks with React/Astro components.
// Register shortcodeadd_shortcode('product_card', function($atts) { $atts = shortcode_atts([ 'id' => 0, 'show_price' => true ], $atts);
$product = get_post($atts['id']); $price = get_post_meta($atts['id'], 'price', true);
return sprintf( '<div class="product-card"> <h3>%s</h3> %s </div>', esc_html($product->post_title), $atts['show_price'] ? '<p>$' . esc_html($price) . '</p>' : '' );
});
// Usage in content: [product_card id="123" show_price="true"]// Define Portable Text block schemaconst productCardBlock = { name: 'productCard', type: 'object', fields: [ { name: 'productId', type: 'reference', to: 'products' }, { name: 'showPrice', type: 'boolean', default: true } ]};
// Render component (Astro)---// src/components/ProductCard.astroimport { getEntry } from 'emdash';
const { productId, showPrice = true } = Astro.props;const product = await getEntry('products', productId);---
<div class="product-card"> <h3>{product.data.title}</h3> {showPrice && <p>${product.data.price}</p>}</div>// Register with Portable Text rendererimport ProductCard from "./components/ProductCard.astro";
const components = { types: { productCard: ProductCard, },};
// Usage: <PortableText value={content} components={components} />Admin Pages
Section titled “Admin Pages”WordPress uses add_menu_page() for admin screens. EmDash plugins define admin routes.
add_action('admin_menu', function() { add_menu_page( 'My Plugin Settings', 'My Plugin', 'manage_options', 'my-plugin', 'render_settings_page', 'dashicons-admin-generic', 30 );});
function render_settings_page() {?>
<div class="wrap"><h1>My Plugin Settings</h1><form method="post" action="options.php"><?php settings_fields('my_plugin_options'); ?><input type="text" name="api_key" value="<?php echo esc_attr(get_option('api_key')); ?>"><?php submit_button(); ?></form></div><?php}// Plugin definitionexport default { name: 'my-plugin',
admin: { // Menu entry menu: { label: 'My Plugin', icon: 'settings' },
// Admin page component pages: [{ path: '/settings', component: () => import('./admin/Settings') }] }};// admin/Settings.tsx (React component)import { useState, useEffect } from "react";
export default function Settings() { const [apiKey, setApiKey] = useState("");
useEffect(() => { fetch("/_emdash/api/plugins/my-plugin/settings") .then((r) => r.json()) .then((data) => setApiKey(data.apiKey || "")); }, []);
const save = async () => { await fetch("/_emdash/api/plugins/my-plugin/settings", { method: "POST", body: JSON.stringify({ apiKey }), }); };
return ( <div> <h1>My Plugin Settings</h1> <input value={apiKey} onChange={(e) => setApiKey(e.target.value)} /> <button onClick={save}>Save</button> </div> );}REST API Endpoints
Section titled “REST API Endpoints”WordPress uses register_rest_route(). EmDash plugins define API handlers.
add_action('rest_api_init', function() { register_rest_route('my-plugin/v1', '/calculate', [ 'methods' => 'POST', 'callback' => function($request) { $params = $request->get_json_params(); $result = $params['a'] + $params['b']; return new WP_REST_Response(['result' => $result]); }, 'permission_callback' => function() { return current_user_can('edit_posts'); } ]);});// Plugin API routesexport default { name: 'my-plugin',
api: { routes: [{ method: 'POST', path: '/calculate', handler: async (ctx, req) => { // Check permissions if (!ctx.user?.can('edit:content')) { return new Response('Forbidden', { status: 403 }); }
const { a, b } = await req.json(); return Response.json({ result: a + b }); } }] }};Migration Workflow
Section titled “Migration Workflow”-
Analyze the WordPress plugin
Identify what the plugin does:
- Custom post types and fields
- Admin pages
- Shortcodes or blocks
- Hooks used
- Database tables
- API endpoints
-
Map concepts to EmDash
Use the tables above to find equivalents. Note which features need different approaches.
-
Create the EmDash plugin structure
my-plugin/├── index.ts # Plugin entry point├── hooks.ts # Hook implementations├── api/ # API route handlers├── admin/ # Admin UI components└── components/ # Portable Text components -
Implement core functionality
Start with the data model (collections and fields), then add hooks, then admin UI.
-
Migrate data
If the WordPress plugin stored custom data:
- Export from WordPress (custom tables, post meta)
- Transform to EmDash format
- Import via API or direct database insert
-
Test thoroughly
- Verify hook behavior matches expectations
- Test admin pages render correctly
- Check API endpoints return correct data
Common Plugin Patterns
Section titled “Common Plugin Patterns”SEO Plugin
Section titled “SEO Plugin”WordPress SEO plugins add meta fields and generate tags.
export default { name: "seo",
hooks: { "content:beforeSave": async (ctx, entry) => { // Auto-generate meta description from excerpt if (!entry.data.seo?.description && entry.data.excerpt) { return { ...entry, data: { ...entry.data, seo: { ...entry.data.seo, description: entry.data.excerpt.slice(0, 160), }, }, }; } return entry; }, },
// Add SEO fields to all collections fields: { seo: { type: "object", fields: [ { slug: "title", type: "string" }, { slug: "description", type: "text" }, { slug: "keywords", type: "string" }, ], }, },};Form Plugin
Section titled “Form Plugin”WordPress form plugins store submissions.
export default { name: "forms",
// Create submissions collection on install install: async (ctx) => { await ctx.schema.createCollection({ slug: "form_submissions", label: "Form Submissions", fields: [ { slug: "formId", type: "string" }, { slug: "data", type: "json" }, { slug: "submittedAt", type: "datetime" }, ], }); },
api: { routes: [ { method: "POST", path: "/submit/:formId", handler: async (ctx, req) => { const formId = ctx.params.formId; const data = await req.json();
await ctx.content.create("form_submissions", { formId, data, submittedAt: new Date().toISOString(), });
return Response.json({ success: true }); }, }, ], },};E-commerce Plugin
Section titled “E-commerce Plugin”WordPress WooCommerce patterns translated to EmDash.
export default { name: "shop",
collections: [ { slug: "products", label: "Products", fields: [ { slug: "title", type: "string", required: true }, { slug: "price", type: "number", required: true }, { slug: "salePrice", type: "number" }, { slug: "sku", type: "string" }, { slug: "stock", type: "number", default: 0 }, { slug: "gallery", type: "media", multiple: true }, ], }, ],
hooks: { "content:beforeSave": async (ctx, entry) => { if (entry.collection !== "products") return entry;
// Generate SKU if not set if (!entry.data.sku) { const count = await ctx.content.count("products"); entry.data.sku = `PROD-${String(count + 1).padStart(5, "0")}`; }
return entry; }, },};Security Considerations
Section titled “Security Considerations”Available in Sandbox
Section titled “Available in Sandbox”ctx.storage— Database accessctx.kv— Key-value storectx.content— Content APIctx.media— Media APIfetch()— HTTP requests
Not Available
Section titled “Not Available”- File system access
- Shell commands
- Environment variables (use plugin settings)
- Global state between requests
Next Steps
Section titled “Next Steps”- WordPress Migration — Import your WordPress content
- Plugin Development — Full plugin development guide
- Hooks Reference — Complete hooks API