Skip to content

Porting WordPress Themes

WordPress themes can be systematically converted to EmDash. The visual design, content structure, and dynamic features all transfer using a three-phase approach.

  1. Design Extraction

    Extract CSS variables, fonts, colors, and layout patterns from the WordPress theme. Analyze the live site to capture computed styles and responsive breakpoints.

  2. Template Conversion

    Convert PHP templates to Astro components. Map the WordPress template hierarchy to Astro routes and transform template tags to EmDash API calls.

  3. Dynamic Features

    Port navigation menus, widget areas, taxonomies, and site settings to their EmDash equivalents. Create a seed file to capture the complete content model.

FilePurpose
style.cssMain stylesheet with theme header
assets/css/Additional stylesheets
theme.jsonBlock themes (WP 5.9+) - structured tokens
WordPress PatternEmDash Variable
Body font family--font-body
Heading font--font-heading
Primary color--color-primary
Background--color-base
Text color--color-contrast
Content width--content-width

Create src/layouts/Base.astro with extracted CSS variables, header/footer structure, font loading, and responsive breakpoints:

src/layouts/Base.astro
---
import { getSiteSettings, getMenu } from "emdash";
import "../styles/global.css";
const { title, description } = Astro.props;
const settings = await getSiteSettings();
const primaryMenu = await getMenu("primary");
const pageTitle = title ? `${title} | ${settings.title}` : settings.title;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{pageTitle}</title>
</head>
<body>
<header>
{settings.logo ? (
<img src={settings.logo.url} alt={settings.title} />
) : (
<span>{settings.title}</span>
)}
<nav>
{primaryMenu?.items.map((item) => (
<a href={item.url}>{item.label}</a>
))}
</nav>
</header>
<main><slot /></main>
</body>
</html>
WordPress TemplateAstro Route
index.phpsrc/pages/index.astro
single.phpsrc/pages/posts/[slug].astro
single-{post_type}.phpsrc/pages/{type}/[slug].astro
page.phpsrc/pages/[...slug].astro
archive.phpsrc/pages/posts/index.astro
category.phpsrc/pages/categories/[slug].astro
tag.phpsrc/pages/tags/[slug].astro
search.phpsrc/pages/search.astro
404.phpsrc/pages/404.astro
header.php / footer.phpPart of src/layouts/Base.astro
sidebar.phpsrc/components/Sidebar.astro
WordPress FunctionEmDash Equivalent
have_posts() / the_post()getEmDashCollection()
get_post()getEmDashEntry()
the_title()post.data.title
the_content()<PortableText value={post.data.content} />
the_excerpt()post.data.excerpt
the_permalink()/posts/${post.slug}
the_post_thumbnail()post.data.featured_image
get_the_date()post.data.publishedAt
get_the_category()getEntryTerms(coll, id, "categories")
get_the_tags()getEntryTerms(coll, id, "tags")
archive.php
<?php while (have_posts()) : the_post(); ?>
<article>
<h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
<?php the_excerpt(); ?>
</article>
<?php endwhile; ?>
single.php
<?php get_header(); ?>
<article>
<h1><?php the_title(); ?></h1>
<?php the_content(); ?>
<div class="post-meta">
Posted in: <?php the_category(', '); ?>
</div>
</article>
<?php get_footer(); ?>

WordPress get_template_part() calls become Astro component imports. The template-parts/content-post.php partial becomes a PostCard.astro component that you import and render in a loop.

Identify menus in functions.php and create corresponding EmDash menus:

src/components/PrimaryNav.astro
---
import { getMenu } from "emdash";
const menu = await getMenu("primary");
---
{menu && (
<nav class="primary-nav">
<ul>
{menu.items.map((item) => (
<li>
<a href={item.url} aria-current={Astro.url.pathname === item.url ? "page" : undefined}>
{item.label}
</a>
{item.children.length > 0 && (
<ul class="submenu">
{item.children.map((child) => (
<li><a href={child.url}>{child.label}</a></li>
))}
</ul>
)}
</li>
))}
</ul>
</nav>
)}

Identify widget areas in the theme and render them:

