---
title: Porting WordPress Themes
description: Convert WordPress themes to EmDash themes using a structured approach
---

import { Aside, Steps, Tabs, TabItem } from "@astrojs/starlight/components";

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

<Aside type="tip" title="AI-Assisted Porting">
	AI coding agents excel at mechanical conversions like template porting. Feed the agent your
	WordPress theme files along with the concept mapping tables in this guide, and it can generate a
	solid first draft of Astro components. Review and refine the output—the agent handles the tedious
	parts while you focus on quality.
</Aside>

## Three-Phase Approach

<Steps>

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.

</Steps>

## Phase 1: Design Extraction

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

| 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

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

```astro title="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>
```

## Phase 2: Template Conversion

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

| 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

<Tabs>
<TabItem label="WordPress">
```php title="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; ?>
```
</TabItem>
<TabItem label="EmDash">
```astro title="src/pages/posts/index.astro"
---
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>
```
</TabItem>
</Tabs>

### Converting Single Templates

<Tabs>
<TabItem label="WordPress">
```php title="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(); ?>
```
</TabItem>
<TabItem label="EmDash">
```astro title="src/pages/posts/[slug].astro"
---
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>
```
</TabItem>
</Tabs>

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

### Navigation Menus

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

```astro title="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>
)}
```

### Widget Areas (Sidebars)

Identify widget areas in the theme and render them:

```astro title="src/components/Sidebar.astro"
---
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 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

Query taxonomies registered in the theme:

```astro title="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>
```

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

WordPress shortcodes become Portable Text custom blocks:

<Tabs>
<TabItem label="WordPress">
```php title="functions.php"
add_shortcode('gallery', function($atts) {
  $ids = explode(',', $atts['ids']);
  return '<div class="gallery">...</div>';
});
```
</TabItem>
<TabItem label="EmDash">
```astro title="src/components/blocks/Gallery.astro"
---
const { images } = Astro.props;
---

<div class="gallery">
	{images.map((img) => (
		<img src={img.url} alt={img.alt || ""} loading="lazy" />
	))}
</div>
```

Register with PortableText:

```astro
<PortableText value={content} components={{ gallery: Gallery }} />
```

</TabItem>
</Tabs>

## Seed File Structure

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

```json title=".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](/themes/seed-files/) 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

- **[Creating Themes](/themes/creating-themes/)** — Build distributable EmDash themes
- **[Seed File Format](/themes/seed-files/)** — Complete seed file specification
- **[Migrate from WordPress](/migration/from-wordpress/)** — Import WordPress content
