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
What’s Different
The implementation changes, but the mental model stays the same:
TypeScript instead of PHP
Templates are Astro components. The syntax is cleaner, but the concept is the same: server code that outputs HTML.
Content APIs instead of WP_Query
Query functions like getEmDashCollection() replace WP_Query. No SQL, just function calls.
File-based routing
Files in src/pages/ become URLs. No rewrite rules or template hierarchy to memorize.
Components instead of template parts
Import and use components. Same idea as get_template_part(), better organization.
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.
WordPress
<?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; ?> EmDash
---
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>
))} Getting a Single Entry
WordPress
<?php
$post = get_post($id);
?>
<article>
<h1><?php echo $post->post_title; ?></h1>
<?php echo apply_filters('the_content', $post->post_content); ?>
</article> EmDash
---
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> 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 |
Template Parts → Components
WordPress template parts become Astro components:
WordPress
// 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> EmDash
---
const { post } = Astro.props;
---
<article class="post">
<h2>{post.data.title}</h2>
<p>{post.data.excerpt}</p>
</article>---
import PostCard from "../components/PostCard.astro";
import { getEmDashCollection } from "emdash";
const { entries: posts } = await getEmDashCollection("posts");
---
{posts.map((post) => <PostCard {post} />)} Menus
EmDash has first-class menu support with automatic URL resolution:
WordPress
<?php
wp_nav_menu([
'theme_location' => 'primary',
'container' => 'nav',
]);
?> EmDash
---
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> Menus are created via the admin UI, seed files, or WordPress import.
Widget Areas
Widget areas work like sidebars in WordPress:
WordPress
<?php if (is_active_sidebar('sidebar-1')) : ?>
<aside>
<?php dynamic_sidebar('sidebar-1'); ?>
</aside>
<?php endif; ?> EmDash
---
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>
)} 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 |
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):
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
Type Safety
TypeScript throughout. Collections, queries, and components are fully typed. No more guessing field names or return types.
Performance
No PHP overhead. Static generation by default. Server rendering when needed. Edge deployment ready.
Modern DX
Hot module replacement. Component-based architecture. Modern tooling (Vite, TypeScript, ESLint).
Git-based Deployments
Code and templates in git. Content in the database. No FTP, no file permissions, no hacked sites.
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:
- Export from WordPress (Tools → Export)
- Upload the
.xmlfile in EmDash’s admin - Map post types to collections
- 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 for complete instructions.
Next Steps
- Getting Started — Set up your first EmDash site
- Querying Content — Deep dive into content APIs
- Taxonomies — Categories, tags, and custom taxonomies
- Menus — Navigation menus
- Migrate from WordPress — Import existing content