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.
Three-Phase Approach
Section titled “Three-Phase Approach”-
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.
-
Template Conversion
Convert PHP templates to Astro components. Map the WordPress template hierarchy to Astro routes and transform template tags to EmDash API calls.
-
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.
Phase 1: Design Extraction
Section titled “Phase 1: Design Extraction”Locate CSS and Design Tokens
Section titled “Locate CSS and Design Tokens”| File | Purpose |
|---|---|
style.css | Main stylesheet with theme header |
assets/css/ | Additional stylesheets |
theme.json | Block themes (WP 5.9+) - structured tokens |
Extract Design Tokens
Section titled “Extract Design Tokens”| WordPress Pattern | EmDash 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 Base Layout
Section titled “Create Base Layout”Create src/layouts/Base.astro with extracted CSS variables, header/footer structure, font loading, and responsive breakpoints:
---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>Phase 2: Template Conversion
Section titled “Phase 2: Template Conversion”Template Hierarchy Mapping
Section titled “Template Hierarchy Mapping”| WordPress Template | Astro Route |
|---|---|
index.php | src/pages/index.astro |
single.php | src/pages/posts/[slug].astro |
single-{post_type}.php | src/pages/{type}/[slug].astro |
page.php | src/pages/[...slug].astro |
archive.php | src/pages/posts/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 | Part of src/layouts/Base.astro |
sidebar.php | src/components/Sidebar.astro |
Template Tags Mapping
Section titled “Template Tags Mapping”| WordPress Function | EmDash 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") |
Converting The Loop
Section titled “Converting The Loop”<?php while (have_posts()) : the_post(); ?> <article> <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2> <?php the_excerpt(); ?> </article><?php endwhile; ?>---import { getEmDashCollection } from "emdash";import Base from "../../layouts/Base.astro";
const { entries: posts } = await getEmDashCollection("posts", {where: { status: "published" },orderBy: { publishedAt: "desc" },});
---
<Base title="Blog"> {posts.map((post) => ( <article> <h2><a href={`/posts/${post.slug}`}>{post.data.title}</a></h2> <p>{post.data.excerpt}</p> </article> ))}</Base>Converting Single Templates
Section titled “Converting Single Templates”<?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(); ?>---import { getEmDashCollection, getEntryTerms } from "emdash";import { PortableText } from "emdash/astro";import Base from "../../layouts/Base.astro";
export async function getStaticPaths() { const { entries: posts } = await getEmDashCollection("posts"); return posts.map((post) => ({ params: { slug: post.slug }, props: { post }, }));}
const { post } = Astro.props;const categories = await getEntryTerms("posts", post.id, "categories");
---
<Base title={post.data.title}> <article> <h1>{post.data.title}</h1> <PortableText value={post.data.content} /> <div class="post-meta"> Posted in: {categories.map((cat) => ( <a href={`/categories/${cat.slug}`}>{cat.label}</a> ))} </div> </article></Base>Converting Template Parts
Section titled “Converting Template Parts”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.
Phase 3: Dynamic Features
Section titled “Phase 3: Dynamic Features”Navigation Menus
Section titled “Navigation Menus”Identify menus in functions.php and create corresponding EmDash menus:
---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>)}Widget Areas (Sidebars)
Section titled “Widget Areas (Sidebars)”Identify widget areas in the theme and render them:
---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>)}Widget Type Mapping
Section titled “Widget Type Mapping”| WordPress Widget | EmDash Widget Type |
|---|---|
| Text/Custom HTML | type: "content" |
| Custom Menu | type: "menu" |
| Recent Posts | component: "core:recent-posts" |
| Categories | component: "core:categories" |
| Tag Cloud | component: "core:tag-cloud" |
| Search | component: "core:search" |
Taxonomies
Section titled “Taxonomies”Query taxonomies registered in the theme:
---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>Site Settings Mapping
Section titled “Site Settings Mapping”| WordPress Customizer | EmDash Setting |
|---|---|
| Site Title | title |
| Tagline | tagline |
| Site Icon | favicon |
| Custom Logo | logo |
| Posts per page | postsPerPage |
Shortcodes to Portable Text
Section titled “Shortcodes to Portable Text”WordPress shortcodes become Portable Text custom blocks:
add_shortcode('gallery', function($atts) { $ids = explode(',', $atts['ids']); return '<div class="gallery">...</div>';});---const { images } = Astro.props;---
<div class="gallery"> {images.map((img) => ( <img src={img.url} alt={img.alt || ""} loading="lazy" /> ))}</div>Register with PortableText:
<PortableText value={content} components={{ gallery: Gallery }} />Seed File Structure
Section titled “Seed File Structure”Capture the complete content model in a seed file. Include settings, taxonomies, menus, and widget areas:
{ "$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.
Porting Checklist
Section titled “Porting Checklist”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.
Edge Cases
Section titled “Edge Cases”Child Themes
Section titled “Child Themes”If the theme has a parent (check style.css for Template:), analyze the parent theme first, then apply child theme overrides.
Block Themes (FSE)
Section titled “Block Themes (FSE)”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.
Page Builders
Section titled “Page Builders”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.
Next Steps
Section titled “Next Steps”- Creating Themes — Build distributable EmDash themes
- Seed File Format — Complete seed file specification
- Migrate from WordPress — Import WordPress content