このページで分かること

  • 移植の前に記録すること(投稿タイプ、タクソノミー、パーマリンク、メニュー、ウィジェットなど)と、WordPressのテンプレートファイルとAstroのルートの対応
  • デザインの取り出し方と、一覧・個別記事・タクソノミーのアーカイブ、メニュー・ウィジェットエリア・サイト情報・ページテンプレート・ショートコードの変換
  • シードファイルの作成、段階的な移植の手順、子テーマ・ブロックテーマ・ページビルダー・WooCommerceのテーマの扱い
難易度
実践
読む時間
5分
このページの目次

WordPressのテーマは、デザイン、ルートのテンプレート、データベースで管理する機能の3つに分けて移植します。移植の結果は、EmDashの取得処理とシードファイルを備えた完全なAstroプロジェクトになります。テーマの実行環境が読み込むPHPのテーマにはなりません。

コンテンツとURLから始める

テンプレートを変換する前に、代表的なコンテンツをインポートします。サイトで使っているWordPressの投稿タイプ、タクソノミー、パーマリンクのルール、メニュー、ウィジェットエリア、カスタマイザーの値、ショートコード、プラグインが生成するマークアップを記録します。

コンテンツの種類ごとに、新しいURLを決めます。Astroのルートは明示的なファイルで決まるため、WordPressのテンプレート階層に自動で対応するものはありません。

現在のblogテンプレートは、次の対応を使っています。

WordPress EmDashのテンプレートのパス
front-page.php または home.php src/pages/index.astro
single.php src/pages/posts/[slug].astro
archive.php src/pages/posts/index.astro
page.php src/pages/pages/[slug].astro
category.php src/pages/category/[slug].astro
tag.php src/pages/tag/[slug].astro
search.php src/pages/search.astro
404.php src/pages/404.astro
header.phpfooter.php src/layouts/Base.astro
template-parts/content.php src/components/PostCard.astro または別のコンポーネント

starterテンプレートは、固定ページに src/pages/[slug].astro を使っています。ルートの構成をどちらか1つに決め、シードの urlPattern とリダイレクトをそれに合わせます。

本サイトの補足 やさしい解説

WordPressでは、single.phparchive.php のようなファイル名の決まり(テンプレート階層)に従って、どのファイルでページを描画するかが自動で決まります。Astroにはこの仕組みがなく、src/pages/ の下に置いたファイルの場所がそのままURLになります。そのため、移植の最初に「記事は /posts/スラッグ にする」のようにURLを決め、上の表を参考に対応するファイルを作ります。決めたURLは、シードファイルの urlPattern と、古いURLからのリダイレクトにも同じものを使います。

デザインを取り出す

描画されるデザインを決めているソースファイルを集めます。

  • style.css と、読み込まれている(enqueueされている)スタイルシート。
  • ブロックテーマの場合は theme.json
  • テンプレートのマークアップとテンプレートパーツ。
  • フォント、アイコン、画像と、それぞれのライセンス。
  • レスポンシブのブレークポイントと、操作に応じた動き。

デザイントークンをAstroプロジェクトのCSSにコピーし、全ページ共通の文書の枠組みを src/layouts/Base.astro に作ります。個々のルートに広げる前に、繰り返し使うテンプレートパーツをコンポーネントに変換します。

新しいコンポーネントがその動作を必要としない場合は、WordPressが生成したクラス名やJavaScriptをコピーしないでください。残すのは目に見えるレイアウトと操作であり、実装の名残ではありません。

アーカイブの取得処理を変換する

WP_QuerygetEmDashCollection() になります。絞り込みと並べ替えを取得処理の中で指定し、データベースに処理させます。

次の例は、最新の記事のアーカイブを描画します。

WordPress

archive.php
<?php
$posts = new WP_Query([
  'post_type' => 'post',
  'post_status' => 'publish',
  'posts_per_page' => 12,
  'orderby' => 'date',
  'order' => 'DESC',
]);

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

EmDash

src/pages/posts/index.astro
---
import { getEmDashCollection } from "emdash";

const { entries: posts, error, cacheHint } = await getEmDashCollection("posts", {
  orderBy: { published_at: "desc" },
  limit: 12,
});

if (error) return new Response("Could not load posts", { status: 500 });
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---

{posts.map((post) => (
  <article>
    <h2><a href={`/posts/${post.id}`}>{post.data.title}</a></h2>
  </article>
))}

sortsortBy ではなく、orderBy を使います。キーには、published_atcreated_at のような保存されているフィールド名や、インデックスを持つコレクションのフィールドを指定します。

