Porting WordPress Themes

On this page

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

  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.

Phase 1: Design Extraction

Locate CSS and Design Tokens

FilePurpose
style.cssMain stylesheet with theme header
assets/css/Additional stylesheets
theme.jsonBlock themes (WP 5.9+) - structured tokens

Extract Design 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 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

Template Hierarchy Mapping

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

Template Tags Mapping

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")

Converting The Loop

WordPress

<?php while (have_posts()) : the_post(); ?>
  <article>
    <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
    <?php the_excerpt(); ?>
  </article>
<?php endwhile; ?>

EmDash

---
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

WordPress

<?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(); ?>

EmDash

---
import { getEmDashCollection, getEntryTerms } from "emdash";
import { PortableText } from "emdash/ui";
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

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

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)

Identify widget areas in the theme and render them:

---
import { getWidgetArea, getMenu } from "emdash";
import { PortableText } from "emdash/ui";
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

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"

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

WordPress CustomizerEmDash Setting
Site Titletitle
Taglinetagline
Site Iconfavicon
Custom Logologo
Posts per pagepostsPerPage

Shortcodes to Portable Text

WordPress shortcodes become Portable Text custom blocks:

WordPress

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

EmDash

---
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

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

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

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)

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

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