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

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
페이지당 게시물 수postsPerPage

페이지 템플릿

편집자가 여러 페이지 레이아웃 중에서 선택해야 한다면, 페이지 컬렉션에 select 필드를 추가하고 저장된 각 값을 알려진 컴포넌트에 매핑하세요. 저장된 문자열을 임의의 import 경로로 바꾸지 마세요.

숏코드와 블록

콘텐츠를 담고 있는 숏코드와 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 테마: 커머스 동작은 별도의 애플리케이션 통합으로 취급하세요. 표시용 파일을 포팅한다고 해서 상품, 장바구니, 결제, 지불, 주문의 의미 체계가 대체되지는 않습니다.