---
title: EmDash for WordPress Developers
description: A guide to EmDash's features and concepts for developers familiar with WordPress
---

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

EmDash brings familiar WordPress concepts—posts, pages, taxonomies, menus, widgets, and a media library—into a modern Astro stack. Your content management knowledge transfers directly.

## What Stays Familiar

The concepts you know from WordPress are first-class features in EmDash:

- **Collections** work like Custom Post Types—define your content structure, query it in templates
- **Taxonomies** work the same way—hierarchical (like categories) and flat (like tags)
- **Menus** with drag-and-drop ordering and nested items
- **Widget Areas** for sidebars and dynamic content regions
- **Media library** with upload, organization, and image management
- **Admin UI** that content editors can use without touching code

<Aside type="tip">
	You don't need to know React or any specific JavaScript framework. Astro components use HTML with
	simple template expressions—closer to PHP templates than to React.
</Aside>

## What's Different

The implementation changes, but the mental model stays the same:

<CardGrid>
	<Card title="TypeScript instead of PHP" icon="seti:typescript">
		Templates are Astro components. The syntax is cleaner, but the concept is the same: server code
		that outputs HTML.
	</Card>
	<Card title="Content APIs instead of WP_Query" icon="document">
		Query functions like `getEmDashCollection()` replace `WP_Query`. No SQL, just function calls.
	</Card>
	<Card title="File-based routing" icon="puzzle">
		Files in `src/pages/` become URLs. No rewrite rules or template hierarchy to memorize.
	</Card>
	<Card title="Components instead of template parts" icon="rocket">
		Import and use components. Same idea as `get_template_part()`, better organization.
	</Card>
</CardGrid>

## Quick Reference

| WordPress              | EmDash                             | Notes                             |
| ---------------------- | ------------------------------------ | --------------------------------- |
| Custom Post Types      | Collections                          | Define via admin UI or API        |
| `WP_Query`             | `getEmDashCollection()`            | Filters, limits, taxonomy queries |
| `get_post()`           | `getEmDashEntry()`                 | Returns entry or null             |
| Categories/Tags        | Taxonomies                           | Hierarchical support preserved    |
| `register_nav_menus()` | `getMenu()`                          | First-class menu support          |
| `register_sidebar()`   | `getWidgetArea()`                    | First-class widget areas          |
| `bloginfo('name')`     | `getSiteSetting("title")`            | Site settings API                 |
| `the_content()`        | `<PortableText />`                   | Structured content rendering      |
| Shortcodes             | Portable Text blocks                 | Custom components                 |
| `add_action/filter()`  | Plugin hooks                         | `content:beforeSave`, etc.        |
| `wp_options`           | `ctx.kv`                             | Key-value storage                 |
| Theme directory        | `src/` directory                     | Components, layouts, pages        |
| `functions.php`        | `astro.config.mjs` + EmDash config | Build and runtime config          |

## Content APIs

### Querying Collections

WordPress queries use `WP_Query` or helper functions. EmDash uses typed query functions.

<Tabs>
	<TabItem label="WordPress">
```php title="archive.php"
<?php
$posts = new WP_Query([
  'post_type' => 'post',
  'posts_per_page' => 10,
  'post_status' => 'publish',
  'category_name' => 'news',
]);

while ($posts->have_posts()) :
$posts->the_post();
?>

  <h2><?php the_title(); ?></h2>
  <?php the_excerpt(); ?>
<?php endwhile; ?>
```
	</TabItem>
	<TabItem label="EmDash">
```astro title="src/pages/posts/index.astro"
---
import { getEmDashCollection } from "emdash";

const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
limit: 10,
where: { category: "news" },
});

---

{posts.map((post) => (

  <article>
    <h2>{post.data.title}</h2>
    <p>{post.data.excerpt}</p>
  </article>
))}
```
	</TabItem>
</Tabs>

### Getting a Single Entry

<Tabs>
	<TabItem label="WordPress">
```php title="single.php"
<?php
$post = get_post($id);
?>
<article>
  <h1><?php echo $post->post_title; ?></h1>
  <?php echo apply_filters('the_content', $post->post_content); ?>
</article>
```
	</TabItem>
	<TabItem label="EmDash">
