移植 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 来承载页面。请选定一种路由模型,并让种子文件中的 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>
))}

请使用 orderBy,而不是 sort 或 sortBy。键是存储的字段名,例如 published_at、created_at,或已建立索引的集合字段。

转换单条目模板

使用 Astro.params 中的 slug 调用 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 的路由标识符,通常就是 slug。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
每页文章数postsPerPage

页面模板

如果编辑者必须在多种页面布局中进行选择,请在页面集合中添加一个选择字段,并把每个存储的值映射到已知的组件。不要把存储的字符串变成任意的导入路径。

短代码和区块

将承载内容的短代码和 WordPress 区块映射为 Portable Text 的结构。内置区块使用普通的 PortableText 渲染器,自定义的 _type 值则使用组件映射。

种子文件可以提供自定义区块数据,但它不会注册自定义编辑器。如果移植需要打包的 Astro 渲染器或自定义 React 编辑界面,请使用原生插件。

例如,把 WordPress 的图库短代码映射为带命名空间的 _type,如 publication.gallery,创建一个 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、引用、本地化、媒体、校验和应用选项,请阅读 Seed 文件。

分阶段移植并逐步验证

  1. 导入具有代表性的 WordPress 导出文件,并记录所有不受支持的区块或字段。

  2. 从适用于部署目标的当前 EmDash 模板开始。

  3. 实现共享布局、设计令牌、排版和响应式外壳。

  4. 使用导入后的集合 slug 和字段,实现归档路由和单条目路由。

  5. 添加源主题实际用到的分类法路由、搜索、菜单、小部件区域和站点设置。

  6. 为每个路由依赖项添加种子定义,并在空数据库上测试设置流程。

  7. 在移动端和桌面端宽度下比较具有代表性的公开 URL。测试空字段、过长的标题、缺失的图片、草稿和 404 路由。

  8. 在切换上线之前,为每个发生变化的固定链接准备重定向。

应对棘手的 WordPress 主题

  • 子主题: 合并解析后的父主题和子主题的输出。移植的是实际生效的模板和样式,而不仅仅是子主题目录。
  • 区块主题: 将 theme.json 作为设计令牌的来源,将 templates/*.html 作为内容结构的来源,然后用 Astro 组件来表达结果。
  • 页面构建器: 先导入具有代表性的页面。构建器的短代码和专有 JSON 通常需要经过审慎的 Portable Text 转换,或者重新设计页面模板。
  • WooCommerce 主题: 将商务行为视为独立的应用集成。移植表现层文件并不能取代产品、购物车、结账、支付或订单的语义。