src/components/Sidebar.astro
---
import { getWidgetArea, getMenu } from "emdash";
import { PortableText } from "emdash/astro";
import RecentPosts from "./widgets/RecentPosts.astro";
const sidebar = await getWidgetArea("sidebar");
const widgetComponents = { "core:recent-posts": RecentPosts };
---
{sidebar && sidebar.widgets.length > 0 && (
<aside class="sidebar">
{sidebar.widgets.map(async (widget) => (
<div class="widget">
{widget.title && <h3>{widget.title}</h3>}
{widget.type === "content" && <PortableText value={widget.content} />}
{widget.type === "menu" && (
<nav>
{await getMenu(widget.menuName).then((m) =>
m?.items.map((item) => <a href={item.url}>{item.label}</a>)
)}
</nav>
)}
{widget.type === "component" && widgetComponents[widget.componentId] && (
<Fragment>
{(() => {
const Component = widgetComponents[widget.componentId];
return <Component {...widget.componentProps} />;
})()}
</Fragment>
)}
</div>
))}
</aside>
)}
WordPress WidgetEmDash Widget Type
Text/Custom HTMLtype: "content"
Custom Menutype: "menu"
Recent Postscomponent: "core:recent-posts"
Categoriescomponent: "core:categories"
Tag Cloudcomponent: "core:tag-cloud"
Searchcomponent: "core:search"

Query taxonomies registered in the theme:

src/pages/genres/[slug].astro
---
import { getTaxonomyTerms, getEntriesByTerm } from "emdash";
import Base from "../../layouts/Base.astro";
export async function getStaticPaths() {
const genres = await getTaxonomyTerms("genre");
return genres.map((genre) => ({
params: { slug: genre.slug },
props: { genre },
}));
}
const { genre } = Astro.props;
const books = await getEntriesByTerm("books", "genre", genre.slug);
---
<Base title={genre.label}>
<h1>{genre.label}</h1>
{books.map((book) => (
<article>
<h2><a href={`/books/${book.slug}`}>{book.data.title}</a></h2>
</article>
))}
</Base>
WordPress CustomizerEmDash Setting
Site Titletitle
Taglinetagline
Site Iconfavicon
Custom Logologo
Posts per pagepostsPerPage

WordPress shortcodes become Portable Text custom blocks:

functions.php
add_shortcode('gallery', function($atts) {
$ids = explode(',', $atts['ids']);
return '<div class="gallery">...</div>';
});

Capture the complete content model in a seed file. Include settings, taxonomies, menus, and widget areas:

.emdash/seed.json
{
"$schema": "https://emdashcms.com/seed.schema.json",
"version": "1",
"meta": { "name": "Ported Theme" },
"settings": { "title": "My Site", "tagline": "Welcome", "postsPerPage": 10 },
"taxonomies": [
{
"name": "category",
"label": "Categories",
"hierarchical": true,
"collections": ["posts"]
}
],
"menus": [
{
"name": "primary",
"label": "Primary Navigation",
"items": [
{ "type": "custom", "label": "Home", "url": "/" },
{ "type": "custom", "label": "Blog", "url": "/posts" }
]
}
],
"widgetAreas": [
{
"name": "sidebar",
"label": "Main Sidebar",
"widgets": [
{
"type": "component",
"componentId": "core:recent-posts",
"props": { "count": 5 }
}
]
}
]
}

See Seed File Format for the complete specification.

Phase 1 (Design): CSS variables extracted, fonts loading, color scheme matches, responsive breakpoints work.

Phase 2 (Templates): Homepage, single posts, archives, and 404 page all render correctly.

Phase 3 (Dynamic): Site settings configured, menus functional, taxonomies queryable, widget areas rendering, seed file complete.

If the theme has a parent (check style.css for Template:), analyze the parent theme first, then apply child theme overrides.

WordPress 5.9+ block themes use theme.json for design tokens and templates/*.html for block markup. Convert block markup to Astro components and extract tokens from theme.json.

Content built with Elementor, Divi, or similar is stored in post meta, not theme files. This content imports via WXR, not theme porting. Focus theme porting on the shell—page builder content renders through Portable Text after import.