WordPress テーマの移植

このページ

WordPress テーマは体系的に EmDash に変換できます。ビジュアルデザイン、コンテンツ構造、動的機能はすべて 3 フェーズアプローチで移行できます。

3 フェーズアプローチ

  1. デザイン抽出

    WordPress テーマから CSS 変数、フォント、カラー、レイアウトパターンを抽出します。ライブサイトを分析して計算済みスタイルとレスポンシブブレークポイントをキャプチャします。

  2. テンプレート変換

    PHP テンプレートを Astro コンポーネントに変換します。WordPress テンプレート階層を Astro ルートにマッピングし、テンプレートタグを EmDash API 呼び出しに変換します。

  3. 動的機能

    ナビゲーションメニュー、ウィジェットエリア、タクソノミー、サイト設定を EmDash の対応機能に移植します。完全なコンテンツモデルをキャプチャするシードファイルを作成します。

フェーズ 1: デザイン抽出

CSS とデザイントークンの確認

ファイル目的
style.cssテーマヘッダー付きメインスタイルシート
assets/css/追加のスタイルシート
theme.jsonブロックテーマ(WP 5.9+)- 構造化されたトークン

デザイントークンの抽出

WordPress パターンEmDash 変数
Body フォントファミリー--font-body
見出しフォント--font-heading
プライマリカラー--color-primary
背景色--color-base
テキスト色--color-contrast
コンテンツ幅--content-width

ベースレイアウトの作成

抽出した CSS 変数、ヘッダー/フッター構造、フォント読み込み、レスポンシブブレークポイントで 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>

フェーズ 2: テンプレート変換

テンプレート階層マッピング

WordPress テンプレートAstro ルート
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.phpsrc/layouts/Base.astro の一部
sidebar.phpsrc/components/Sidebar.astro

テンプレートタグマッピング

WordPress 関数EmDash の対応
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")

ループの変換

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>

単一テンプレートの変換

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>

テンプレートパーツの変換

WordPress の get_template_part() 呼び出しは Astro コンポーネントのインポートになります。template-parts/content-post.php パーシャルは PostCard.astro コンポーネントとなり、ループ内でインポートしてレンダリングします。

フェーズ 3: 動的機能

ナビゲーションメニュー

functions.php 内のメニューを特定し、対応する EmDash メニューを作成します。

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

ウィジェットエリア(サイドバー)

テーマ内のウィジェットエリアを特定してレンダリングします。

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

ウィジェットタイプマッピング

WordPress ウィジェットEmDash ウィジェットタイプ
Text/Custom HTMLtype: "content"
Custom Menutype: "menu"
Recent Postscomponent: "core:recent-posts"
Categoriescomponent: "core:categories"
Tag Cloudcomponent: "core:tag-cloud"
Searchcomponent: "core:search"

タクソノミー

テーマに登録されたタクソノミーをクエリします。

---
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 カスタマイザーEmDash 設定
Site Titletitle
Taglinetagline
Site Iconfavicon
Custom Logologo
Posts per pagepostsPerPage

ショートコードから Portable Text へ

WordPress のショートコードは Portable Text カスタムブロックになります。

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>

PortableText に登録します:

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

シードファイル構造

完全なコンテンツモデルをシードファイルにキャプチャします。設定、タクソノミー、メニュー、ウィジェットエリアを含めます。

{
	"$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 }
				}
			]
		}
	]
}

完全な仕様についてはシードファイル形式を参照してください。

移植チェックリスト

フェーズ 1(デザイン): CSS 変数の抽出、フォントの読み込み、カラースキームの一致、レスポンシブブレークポイントの動作。

フェーズ 2(テンプレート): ホームページ、単一投稿、アーカイブ、404 ページがすべて正しくレンダリングされる。

フェーズ 3(動的機能): サイト設定の構成、メニューの動作、タクソノミーのクエリ、ウィジェットエリアのレンダリング、シードファイルの完成。

エッジケース

子テーマ

テーマに親がある場合(style.cssTemplate: を確認)、まず親テーマを分析してから子テーマのオーバーライドを適用します。

ブロックテーマ(FSE)

WordPress 5.9+ のブロックテーマはデザイントークンに theme.json、ブロックマークアップに templates/*.html を使用します。ブロックマークアップを Astro コンポーネントに変換し、theme.json からトークンを抽出します。

ページビルダー

Elementor、Divi などで構築されたコンテンツはテーマファイルではなくポストメタに保存されます。このコンテンツはテーマ移植ではなく WXR 経由でインポートします。テーマ移植ではシェルに集中してください。ページビルダーのコンテンツはインポート後に Portable Text でレンダリングされます。

次のステップ