Package Exports
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (to-wordpress) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Migrate anything text-shaped to WordPress — powered by GitHub Copilot CLI.
Point it at a Jekyll / Hugo / Eleventy / Gatsby / Next / Astro / Hexo /
Docusaurus / MkDocs site, a WordPress WXR export, a Medium or Substack
export, a folder of Word documents, a spreadsheet of rows, a library of
PDFs, an EPUB, a GitHub repo README, a pile of markdown notes, or plain
.txt files. Walk away. Come back to a fully working, pixel-close
WordPress under
wp-env
with a custom theme, a site plugin, imported content and media,
preserved permalinks (where they existed), and a self-verified build.
Install · Quick start · How it works · Supported sources · CLI · FAQ
Why
Moving a content-heavy static site to WordPress by hand is a week of
template translation, shortcode rewrites, data shuffling, and URL remapping.
to-wordpress collapses that into one command: a deterministic TypeScript
pipeline drives GitHub Copilot CLI
(copilot -p) in a hybrid orchestration — the tool owns phase
transitions, file I/O, and verification; Copilot handles the creative parts
(Liquid → PHP, custom-block generation, edge-case normalization, auto-fix).
What lands in wp-content/:
- A bespoke classic WordPress theme with
template-parts/…mirroring your source includes one-to-one. - A site plugin owning non-theme concerns — CPTs, newsletter, giscus, analytics, cookie banner, redirects, options page.
- Posts, pages, terms, menus, users, featured images — imported via
wp-cliwith original permalinks preserved by default. - A self-written
WORDPRESS_MIGRATION.mdthat documents every decision so you can audit or replay the run.
Install
to-wordpress is meant for one-shot runs, so use npx — no global install
needed:
npx to-wordpress ./path/to/your-siteOr install globally if you iterate on the same source:
npm install -g to-wordpress
to-wordpress ./path/to/your-siteRequirements
- Node.js ≥ 20
- Docker (for
wp-envand the Jekyll prerender container) - GitHub Copilot CLI:
brew install --cask github-copilot-clithencopilot login
The tool is self-contained otherwise — no Ruby, no PHP, no wp-cli on the
host. wp-env runs WordPress in Docker and the tool talks to wp-cli
through it.
Quick start
# 1. Clone or cd into any static site
git clone https://github.com/you/your-jekyll-site
cd your-jekyll-site
# 2. Run the migration (interactive TUI, asks a few URL questions)
npx to-wordpress .
# 3. Open the result
open http://localhost:8888When it finishes you'll have:
your-jekyll-site/
├── .wp-env.json # theme + plugin + content mounted into WordPress
├── WORDPRESS_MIGRATION.md # the plan + per-phase status + state block
└── WORDPRESS_MIGRATION/
├── theme/ # your generated classic theme
├── plugin/ # your site plugin
├── content/ # canonical markdown for every post & page
├── media/ # collected image assets
├── rendered/ # Jekyll/Hugo/Eleventy build output (ground truth)
├── import-manifest.json # exact payload fed to wp-cli
├── redirects.json # old-path → new-path map
└── verify-report.json # url parity + count diff from the last runWant non-interactive? Add --yes (uses defaults for every prompt):
npx to-wordpress ./my-site --yesHow it works
flowchart LR
src[Source site] --> D[Detect]
D --> P[Plan]
P --> B[Boot wp-env]
B --> T[Theme]
T --> PL[Plugin]
PL --> N[Normalize]
N --> I[Import]
I --> V[Verify]
V -->|issues| F[Fix]
F --> V
V -->|clean| WP["Working WordPress @ localhost:8888"]Every phase is implemented in TypeScript with a deterministic fallback,
then optionally enriched by copilot -p running in autopilot mode. Copilot
streams JSONL events back to the tool's Ink TUI, which renders a live
phase dashboard, activity log, and approval prompts.
| # | Phase | What it does |
|---|---|---|
| 1 | Detect | Probes the source with one detector per SSG. Falls back to a Copilot-driven freestyle detector that reads files and never skips a folder. |
| 2 | Plan | Produces WORDPRESS_MIGRATION.md with a template map, a feature-to-plugin map, the exact permalink structure, and acceptance criteria. |
| 3 | Boot wp-env | Writes .wp-env.json, mounts your theme + plugin + work dir into WordPress, and runs npx wp-env start. |
| 4 | Theme | Builds the source site to capture ground-truth HTML, mirrors _sass/, assets/, _data/, _layouts/, _includes/ into the theme dir, then drives Copilot to emit PHP templates whose DOM byte-matches the reference. |
| 5 | Plugin | Generates a site plugin covering CPTs, newsletter, comments (giscus/disqus/commento), analytics (GA/GTM/Plausible/Umami), cookie banner, dark mode, social meta, redirects, shortcodes, custom blocks, and an options page. |
| 6 | Normalize | Turns every post/page into canonical markdown with a fixed front-matter schema. Unusual cases are handed to Copilot with a strict edge-case prompt. |
| 7 | Import | Runs a generated PHP script via wp eval-file inside the container to upsert posts, terms, menus, users, and media; sets show_on_front; populates the primary menu from _data/menu.yml. |
| 8 | Verify | Counts posts per type with wp-cli, fetches a sample of source URLs from the live WP, and diffs titles, H1s, and HTTP status. Writes verify-report.json. |
| 9 | Fix | Feeds the verify report back to a scoped Copilot run — minimal surgical edits to theme/plugin/content, idempotent wp-cli calls for data fixes. Re-runs Verify until clean or N iterations. |
Supported sources
to-wordpress ships detectors for every shape of content we've seen —
from real SSGs down to "a folder of PDFs". Each detector emits a
detector briefing that steers the theme, plugin, plan, and
normalize phases; binary/tabular formats are first routed through a
per-format conversion prompt (see src/prompts/convert-*.md)
that turns the raw source into canonical markdown before the rest of
the pipeline runs.
Static-site generators
| Source | Kind | What ships |
|---|---|---|
| Jekyll | jekyll |
Posts (_posts/, _drafts/) + any collections/<x>/, pages, layouts, includes, sass, _data/*, feature-level detection (giscus, mailchimp, analytics, dark mode, OG/Twitter) |
| Hugo | hugo |
Content sections → collections/CPTs, layouts/**/*.html, data/**/*, static/ |
| Eleventy | eleventy |
src/ or content/ posts, njk/liquid/hbs layouts |
| Hexo | hexo |
source/_posts/ posts + source/*.md pages, permalink preserved from _config.yml |
| Astro | astro |
src/content/<collection>/ collections + src/pages/*.{astro,md,mdx} pages |
| Gatsby | gatsby |
src/pages/*.{tsx,jsx} + content/**/*.md(x) |
| Next.js | next |
App/Pages router + content/ / posts/ / blog/ markdown |
Documentation frameworks
| Source | Kind | What ships |
|---|---|---|
| Docusaurus | docusaurus |
docs/ → docs collection, blog/ → posts, admonitions → Gutenberg groups |
| MkDocs (+ Material) | mkdocs |
docs/ collection, mkdocs.yml nav mirrored in WordPress menu |
CMS / platform exports
| Source | Kind | What ships |
|---|---|---|
| WordPress WXR | wp-wxr |
Full .xml dump: posts, pages, CPTs, categories, tags, authors, featured images |
| Ghost export | ghost-export |
Ghost JSON dump (posts, tags, users) |
| Medium export | medium-export |
posts/<date>_<slug>.html → posts, gists/tweets → Gutenberg embeds, canonical URL preserved |
| Substack export | substack-export |
posts.csv + posts/<id>.html → posts, paid-only → private, podcasts → podcast CPT |
Raw documents / text piles
| Source | Kind | What ships |
|---|---|---|
| Word bundle | docx-folder |
Folder of .docx / .doc / .rtf → one post per document (pandoc/mammoth) |
| Spreadsheet | xlsx-sheet |
.xlsx / .xls / .csv / .tsv → one post per row, column → front-matter field |
| PDF library | pdf-folder |
Folder of .pdf → one post per document, figures become Gutenberg image blocks |
| EPUB books | epub-book |
One .epub → one post per chapter, book metadata → site identity |
| Plain text | text-folder |
Folder of .txt / .rst → one post per file (first line = title) |
| Markdown pile | markdown-folder |
Obsidian / Notion / Zettelkasten exports, wiki-links preserved |
| Plain HTML | plain-html |
Every .html file as a page, every .md file as a post |
Code repos
| Source | Kind | What ships |
|---|---|---|
| GitHub repo | github-repo |
README → SaaS landing page (hero + feature grid + install CTA), docs/ → docs collection, LICENSE/CHANGELOG/CONTRIBUTING → separate pages |
Fallback
| Source | Kind | What ships |
|---|---|---|
| Anything else | unknown |
Deterministic file walker + Copilot-driven schema fill — never skips a folder |
Per-format conversion prompts
Formats that aren't already markdown are routed through a Copilot
prompt before normalize runs. Each prompt lives in src/prompts/ and
is self-contained:
convert-docx.md— Word documentsconvert-xlsx.md— Excel workbooksconvert-csv.md— CSV / TSVconvert-pdf.md— PDFsconvert-epub.md— EPUB booksconvert-txt.md— plain text / RSTconvert-html.md— generic HTML pagesconvert-wxr.md— WordPress WXRconvert-medium-html.md— Medium exportconvert-substack.md— Substack exportconvert-readme.md— GitHub README → landing page
Adding a new source type
~60 lines of code and one prompt:
- Drop a detector file in
src/detectors/, implementmatch()+detect(). - Register it in
src/detectors/index.ts. - If the source isn't already markdown, declare
rawSources[]on theDetectedContext, pick (or add) aRawSourceFormatinsrc/types.ts, and write asrc/prompts/convert-<format>.md.
CLI
Usage: to-wordpress [options] [source]
Migrate any codebase to WordPress. Hybrid orchestration with GitHub Copilot CLI.
Arguments:
source path to the source site to migrate (default: ".")
Options:
-v, --version print version
--skip-boot skip wp-env start (assumes already running)
--skip-copilot use deterministic fallbacks only, don't invoke copilot
-y, --yes auto-answer all prompts with defaults
--only <phase> run only this phase (advanced)
--from <phase> start from this phase (skip earlier ones)
--until <phase> stop after this phase (skip later ones)
--max-fix-iterations <n> max Verify→Fix iterations (default: 3)
-h, --help display help<phase> is one of: detect, plan, boot, theme, plugin,
normalize, import, verify, fix.
Re-running just the theme
Iterating on theme fidelity? The state is persisted inside
WORDPRESS_MIGRATION.md, so you can resume any later phase:
npx to-wordpress ./my-site --yes --from theme --skip-bootNon-interactive in CI
npx to-wordpress ./my-site --yes --skip-copilot --until normalizeSkipping Copilot means every Copilot-driven step uses the deterministic fallback — you still get detect, plan, normalize, and import, just without the pixel-perfect theme transforms.
The migration doc
to-wordpress writes a live, human-readable
WORDPRESS_MIGRATION.md inside your
source repo. It contains:
- The phase table with status + timestamps.
- Overview of the migration and permalink strategy.
- A template-mapping table (every source layout/include → WP file).
- A feature-to-output mapping (theme vs plugin vs external plugin).
- Risks & open questions.
- Acceptance criteria the Verify phase checks against.
- A
WPIFY:STATEJSON block at the bottom the tool reads back on resume.
You can commit this file — re-running to-wordpress updates it in place
rather than re-planning from scratch.
Prompts
Every Copilot-driven phase runs with a rigorously structured prompt in
src/prompts/. A shared preamble (_shared.md)
gives every phase the same autonomy + fidelity contract; each phase adds:
- Explicit Scope (which dirs may be written).
- Explicit Required output (every file that must exist).
- Non-negotiable rules (e.g. never
esc_html(get_the_title())in a template). - Banned anti-patterns (no TODOs, no Lorem ipsum, no hard-coded localhost URLs, no silenced PHP errors).
- A self-check the model walks before stopping.
The prompts are intentionally opinionated about WordPress best practices: escaping, text-domain consistency, activation hooks, idempotent wp-cli, options API, rewrite rules.
FAQ
Is my data safe? The tool writes everything under WORDPRESS_MIGRATION/
inside your source repo and to a local wp-env Docker volume. No network
calls except to the GitHub Copilot API and Docker Hub. Nothing is sent to
your live WordPress until you decide to deploy the generated theme/plugin.
Why wp-env? It pins WordPress + PHP versions, runs wp-cli in-container,
and tears down cleanly. You can export the database afterwards with
npx wp-env run cli wp db export.
What about pixel-perfect? The theme phase builds your Jekyll/Hugo site into HTML first (inside a Ruby 3.2 container so host Ruby version doesn't matter), then feeds that ground-truth DOM to Copilot as the exact target. Verify re-fetches your local WP and diffs structure + titles + status — any drift goes back to a scoped Fix loop.
Can I use a live WordPress instead of wp-env? Not yet — the import
uses wp eval-file inside the wp-env cli container. Remote
WP-via-REST-API is a planned target.
Does it migrate comments? Comments stay wherever they live (giscus/disqus/commento). The plugin re-attaches the same integration in WordPress so threads keep working.
What's the Copilot bill? Expect 3–5 Copilot sessions per full run (plan, theme, plugin, per-post normalize edge cases, fix loop). A ~40-post Jekyll site runs in ~20 minutes end-to-end.
Development
git clone https://github.com/f/to-wordpress
cd to-wordpress
npm install
npm run build
node dist/cli.js ./fixtures/unknown --skip-boot --skip-copilot --yes --until normalizeRun type checks and build in watch mode:
npm run typecheck
npm run devThe codebase is:
src/cli.tsx— commander entry + Ink TUI bootstrap.src/copilot/run.ts— spawncopilot -p, parse JSONL events.src/detectors/— one detector per SSG + freestyle fallback.src/phases/— one file per phase.src/prompts/— markdown templates + loader.src/tui/— Ink app, event bus, headless logger.src/wp/— thin wrappers aroundnpx wp-envandwp-cli.
PRs welcome — especially new detectors, new Copilot prompts for specific frameworks, and verify rules that catch more drift.
License
MIT © Fatih Kadir Akın