```astro title="src/pages/posts/[slug].astro"
---
import { getEmDashEntry } from "emdash";
import { PortableText } from "emdash/ui";

const { slug } = Astro.params;
const { entry: post } = await getEmDashEntry("posts", slug);

## if (!post) return Astro.redirect("/404");

<article>
  <h1>{post.data.title}</h1>
  <PortableText value={post.data.content} />
</article>
```
	</TabItem>
</Tabs>

## Template Hierarchy

WordPress uses a template hierarchy to select which file renders a page. Astro uses explicit file-based routing.

| WordPress Template          | EmDash Equivalent                 |
| --------------------------- | ----------------------------------- |
| `index.php`                 | `src/pages/index.astro`             |
| `single.php`                | `src/pages/posts/[slug].astro`      |
| `single-{type}.php`         | `src/pages/{type}/[slug].astro`     |
| `page.php`                  | `src/pages/pages/[slug].astro`      |
| `archive.php`               | `src/pages/posts/index.astro`       |
| `archive-{type}.php`        | `src/pages/{type}/index.astro`      |
| `category.php`              | `src/pages/categories/[slug].astro` |
| `tag.php`                   | `src/pages/tags/[slug].astro`       |
| `search.php`                | `src/pages/search.astro`            |
| `404.php`                   | `src/pages/404.astro`               |
| `header.php` / `footer.php` | `src/layouts/Base.astro`            |
| `sidebar.php`               | `src/components/Sidebar.astro`      |

<Aside type="tip">
	Astro's routing is more explicit than WordPress's hierarchy. Each route is a file. Dynamic
	segments use `[param]` syntax.
</Aside>

## Template Parts → Components

WordPress template parts become Astro components:

<Tabs>
	<TabItem label="WordPress">
```php title="functions.php / template"
// In template:
get_template_part('template-parts/content', 'post');

// template-parts/content-post.php:

<article class="post">
  <h2><?php the_title(); ?></h2>
  <?php the_excerpt(); ?>
</article>
```
	</TabItem>
	<TabItem label="EmDash">
```astro title="src/components/PostCard.astro"
---
const { post } = Astro.props;
---

<article class="post">
	<h2>{post.data.title}</h2>
	<p>{post.data.excerpt}</p>
</article>
```

```astro title="src/pages/index.astro"
---
import PostCard from "../components/PostCard.astro";
import { getEmDashCollection } from "emdash";

const { entries: posts } = await getEmDashCollection("posts");
---

{posts.map((post) => <PostCard {post} />)}
```

    </TabItem>

</Tabs>

## Menus

EmDash has first-class menu support with automatic URL resolution:

<Tabs>
	<TabItem label="WordPress">
```php title="header.php"
<?php
wp_nav_menu([
  'theme_location' => 'primary',
  'container' => 'nav',
]);
?>
```
	</TabItem>
	<TabItem label="EmDash">
```astro title="src/components/Header.astro"
---
import { getMenu } from "emdash";

## const menu = await getMenu("primary");

<nav>
  <ul>
    {menu?.items.map((item) => (
      <li>
        <a href={item.url}>{item.label}</a>
      </li>
    ))}
  </ul>
</nav>
```
	</TabItem>
</Tabs>

Menus are created via the admin UI, seed files, or WordPress import.

## Widget Areas

Widget areas work like sidebars in WordPress:

<Tabs>
	<TabItem label="WordPress">
```php title="sidebar.php"
<?php if (is_active_sidebar('sidebar-1')) : ?>
  <aside>
    <?php dynamic_sidebar('sidebar-1'); ?>
  </aside>
<?php endif; ?>
```
	</TabItem>
	<TabItem label="EmDash">
```astro title="src/components/Sidebar.astro"
---
import { getWidgetArea } from "emdash";
import { PortableText } from "emdash/ui";

## const sidebar = await getWidgetArea("sidebar");

{sidebar && (

  <aside>
    {sidebar.widgets.map((widget) => {
      if (widget.type === "content") {
        return <PortableText value={widget.content} />;
      }
      // Handle other widget types
    })}
  </aside>
)}
```
	</TabItem>