本サイトの補足 やさしい解説

WordPressの WP_Query で「投稿を日付の新しい順に12件」と指定していた部分は、EmDashでは getEmDashCollection("posts", { orderBy: ..., limit: 12 }) と書きます。while ループで1件ずつ取り出す代わりに、受け取った posts の配列を map で順に描画します。取得に失敗した場合は error に値が入るため、例ではステータス500の応答を返しています。

個別エントリーのテンプレートを変換する

Astro.params から得たスラッグを使って、getEmDashEntry() を呼び出します。リッチテキストは PortableText で描画します。

src/pages/posts/[slug].astro
---
import { decodeSlug, getEmDashEntry } from "emdash";
import { PortableText } from "emdash/ui";
import Base from "../../layouts/Base.astro";

const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.redirect("/404");

const { entry: post, error, cacheHint } = await getEmDashEntry("posts", slug);
if (error) return new Response("Could not load the post", { status: 500 });
if (!post) return Astro.redirect("/404");
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---

<Base title={post.data.title} content={{ collection: "posts", id: post.data.id, slug }}>
  <article>
    <h1 {...post.edit.title}>{post.data.title}</h1>
    <PortableText value={post.data.content} />
  </article>
</Base>

post.id はAstroのルート識別子で、通常はスラッグです。post.data.id は、EmDashに保存されているコンテンツIDです。コメント、タクソノミーのヘルパー、ビジュアル編集のコンテキストには data.id を使います。

タクソノミーのアーカイブを変換する

タクソノミー名を指定してタームを取得し、そのタクソノミー名をコレクションの where フィルターに使います。現在のblogテンプレートは、単数形のタクソノミー名とルートのディレクトリー名を使っています。

src/pages/category/[slug].astro
---
import { decodeSlug, getEmDashCollection, getTerm } from "emdash";

const slug = decodeSlug(Astro.params.slug);
const term = slug ? await getTerm("category", slug, { includeCounts: false }) : null;
if (!term) return Astro.redirect("/404");

const { entries: posts } = await getEmDashCollection("posts", {
  where: { category: term.slug },
  orderBy: { published_at: "desc" },
});
---

<h1>{term.label}</h1>
{posts.map((post) => <a href={`/posts/${post.id}`}>{post.data.title}</a>)}

アーカイブで多数のエントリーのタームが必要な場合は、entry.data.id を指定して getTermsForEntries() を使います。これにより、エントリーごとのヘルパーをループの中で呼び出す代わりに、関連を取得するクエリを1回にまとめられます。

テーマの動的な機能を変換する

メニューの定義を seed/seed.json に作成し、getMenu("primary") で取得します。メニューの項目は初期設定の後に並べ替えや編集ができるため、同じナビゲーションをハードコードしたリンクとして重複して書かないでください。

次のコンポーネントは、入れ子になったメニュー項目を保ち、現在のページに印を付けます。

src/components/PrimaryNav.astro
---
import { getMenu } from "emdash";

const menu = await getMenu("primary");
---

{menu && (
  <nav aria-label="Primary navigation">
    <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>
              {item.children.map((child) => (
                <li><a href={child.url}>{child.label}</a></li>
              ))}
            </ul>
          )}
        </li>
      ))}
    </ul>
  </nav>
)}

ウィジェットエリア

ウィジェットエリアをシードで定義し、emdash/ui<WidgetArea name="sidebar" /> で描画します。コンポーネントウィジェットは、テンプレートが対応するコンポーネントIDを登録している場合にだけ使います。

src/layouts/Post.astro
---
import { WidgetArea } from "emdash/ui";
---

<main><slot /></main>
<aside aria-label="Related content">
  <WidgetArea name="sidebar" />
</aside>

サイト情報

サイトのタイトル、キャッチフレーズ、ロゴ、ファビコンは getSiteSettings() で読み込みます。変わらない法的な文言やデザイン上の文言はテンプレートに残してかまいませんが、管理者が管理するサイト情報は設定から取得する必要があります。

WordPressの値 EmDashの設定
サイトのタイトル title
キャッチフレーズ tagline
カスタムロゴ logo
サイトアイコン favicon
1ページに表示する最大投稿数 postsPerPage

ページテンプレート

編集者が複数のページレイアウトから選ぶ必要がある場合は、pagesコレクションにセレクトフィールドを追加し、保存される値ごとに既知のコンポーネントを対応させます。保存された文字列を、任意のインポートパスに変換しないでください。

