---
title: Porting WordPress Plugins
description: Migrate WordPress plugin functionality to EmDash plugins.
---

import { Aside, Card, CardGrid, Steps, Tabs, TabItem } from "@astrojs/starlight/components";

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

| 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

### Actions and Filters → Hooks

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

<Tabs>
<TabItem label="WordPress">
```php
// 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>';
});

````
</TabItem>
<TabItem label="EmDash">
```typescript
// EmDash hook
export 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>';
  }
};
````

</TabItem>
</Tabs>

### 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

WordPress uses `$wpdb` for direct database queries. EmDash provides `ctx.storage` for structured data access.

<Tabs>
<TabItem label="WordPress">
```php
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']
);

````
</TabItem>
<TabItem label="EmDash">
```typescript
// Using ctx.storage (D1/SQLite)
const db = ctx.storage;

// Insert
await db.prepare(
  'INSERT INTO custom_table (name, value) VALUES (?, ?)'
).bind('Example', 42).run();

// Query
const results = await db.prepare(
  'SELECT * FROM custom_table WHERE value > ?'
).bind(10).all();

// Update
await db.prepare(
  'UPDATE custom_table SET value = ? WHERE name = ?'
).bind(50, 'Example').run();
````

</TabItem>
</Tabs>

<Aside>
	EmDash runs on Cloudflare Workers with D1 (SQLite). Use prepared statements with parameter
	binding for security.
</Aside>

### Options Storage

WordPress uses `get_option()` / `update_option()`. EmDash uses `ctx.kv` for key-value storage.

<Tabs>
<TabItem label="WordPress">
```php
// 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');

````
</TabItem>
<TabItem label="EmDash">
```typescript
// Get value
const apiKey = await ctx.kv.get('my_plugin_api_key') ?? '';

// Set value
await ctx.kv.put('my_plugin_api_key', 'abc123');

// Delete value
await ctx.kv.delete('my_plugin_api_key');
````

</TabItem>
</Tabs>

### Custom Post Types → Collections

WordPress registers post types with `register_post_type()`. EmDash uses collections defined in the admin UI or via API.

<Tabs>
<TabItem label="WordPress">
```php
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
]);

````
</TabItem>
<TabItem label="EmDash">
```typescript
// Create via API
await 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' }
    ]
  })
});
````

</TabItem>
</Tabs>

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

### Shortcodes → Portable Text Blocks

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

<Tabs>
<TabItem label="WordPress">
```php
// 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"]

````
</TabItem>
<TabItem label="EmDash">
```typescript
// Define Portable Text block schema
const productCardBlock = {
  name: 'productCard',
  type: 'object',
  fields: [
    { name: 'productId', type: 'reference', to: 'products' },
    { name: 'showPrice', type: 'boolean', default: true }
  ]
};

// Render component (Astro)
---
// src/components/ProductCard.astro
import { 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>
````

```typescript
// Register with Portable Text renderer
import ProductCard from "./components/ProductCard.astro";

const components = {
	types: {
		productCard: ProductCard,
	},
};

// Usage: <PortableText value={content} components={components} />
```

</TabItem>
</Tabs>

### Admin Pages

WordPress uses `add_menu_page()` for admin screens. EmDash plugins define admin routes.

<Tabs>
<TabItem label="WordPress">
```php
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
}

````
</TabItem>
<TabItem label="EmDash">
```typescript
// Plugin definition
export default {
  name: 'my-plugin',

  admin: {
    // Menu entry
    menu: {
      label: 'My Plugin',
      icon: 'settings'
    },

    // Admin page component
    pages: [{
      path: '/settings',
      component: () => import('./admin/Settings')
    }]
  }
};
````

```tsx
// 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>
	);
}
```

</TabItem>
</Tabs>

### REST API Endpoints

WordPress uses `register_rest_route()`. EmDash plugins define API handlers.

<Tabs>
<TabItem label="WordPress">
```php
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');
        }
    ]);
});
```
</TabItem>
<TabItem label="EmDash">
```typescript
// Plugin API routes
export 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 });
      }
    }]
  }
};
```
</TabItem>
</Tabs>

## Migration Workflow

<Steps>

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

</Steps>

## Common Plugin Patterns

### SEO Plugin

WordPress SEO plugins add meta fields and generate tags.

```typescript
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

WordPress form plugins store submissions.

```typescript
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

WordPress WooCommerce patterns translated to EmDash.

```typescript
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

<Aside type="caution">
	EmDash plugins run in a sandboxed environment with limited capabilities. Direct file system
	access and shell commands are not available.
</Aside>

### Available in Sandbox

- `ctx.storage` — Database access
- `ctx.kv` — Key-value store
- `ctx.content` — Content API
- `ctx.media` — Media API
- `fetch()` — HTTP requests

### Not Available

- File system access
- Shell commands
- Environment variables (use plugin settings)
- Global state between requests

## Next Steps

- **[WordPress Migration](/migration/from-wordpress/)** — Import your WordPress content
- **[Plugin Development](/plugins/development/)** — Full plugin development guide
- **[Hooks Reference](/reference/hooks/)** — Complete hooks API