</Tabs>

## Site Settings

Site options and customizer settings map to `getSiteSetting()`:

| WordPress                   | EmDash                       |
| --------------------------- | ------------------------------ |
| `bloginfo('name')`          | `getSiteSetting("title")`      |
| `bloginfo('description')`   | `getSiteSetting("tagline")`    |
| `get_custom_logo()`         | `getSiteSetting("logo")`       |
| `get_option('date_format')` | `getSiteSetting("dateFormat")` |
| `home_url()`                | `Astro.site`                   |

```ts
import { getSiteSetting } from "emdash";

const title = await getSiteSetting("title");
const logo = await getSiteSetting("logo"); // Returns { mediaId, alt, url }
```

## Taxonomies

Taxonomies work the same conceptually—hierarchical (like categories) or flat (like tags):

```ts
import { getTaxonomyTerms, getEntryTerms, getTerm } from "emdash";

// Get all categories
const categories = await getTaxonomyTerms("categories");

// Get a specific term
const news = await getTerm("categories", "news");

// Get terms for a post
const postCategories = await getEntryTerms("posts", postId, "categories");
```

## Hooks → Plugin System

WordPress hooks (`add_action`, `add_filter`) become EmDash plugin hooks:

| WordPress Hook  | EmDash Hook           | Purpose                      |
| --------------- | ----------------------- | ---------------------------- |
| `save_post`     | `content:beforeSave`    | Modify content before saving |
| `the_content`   | PortableText components | Transform rendered content   |
| `pre_get_posts` | Query options           | Filter queries               |
| `wp_head`       | Layout `<head>`         | Add head content             |
| `wp_footer`     | Layout before `</body>` | Add footer content           |

## What's Better in EmDash

<CardGrid>
	<Card title="Type Safety" icon="seti:typescript">
		TypeScript throughout. Collections, queries, and components are fully typed. No more guessing
		field names or return types.
	</Card>
	<Card title="Performance" icon="rocket">
		No PHP overhead. Static generation by default. Server rendering when needed. Edge deployment
		ready.
	</Card>
	<Card title="Modern DX" icon="laptop">
		Hot module replacement. Component-based architecture. Modern tooling (Vite, TypeScript, ESLint).
	</Card>
	<Card title="Git-based Deployments" icon="github">
		Code and templates in git. Content in the database. No FTP, no file permissions, no hacked
		sites.
	</Card>
</CardGrid>

### Preview Links

EmDash generates secure preview URLs with HMAC-signed tokens. Content editors can preview drafts without logging into production—share a link, not credentials.

### No Plugin Conflicts

WordPress plugin conflicts disappear. EmDash plugins run in isolated contexts with explicit APIs. No global state pollution.

## Content Editor Experience

Content editors use the EmDash admin panel, similar to wp-admin:

- **Dashboard** with recent activity
- **Collection listings** with search, filter, and bulk actions
- **Rich editor** for content (Portable Text, not Gutenberg)
- **Media library** with drag-and-drop upload
- **Menu builder** with drag-and-drop ordering
- **Widget area editor** for sidebar content

The editing experience is familiar. The technology underneath is modern.

## Migration Path

EmDash imports WordPress content directly:

1. Export from WordPress (Tools → Export)
2. Upload the `.xml` file in EmDash's admin
3. Map post types to collections
4. Import content and media

Posts, pages, taxonomies, menus, and media transfer. Gutenberg blocks convert to Portable Text. Custom fields are analyzed and mapped.

See the [WordPress Migration Guide](/migration/from-wordpress/) for complete instructions.

## Next Steps

- **[Getting Started](/getting-started/)** — Set up your first EmDash site
- **[Querying Content](/guides/querying-content/)** — Deep dive into content APIs
- **[Taxonomies](/guides/taxonomies/)** — Categories, tags, and custom taxonomies
- **[Menus](/guides/menus/)** — Navigation menus
- **[Migrate from WordPress](/migration/from-wordpress/)** — Import existing content
