WordPress テーマを移植する

このページ

WordPress テーマを移植するには、デザイン、ルートテンプレート、データベースで管理される機能を分けて扱います。成果物は、EmDash のクエリとシードファイルを備えた完全な Astro プロジェクトであり、テーマランタイムが読み込む PHP テーマではありません。

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

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

コンテンツタイプごとに新しい URL を決めます。Astro のルートは明示的なファイルなので、WordPress のテンプレート階層に相当する自動的な仕組みはありません。

現在のブログテンプレートでは、次のように対応付けています。

WordPressEmDash のテンプレートパス
front-page.php または home.phpsrc/pages/index.astro
single.phpsrc/pages/posts/[slug].astro
archive.phpsrc/pages/posts/index.astro
page.phpsrc/pages/pages/[slug].astro
category.phpsrc/pages/category/[slug].astro
tag.phpsrc/pages/tag/[slug].astro
search.phpsrc/pages/search.astro
404.phpsrc/pages/404.astro
header.php と footer.phpsrc/layouts/Base.astro
template-parts/content.phpsrc/components/PostCard.astro またはその他のコンポーネント

一方、スターターテンプレートではページに src/pages/[slug].astro を使います。ルートのモデルを 1 つに決め、シードの urlPattern とリダイレクトをそれに合わせてください。

デザインを抽出する

レンダリングされるデザインを決定するソースファイルを集めます。

  • style.css と、enqueue されているスタイルシート。
  • ブロックテーマの場合は theme.json。
  • テンプレートのマークアップとテンプレートパーツ。
  • フォント、アイコン、画像と、そのライセンス。
  • レスポンシブのブレークポイントとインタラクティブな動作。

デザイントークンを Astro プロジェクトの CSS にコピーし、共有のドキュメントシェルを src/layouts/Base.astro に構築します。個々のルートを広げる前に、繰り返し使われるテンプレートパーツをコンポーネントに変換してください。

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

アーカイブのクエリを変換する

WP_Query は getEmDashCollection() になります。フィルタと並べ替えはクエリの中で行い、データベースに処理を任せます。

次の例は、最新の投稿のアーカイブをレンダリングします。

WordPress

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

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

sort や sortBy ではなく、orderBy を使ってください。キーには、published_at、created_at、またはインデックスが設定されたコレクションのフィールドなど、保存されているフィールド名を指定します。

単一エントリのテンプレートを変換する

Astro.params のスラッグを使って getEmDashEntry() を呼び出します。リッチテキストは PortableText でレンダリングします。

---
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.rewrite("/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.rewrite("/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 フィルタで使います。現在のブログテンプレートでは、単数形のタクソノミー名とルートのディレクトリを使っています。

---
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.rewrite("/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>)}

アーカイブで多数のエントリのタームが必要な場合は、getTermsForEntries() を entry.data.id と組み合わせて使います。これにより、ループ内でエントリごとのヘルパーを呼び出す代わりに、リレーションのクエリがバッチ化されます。

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

メニュー

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

次のコンポーネントは、入れ子のメニュー項目を保ち、現在のページを示します。

---
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 を登録している場合にのみ使います。

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

ページテンプレート

エディターが複数のページレイアウトから選択できる必要がある場合は、ページのコレクションに select フィールドを追加し、保存された各値を既知のコンポーネントに対応付けます。保存された文字列を、任意のインポートパスに変えてはいけません。

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

コンテンツを持つショートコードと WordPress のブロックを、Portable Text の形に対応付けます。組み込みのブロックには通常の PortableText レンダラーを使い、カスタムの _type の値にはコンポーネントマップを使います。

シードはカスタムブロックのデータを提供できますが、カスタムエディターは登録しません。移植にパッケージ化された Astro レンダラーやカスタムの React 編集 UI が必要な場合は、ネイティブプラグインを使ってください。

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

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

シードを構築する

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

次のフラグメントは、投稿のうちルートに関わるフィールドを定義します。

{
  "$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 テーマ: コマース機能は、別のアプリケーション連携として扱います。表示用のファイルを移植しても、商品、カート、チェックアウト、支払い、注文の意味論を置き換えることにはなりません。