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 변수
본문 폰트 패밀리--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 CustomizerEmDash 설정
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.css에서 Template: 확인), 부모 테마를 먼저 분석한 다음 자식 테마 오버라이드를 적용합니다.

블록 테마 (FSE)

WordPress 5.9+ 블록 테마는 디자인 토큰에 theme.json을, 블록 마크업에 templates/*.html을 사용합니다. 블록 마크업을 Astro 컴포넌트로 변환하고 theme.json에서 토큰을 추출합니다.

페이지 빌더

Elementor, Divi 또는 유사한 빌더로 구축된 콘텐츠는 테마 파일이 아닌 포스트 메타에 저장됩니다. 이 콘텐츠는 테마 포팅이 아닌 WXR을 통해 가져옵니다. 테마 포팅은 셸에 집중하세요—페이지 빌더 콘텐츠는 가져오기 후 Portable Text를 통해 렌더링됩니다.

다음 단계