Skip to content

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.

WordPressEmDash
PHP files in wp-content/plugins/TypeScript modules registered in config
add_action() / add_filter()Hook functions
Admin menu pagesAdmin panel routes
REST API endpointsAPI route handlers
Database via $wpdbStorage via ctx.storage
Options via wp_optionsKey-value via ctx.kv
Post metaCollection fields
ShortcodesPortable Text custom blocks
Gutenberg blocksPortable Text custom blocks

WordPress uses add_action() and add_filter() for extensibility. EmDash uses typed hook functions.

// WordPress action
add_action('save_post', function($post_id, $post) {
if ($post->post_type !== 'product') return;
update_post_meta($post_id, 'last_updated', time());
}, 10, 2);
// WordPress filter
add_filter('the_content', function($content) {
return $content . '<p>Read more articles</p>';
});
HookEquivalent WordPress HookPurpose
content:beforeSavewp_insert_post_dataModify content before save
content:afterSavesave_postReact after content saved
content:beforeDeletebefore_delete_postValidate before deletion
content:afterRenderthe_contentTransform rendered output
media:beforeUploadwp_handle_upload_prefilterValidate/transform uploads
media:afterUploadadd_attachmentReact after upload
admin:initadmin_initAdmin panel initialization
api:requestrest_pre_dispatchIntercept API requests

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']
);

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 option
update_option('my_plugin_api_key', 'abc123');
// Delete option
delete_option('my_plugin_api_key');

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
]);

Collections are typically created through the admin UI at Content Types → New Content Type.

WordPress shortcodes embed dynamic content. EmDash uses custom Portable Text blocks with React/Astro components.

// Register shortcode
add_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"]

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
}

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');
}
]);
});
  1. 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
  2. Map concepts to EmDash

    Use the tables above to find equivalents. Note which features need different approaches.

  3. 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
  4. Implement core functionality

    Start with the data model (collections and fields), then add hooks, then admin UI.

  5. 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
  6. Test thoroughly

    • Verify hook behavior matches expectations
    • Test admin pages render correctly
    • Check API endpoints return correct data

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" },
],
},
},
};

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 });
},
},
],
},
};

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;
},
},
};
  • ctx.storage — Database access
  • ctx.kv — Key-value store
  • ctx.content — Content API
  • ctx.media — Media API
  • fetch() — HTTP requests
  • File system access
  • Shell commands
  • Environment variables (use plugin settings)
  • Global state between requests