ショートコードとブロック

コンテンツを含むショートコードとWordPressのブロックは、Portable Textの形に対応させます。組み込みのブロックには通常の PortableText レンダラーを使い、独自の _type の値にはコンポーネントマップを使います。

シードは独自のブロックのデータを用意できますが、独自のエディターは登録しません。移植にパッケージ化したAstroのレンダラーや、独自のReactの編集UIが必要な場合は、ネイティブ型プラグインを使います。

たとえば、WordPressのギャラリーのショートコードを publication.gallery のような名前空間付きの _type に対応させ、Gallery.astro レンダラーを作成し、PortableText のコンポーネントマップに渡します。

src/components/ArticleBody.astro
---
import type { PortableTextBlock } from "emdash";
import { PortableText } from "emdash/ui";
import Gallery from "./blocks/Gallery.astro";

interface Props {
  value: PortableTextBlock[];
}

const { value } = Astro.props;
const customTypes = { "publication.gallery": Gallery };
---

<PortableText value={value} components={{ type: customTypes }} />
本サイトの補足 やさしい解説

WordPressでは、メニュー、ウィジェット、サイトのタイトルやロゴを管理画面で変更でき、テーマはそれを読み込んで表示します。EmDashでも同じで、テンプレートにリンクや文言を直接書かず、getMenu()<WidgetArea>getSiteSettings() で管理画面の設定を読み込みます。記事の本文中のショートコード(たとえばギャラリー)は、Portable Textの独自の _type として保存し、その _type を描画するAstroのコンポーネントを自分で用意します。

シードを作る

現在のテンプレートでは、シードは seed/seed.json に置き、package.json#emdash.seed でそのファイルを指定します。テンプレートが前提とするコレクションのフィールド、タクソノミー、メニュー、ウィジェットエリア、セクション、サンプルのエントリーをすべて宣言します。

次の断片は、記事のルートで使うフィールドを定義します。

seed/seed.json
{
  "$schema": "https://emdashcms.com/seed.schema.json",
  "version": "1",
  "collections": [
    {
      "slug": "posts",
      "label": "Posts",
      "labelSingular": "Post",
      "supports": ["drafts", "revisions", "search", "seo"],
      "urlPattern": "/posts/{slug}",
      "fields": [
        { "slug": "title", "label": "Title", "type": "string", "required": true },
        { "slug": "content", "label": "Content", "type": "portableText" },
        { "slug": "excerpt", "label": "Excerpt", "type": "text" },
        { "slug": "featured_image", "label": "Featured image", "type": "image" }
      ]
    }
  ]
}

コンテンツID、参照、多言語化、メディア、検証、適用のオプションについては、シードファイルを参照してください。

確かめられる段階に分けて移植する

  1. WordPressの代表的なエクスポートをインポートし、対応していないブロックやフィールドを記録します。

  2. デプロイ先に合った、現在のEmDashのテンプレートから始めます。

  3. 共通のレイアウト、トークン、タイポグラフィー、レスポンシブの枠組みを実装します。

  4. インポートしたコレクションのスラッグとフィールドを使って、アーカイブと個別エントリーのルートを実装します。

  5. 移植元のテーマが実際に使っているタクソノミーのルート、検索、メニュー、ウィジェットエリア、サイトの設定を追加します。

  6. ルートが依存するものすべてのシードの定義を追加し、空のデータベースに対して初期設定をテストします。

  7. 代表的な公開URLを、モバイルとデスクトップの幅で比較します。空のフィールド、長いタイトル、画像がない場合、下書き、404のルートをテストします。

  8. 切り替えの前に、変更したすべてのパーマリンクのリダイレクトを用意します。

移植が難しいWordPressテーマの扱い

  • 子テーマ:親テーマと子テーマを解決した結果の出力を合わせて扱います。子テーマのディレクトリーだけでなく、実際に使われるテンプレートとスタイルを移植します。
  • ブロックテーマtheme.json をデザイントークンの元として、templates/*.html をコンテンツの構造として使い、その結果をAstroのコンポーネントとして表現します。
  • ページビルダー:先に代表的なページをインポートします。ビルダーのショートコードや独自形式のJSONは、通常、意図的にPortable Textへ変換するか、ページテンプレートを設計し直す必要があります。
  • WooCommerceのテーマ:ECの動作は、別のアプリケーションとの連携として扱います。表示用のファイルを移植しても、商品、カート、チェックアウト、決済、注文の仕組みの代わりにはなりません。