用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/tomevault-io/skills-registry --skill wetm-setup命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
基于 SOC 职业分类
| name | wetm-setup |
| description | >- Use when this capability is needed. |
This skill guides you through a complete WordPress → Markdown/MDX migration using the
wordpress-export-to-markdown (wetm) tool in this project directory. It runs in two phases:
wetm.config.jsYou are operating inside the wordpress-export-to-markdown project directory. Run all
node app.js commands from this directory.
If the user hasn't exported from WordPress yet, tell them:
In WordPress admin go to Tools → Export, choose All content, click Download Export File. This gives you a
.xmlfile. Drop it in theinput/folder of this project, or note its full path.
Ask: "Where is your WordPress export XML? If you put it in input/, just tell me the
filename. Otherwise give me the full path."
If they're unsure, look for XML files:
Get-ChildItem -Path "input","." -Filter "*.xml" -Recurse -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty FullName
Once you have the path, note it as $XML_PATH for all subsequent commands.
wetm init to scan the XMLnode app.js init "$XML_PATH"
This prints a discovery report and writes input/wetm.config.<slug>.js. Read both the
console output AND the generated config file. The config file is your working baseline
for all subsequent edits.
If the command refuses because a config already exists for this file, ask the user: "A config for this XML already exists. Do you want to start fresh (delete and regenerate) or keep working with the existing config?"
If starting fresh, delete the old config and re-run init.
Read input/wetm.config.<slug>.js. Extract and note:
postTypes — every type with its count; which ones wetm init already set enabled: falsemeta.rules section — all detected meta keys (even those commented out)shortcodeHandlers stubs — shortcodes found in contentblockHandlers stubs — Gutenberg blocks that need handlersseo.plugin — which SEO plugin was auto-detectedYou now have everything you need to ask smart, targeted questions.
Present each group, wait for answers, then move to the next. Do not dump all questions at once. Use what the generated config already tells you — skip irrelevant questions.
Ask these two questions together:
What framework are you migrating to?
Output format? Recommend based on their answer:
export const blocks that frameworks can import)Explain: "MDX lets you embed React/Astro components in posts and handles complex nested data structures from ACF as importable JavaScript. Plain Markdown works everywhere with no build-step dependency."
Output directory? Suggest output/<site-slug>/ so multiple site migrations
never collide.
Show the user a plain-English table of every detected post type:
Post types in your WordPress export:
ENABLED BY DEFAULT:
├── post (N posts) — standard blog posts
├── page (N posts) — standard pages
└── [other custom types that wetm init left enabled]
DISABLED BY DEFAULT (plugin internals — no real content):
├── acf-field, acf-field-group, etc. — ACF schema definitions
├── bricks_template — Bricks builder layouts
└── seopress_404, seopress_schemas — SEOPress internal records
UNDECIDED (custom types you need to classify):
└── [any type wetm init didn't auto-disable]
For each undecided custom type, ask:
"Is [type-name] (N posts) real content you want to migrate? If yes, what folder name
should it go in? (e.g. reviews, guides, products)"
Types you should always disable (plugin-internal, never content):
acf-field, acf-field-group, acf-post-type, acf-taxonomy, acf-ui-options-pagebricks_templateseopress_404, seopress_schemaswpcode_snippet, yst_prominent_wordshop_order, shop_coupon, shop_order_refund (unless migrating WooCommerce orders)custom_css, nf_sub, wpcf7_contact_formTell the user: "Plugin-internal types store configuration records and layout data, not content. They produce useless output files and inflate counts."
Skip this group if no SEO plugin was detected (no seo.plugin in config).
If SEO plugin was detected (Yoast, RankMath, SEOPress, or All in One SEO), ask:
"Your site uses [PLUGIN]. Should I migrate SEO titles, descriptions, canonical URLs,
and noindex flags into a seo: block in each post's frontmatter?"
If yes: keep the seo section. Ask: "What should the frontmatter key be called?"
(default is seo; some prefer meta, og, or head)
If no: remove the seo section; remove the SEO plugin from plugins.enabled.
This section has the highest impact on migration quality. Do it carefully.
Look at the meta.rules section of the generated config. It lists all detected meta
keys with usage counts and sample values (commented out).
Step D1 — Auto-classify obvious noise (no need to ask the user):
Always skip these — add them to meta.deny if not already there:
_edit_lock, _edit_last, _wp_trash_meta_time, _wp_trash_meta_status_pingme, _encloseme, _oembed_*_wp_page_template (layout config, not content)_thumbnail_id (covered automatically as coverImage)_bricks_page_content_2, _bricks_page_header_2, _bricks_template_type,
_bricks_editor_mode, _bricks_template_settings_kad_blocks_custom_css, _kad_blocks_head_custom_js,
_kad_blocks_body_custom_js, _kad_blocks_footer_custom_js,
kt_blocks_editor_widthACF reference keys (underscore-prefixed keys that store the ACF field key, not the value):
my_field exists, the corresponding _my_field is its ACF reference — skip it.
The acf plugin pack handles this automatically. No explicit rule needed.SEO keys are handled by SEO plugin packs — no explicit rules needed for _yoast_wpseo_*,
rank_math_*, _seopress_*, _aioseop_*.
WooCommerce keys are handled by the woocommerce plugin pack — no explicit rules needed
for _price, _stock_status, _sku, _product_*, etc.
Step D2 — Identify content-bearing fields (ask the user):
Look at the sample values in the commented meta.rules. If a key's sample value contains
HTML tags (<p>, <br>, <ul>, <strong>, etc.) or appears to be a long block of text,
it likely stores the post's real body content in a meta field instead of post_content.
This is extremely common with:
review_content, review_body, review_textguide_content, content_bodyAsk the user: "I found these fields that appear to contain HTML content: [list the fields]. Should this content appear in the post body (body text), in frontmatter (as raw HTML), or be skipped?"
If the user wants it in the body, add to contentFields with an HTML→Markdown template:
{
key: "field_name",
template: (html) => {
if (!html || typeof html !== 'string') return '';
return html
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/p>\s*<p[^>]*>/gi, '\n\n')
.replace(/<p[^>]*>/gi, '').replace(/<\/p>/gi, '')
.replace(/<strong>(.*?)<\/strong>/gi, '**$1**')
.replace(/<em>(.*?)<\/em>/gi, '*$1*')
.replace(/<a[^>]+href="([^"]+)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)')
.replace(/<[^>]+>/g, '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, ' ')
.replace(/\n{3,}/g, '\n\n')
.trim();
},
},
For list fields (HTML <ul>/<ol> bullet lists), use this template instead:
{
key: "key_takeaways",
position: "prepend", // put before the main body
template: (html) => {
if (!html || typeof html !== 'string') return '';
const items = [];
const liRe = /<li[^>]*>([\s\S]*?)<\/li>/gi;
let m;
while ((m = liRe.exec(html)) !== null) {
const text = m[1]
.replace(/<strong>(.*?)<\/strong>/gi, '**$1**')
.replace(/<[^>]+>/g, '')
.replace(/&/g, '&').replace(/ /g, ' ')
.replace(/\s+/g, ' ').trim();
if (text) items.push(`- ${text}`);
}
if (!items.length) return '';
return `## Section Heading\n\n${items.join('\n')}`;
},
},
Important: Any field added to contentFields must also be set to "skip" in
meta.rules so it doesn't double-appear in frontmatter.
Step D3 — Identify frontmatter fields (ask the user):
For remaining custom fields with scalar values (ratings, URLs, names, booleans, dates, short strings), ask: "Should these fields appear in the post's frontmatter YAML?
[list: field_name (sample value: "..."), ...]"
Group fields by prefix and offer wildcard rules. For example, if you see review_rating,
review_platform, review_source, offer:
"review_*": { mode: "frontmatter" },
Ask the user to confirm the wildcard or ask for individual rules if some fields in the group should be skipped.
Step D4 — unknownFallback
Ask: "For any custom fields not explicitly configured, what should happen by default?"
Ask: "How should images be handled?"
Recommend "none" for the first test run — it's faster and lets you verify content
quality before committing to a potentially long image download. Switch to "all" once
you're happy with the markdown output.
Options:
"all") — downloads every image URL referenced anywhere in posts
(featured images + inline images in content). Saves to images/ in the output dir.
Takes significant time for large sites. Requires the old site to still be online."attached") — only featured/attached images, not inline"none") — no downloading; image URLs stay as-is in markdown.
Use if the old CDN will stay online, or if you'll handle images separately."none") — strip image references from outputIf downloading, ask: "Should I generate an image-map.json mapping old URLs to new local
paths?" (Useful for updating references in other systems like a database or CMS.)
Note: If the old site requires authentication or blocks hotlinking, downloading will fail with 403 errors. Mention this risk for large sites.
Ask: "Do you need redirect rules so old WordPress URLs automatically forward to new ones?"
If yes, ask deployment platform:
_redirects filevercel.jsonredirects.js.htaccessnginx-redirects.confSkip this group if the generated config has no shortcodeHandlers or blockHandlers stubs.
Shortcodes: Show the user every detected shortcode. For each, determine:
data-field, type, field, checkbox, contenteditable, your, placeholder,
form numbers like [MV2548] or [SP-41 (Permanent)], bracket text like [your county].
All false positives → "skip".Your, First, Date, Restaurant,
Specific) or prepositions (at, dot). Any detected shortcode name that looks like a
capitalised common word or a JSON fragment is a false positive. Set all to "skip"."skip", "html" (output raw HTML), or write a
custom handler function.Gutenberg blocks: Show the user every detected block with counts. For each:
elementor/*, divi/*, wpbakery/*, beaver-builder/*,
bricks/*) — visual layout data with no textual meaning.
Recommend: wildcard skip "elementor/*": "skip".kadence/rowlayout, kadence/column, kadence/iconlist) → "fallback"
This recurses into inner blocks where the real content lives. Using "skip" on these
produces empty page bodies. This is the most common cause of blank pages on Kadence sites.kadence/listitem, kadence/pane, kadence/tab) → "fallback"kadence/advancedbtn, kadence/icon, kadence/spacer) → "skip"wpforms/form-selector, gravityforms/form) → "skip"cp-timeline/*) → "fallback" if they contain text"html" (output raw HTML via turndown)Also set shortcodes.unknownFallback: "skip" if the site has many unknown shortcodes;
otherwise the output will be cluttered with raw shortcode strings.
After all groups are answered, edit input/wetm.config.<slug>.js to apply every change.
Work through each section systematically:
output.format — "mdx", "md", or "auto"output.dir — the user's chosen output pathpostTypes — enabled/disabled with folder names per user's answersseo section — keep or remove based on Group Cplugins.enabled — include/exclude based on detected plugins and user choicestaxonomies.enabled — include all real taxonomies; remove plugin-internal ones.
Always exclude: nav_menu (WordPress navigation menus), wp_theme (theme taxonomy),
wpcode_location, wpcode_tags, wpcode_type (WP Code Snippets internals).
These appear in the "Custom taxonomies detected" output but produce useless data files.taxonomies.emit.astroCollections: true if using Astro with custom taxonomiescontentFields — add entries for HTML meta fields (from Step D2)meta.rules — uncomment and configure based on Step D3; add wildcardsmeta.unknownFallback — set per Step D4meta.deny — ensure noise keys are listedshortcodes.unknownFallback: "skip" + shortcodes.handlers — apply Group G answersblocks.handlers — apply Group G block answersimages.save — apply Group E choiceimages.emitImageMap — set per user choice in Group Elinks.redirects.format — apply Group F choicelinks.redirects.emit — set to false if user skipped redirectsAlso check: if the user's site has frontmatter fields that need renaming, add to
frontmatter.aliases. If they need computed fields (e.g. extracting reviewer name from
title), add to frontmatter.custom.
Show the user the final config before converting. Point out the key decisions. Ask: "Anything else you'd like to adjust before we run the conversion?"
node app.js --config input/wetm.config.<slug>.js
Watch for errors in the output:
Cannot find module — a plugin file path is wrongTypeError: ... is not a function — a hook or template function has a syntax errorpost_id collision — duplicate posts (harmless, deduplicated automatically)EACCES / EPERM — output directory permission issueIf a syntax error occurs in the config, read and fix the specific line. Common issues:
contentFieldsRe-run after each fix.
Read these files:
output/<slug>/migration-report.txt ← human-readable summary
output/<slug>/migration-report.json ← machine-readable for counting
Extract from the report:
3A — Post count check:
For each enabled post type:
// N posts comment in config)statuses: ["publish"]. Draft/private
posts are expected to be absent unless you added them to statuses.If written count is far below input count (not explained by draft/private posts):
enabled: falseposts.filter function is too restrictivefuture, pending)3B — Content quality check:
Read 3–5 sample output files from each custom post type. Check:
<p>text</p> → indicates missing HTML→MD conversion)[shortcode attr="value"] → add handler{expression} in MDX that shouldn't be there (wetm escapes these, but custom
template functions may bypass this) or local path /year/month/slug/3C — Meta coverage check:
If the migration report shows many meta keys going to "skipped" and the user expected
them in frontmatter, add explicit rules or change unknownFallback.
If keys are going to "complex" (MDX export const blocks) but the user expected them as
simple frontmatter values, check if the raw values are longer than maxStringLength
(default 200). Set frontmatter.maxStringLength higher, or add an explicit rule:
"long_key": { mode: "frontmatter" },
3D — Block and shortcode coverage:
If many blocks fell through to "generic HTML fallback", show the user the block names and offer to add skip handlers or custom converters.
If unknown shortcodes are listed, add them to shortcodes.handlers as "skip" or
write custom handlers.
For each issue, fix the config and re-run. The most common fixes:
| Issue | Config fix |
|---|---|
| Post body is empty (content in meta) | Add contentFields entry with HTML template |
| Field missing from frontmatter | meta.rules: { "key": { mode: "frontmatter" } } |
| Field in frontmatter as raw HTML | Move to contentFields, add "skip" rule |
| Shortcodes cluttering body | shortcodes.handlers: { "name": "skip" } |
| Page builder blocks producing garbage | blocks.handlers: { "elementor/*": "skip" } |
| Post type count too low | Add "draft" or "private" to posts.statuses |
| Taxonomy terms missing | Add taxonomy to taxonomies.enabled |
| Images failing (403/timeout) | Set images.save: "none", handle manually |
MDX build errors from {} in content | Add escapeForMdx call in contentFields template |
| Internal links still pointing to old site | Verify site.url matches old WordPress URL exactly |
After each round of fixes, re-run conversion and re-verify until counts match and sample files look correct.
Once verification passes, report:
Migration complete!
Posts migrated:
post: X written (Y skipped as drafts)
page: X written
[custom-type]: X written
...
Output: output/<slug>/
Config: input/wetm.config.<slug>.js
Things to review manually:
- N blocks used generic HTML fallback: [block-name (count), ...]
→ consider adding skip or custom handlers if output looks wrong
- N shortcodes were skipped: [names]
- Images: N downloaded, M failed
→ failed images listed in migration-report.json under imageFails
Next steps:
1. Spot-check output/<slug>/ — open a few posts and verify content looks right
2. Copy output/<slug>/ into your [framework] project's content directory
3. [If Astro] Define content collections in src/content/config.ts for each post type
4. [If MDX] Run your framework's build — MDX compilation catches any syntax issues
5. [If redirects enabled] Deploy the [format] redirect file so old WordPress URLs forward
6. Test internal links — verify they resolve in the new site's routing
Very common with custom post types built on ACF. The developer stored all content in custom fields rather than the standard WordPress editor. Signs:
post_content is empty or has a placeholderreview_content, guide_body, description_html have HTML valuesFix: Add each content-bearing field to contentFields with an HTML→Markdown template.
Also set those keys to "skip" in meta.rules.
ACF's WYSIWYG (rich text) fields produce HTML identical to standard WordPress content. Use the HTML→Markdown template from Step D2.
ACF repeater fields are stored as PHP-serialized arrays in meta. When decoded, they
become nested objects. wetm auto-classifies them as MDX export const blocks. If you
need them as frontmatter (e.g. an array of tags), add an explicit rule:
"my_repeater": { mode: "complex" }, // export const block (default for nested objects)
Sites with complex HTML or form builders often produce values in brackets that the parser picks up as shortcodes. Common false positives:
[MV2548], [SP-41 (Permanent)], [DMV-100][your name], [your county], [field name][data-field], [type], [checkbox]All → "skip" in shortcodes.handlers.
Each SEO plugin produces dozens of meta keys handled by the built-in plugin packs. Do NOT add explicit rules for these — the plugin packs consume them first.
_yoast_wpseo_*, yoast_wpseo_*rank_math_*_seopress_*_aioseop_*Bricks stores visual layout as JSON in _bricks_page_content_2. This is template/layout
data, not text content. Pages built entirely in Bricks will have empty or minimal
post_content. If content is missing, check if it lives in ACF fields or other meta.
Kadence is a popular WordPress theme/page builder that uses Gutenberg blocks. Unlike
Elementor/Bricks, Kadence content IS stored in post_content as Gutenberg blocks — but
all real content is wrapped in container blocks (kadence/rowlayout > kadence/column).
Critical: You MUST set container blocks to "fallback" (recurse into inner blocks),
NOT "skip" (which drops everything) or leave them unhandled (same as skip for unknown
blocks when unknownFallback: "skip").
Without "fallback" on containers, every Kadence page produces an empty body.
Standard Kadence block config:
blocks: {
handlers: {
'kadence/rowlayout': 'fallback',
'kadence/column': 'fallback',
'kadence/iconlist': 'fallback',
'kadence/listitem': 'fallback',
'kadence/pane': 'fallback',
'kadence/tab': 'fallback',
'kadence/advancedbtn': 'skip',
'kadence/icon': 'skip',
'kadence/spacer': 'skip',
},
},
Kadence also injects per-page CSS/JS into meta — always skip these:
meta: {
rules: {
'_kad_blocks_custom_css': { mode: 'skip' },
'_kad_blocks_head_custom_js': { mode: 'skip' },
'_kad_blocks_body_custom_js': { mode: 'skip' },
'_kad_blocks_footer_custom_js': { mode: 'skip' },
'kt_blocks_editor_width': { mode: 'skip' },
},
},
Some plugins (gallery managers, repeaters, complex ACF fields) store data as:
a:3:{i:0;s:4:"val1";i:1;s:4:"val2";i:2;s:4:"val3";}
wetm decodes these automatically. Simple arrays of scalars → frontmatter as YAML arrays.
Nested objects → MDX export const blocks.
Products have many meta keys managed by the WooCommerce plugin pack. Key decisions:
woocommerce in plugins.enabledshop_order, shop_coupon, shop_order_refundproduct enabled with folder: "products"product_variation type: disable unless you want individual variation pagesAdd these to meta.deny (or rely on the default deny list):
deny: [
"_edit_lock", "_edit_last",
"_wp_trash_meta_time", "_wp_trash_meta_status",
"_pingme", "_encloseme",
"_oembed_*", // cached oEmbed data
"_wp_page_template", // layout setting
"_thumbnail_id", // handled as coverImage automatically
],
When different post types use the same meta key names differently, use per-type rules:
meta: {
rules: {
"content_body": "skip", // global: skip for everyone
},
perType: {
"guide": {
"content_body": { mode: "frontmatter" }, // but keep it for guides
},
},
}
format: "mdx" and taxonomies.emit.astroCollections: truesrc/content/<post-type>/ in your Astro projectsrc/content/config.ts for each collectionexport const blocks in MDX are importable from Astro pages directlyformat: "mdx" with links.redirects.format: "next"next-mdx-remote or @next/mdx to process MDX contentredirects.js from wetm can be merged into next.config.jsformat: "md" (Hugo uses its own template system, not MDX)posts.dateFolders: "year" if your Hugo theme expects date-based URLslinks.redirects.format: "apache" or "netlify" depending on hostingformat: "md" or "auto"links.redirects.format: "netlify" for Netlify deploymentLeaving meta.unknownFallback as null (auto) on a messy export
Every plugin's internal key gets auto-classified and bleeds into frontmatter.
Recommendation: always set to "skip" and add explicit rules for fields you want.
Not using contentFields for HTML meta fields
Raw HTML string ends up in frontmatter. Post body is empty. Users see a blank page.
Always check: does this post type have content in meta fields?
Keeping seopress_404 enabled
Results in 1000+ files full of redirect tracking records — not real content.
Always: seopress_404: { enabled: false }.
Wildcard meta rule too broad
"_*": { mode: "frontmatter" } catches every private key including plugin internals.
Always be specific: "review_*", not "*".
Not checking for typos in meta key names
Some sites have typos in their ACF field names (the field was created with a typo and
never fixed). Check the detected meta keys for near-duplicates like reviewer_* and
reveiw_*. Add rules for both.
Image download failures without warning
The old site might block hotlinking, be offline, or require auth cookies.
Check migration-report.json for imageFails. If count is high, switch to "none".
MDX syntax errors from content with curly braces
Body content containing {value} or ${expr} breaks MDX compilation.
wetm's escapeForMdx() handles the standard body, but custom contentFields templates
that produce {} characters bypass this. Escape them explicitly in your template:
text.replace(/\{/g, '\\{').replace(/\}/g, '\\}')
Internal links still resolving to old WordPress URLs
Ensure site.url in the config exactly matches the WordPress site URL (no trailing slash,
same protocol). This is used to identify which links are internal before rewriting.
Taxonomy slugs with hyphens causing JS property access issues
In the config, use string keys with quotes: "reviewer-state", not .
| Goal | Config change |
|---|---|
| Enable a content post type | postTypes: { "type": { enabled: true, folder: "folder" } } |
| Disable a plugin-internal type | postTypes: { "acf-field": { enabled: false } } |
| Field → frontmatter | meta.rules: { "field": { mode: "frontmatter" } } |
| Field group wildcard → frontmatter | meta.rules: { "review_*": { mode: "frontmatter" } } |
| HTML field → post body | Add to contentFields with HTML template; set rule to "skip" |
| Rename a frontmatter key | meta.rules: { "old_key": { mode: "frontmatter", alias: "newKey" } } |
| Computed frontmatter field | frontmatter.custom: { key: (post) => computedValue } |
| Skip a noisy meta key | meta.rules: { "key": "skip" } |
| Skip a shortcode | shortcodes.handlers: { "name": "skip" } |
| Skip a block type | blocks.handlers: { "namespace/*": "skip" } |
| Rename taxonomy in frontmatter | taxonomies.aliases: { "old-name": "newName" } |
| Include draft posts | posts.statuses: ["publish", "draft"] |
| Date-organized folders | posts.dateFolders: "year" or "year-month" |
| Astro content collections | taxonomies.emit.astroCollections: true |
| Redirect format | links.redirects.format: "netlify" (or vercel/next/apache/nginx) |
| Per-type frontmatter | frontmatter.perType: { "type": { fields: [...], custom: {...} } } |
| Per-type meta rules | meta.perType: { "type": { "key": { mode: "frontmatter" } } } |
| Transform a field value | meta.rules: { "price": { mode: "frontmatter", transform: (v) => Number(v) } } |
| Nested frontmatter key |
Source: enact-on/wp2md — distributed by TomeVault.
reviewer-stateNot deleting output before re-running after config changes Existing output files are intentionally not overwritten — this allows resuming interrupted runs. After changing the config (e.g. enabling Kadence fallback), delete the output directory and re-run so all posts are regenerated with the new settings:
rm -rf output/<slug>/
node app.js --config input/wetm.config.<slug>.js
Exception: image downloads. If images are the only change, you can re-run without clearing output — missing images will be downloaded and existing ones skipped.
meta.rules: { "seo_title": { mode: "frontmatter", alias: "seo.title" } } |