TypeScript instead of PHP
Templates are Astro components. The syntax is cleaner, but the concept is the same: server code that outputs HTML.
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.
The concepts you know from WordPress are first-class features in EmDash:
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.
| 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 |
WordPress queries use WP_Query or helper functions. EmDash uses typed query functions.
<?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; ?>---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>))}<?php$post = get_post($id);?><article> <h1><?php echo $post->post_title; ?></h1> <?php echo apply_filters('the_content', $post->post_content); ?></article>---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>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 |
WordPress template parts become Astro components:
// 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>---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} />)}EmDash has first-class menu support with automatic URL resolution:
<?phpwp_nav_menu([ 'theme_location' => 'primary', 'container' => 'nav',]);?>---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 work like sidebars in WordPress:
<?php if (is_active_sidebar('sidebar-1')) : ?> <aside> <?php dynamic_sidebar('sidebar-1'); ?> </aside><?php endif; ?>---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 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 work the same conceptually—hierarchical (like categories) or flat (like tags):
import { getTaxonomyTerms, getEntryTerms, getTerm } from "emdash";
// Get all categoriesconst categories = await getTaxonomyTerms("categories");
// Get a specific termconst news = await getTerm("categories", "news");
// Get terms for a postconst postCategories = await getEntryTerms("posts", postId, "categories");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 |
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.
EmDash generates secure preview URLs with HMAC-signed tokens. Content editors can preview drafts without logging into production—share a link, not credentials.
WordPress plugin conflicts disappear. EmDash plugins run in isolated contexts with explicit APIs. No global state pollution.
Content editors use the EmDash admin panel, similar to wp-admin:
The editing experience is familiar. The technology underneath is modern.
EmDash imports WordPress content directly:
.xml file in EmDash’s adminPosts, 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.