Skip to content
liutPublic

About

Imports WordPress posts and approved comments directly from the database into Hugo markdown plus remark42-ready WXR.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

28 Commits

Folders and files

Repository files navigation

wpdb2hugo

Reads WordPress posts and approved comments directly from the database into Hugo markdown and remark42-ready WXR. MySQL and PostgreSQL (via pg4wp) supported; driver selected by DB_TYPE, table prefix by DB_TABLE_PREFIX.

Usage

From the Hugo blog repo root:

# Dry-run: connect, query, plan — write nothing.
go run ../wpdb2hugo --root /path/to/blog --dry-run

# Posts only (markdown files under --target):
go run ../wpdb2hugo --export-posts

# Comments only (WXR XML for remark42 import; written to <root>/comments.xml):
HUGO_BASEURL=https://blog.example.com/ \
  go run ../wpdb2hugo --export-comments --dry-run

# Both posts and comments in one run:
HUGO_BASEURL=https://blog.example.com/ \
  go run ../wpdb2hugo --export-posts --export-comments --dry-run

# Pre-flight check on an existing comments.xml (no DB needed):
HUGO_BASEURL=https://blog.example.com/ \
  go run ../wpdb2hugo --validate-comments

The tool fatals without at least one of --export-posts, --export-comments, or --validate-comments.

.env file

.env.example ships as a template. Copy it to your Hugo blog repo, fill in DB_HOST / DB_USER / DB_NAME / etc., then either source it:

set -a; source .env; set +a; go run ../wpdb2hugo --dry-run

or use forego (go install -v github.com/ddollar/forego@latest) to skip the shell toggle:

forego run go run ../wpdb2hugo --dry-run

.env is git-ignored in both repos. Inline env vars from the Usage section above work the same way.

Behavior

  • Reads <prefix>posts filtered to post_status='publish' AND post_type='post'. <prefix> is DB_TABLE_PREFIX (default wp_). Page entries (post_type='page') are excluded by the same filter.
  • Converts HTML post_content to Markdown via github.com/JohannesKaufmann/html-to-markdown/v2.
  • Writes content/posts/<yyyy>/<mm>/<post_name>.zh.md (yyyy/mm from post_date_gmt UTC, two-digit month padded).
  • Idempotent: existing target files are skipped with a WARN line, never overwritten.
  • post_name is validated against ^[a-zA-Z0-9_-]+$; unsafe slugs are skip-and-WARN'd, not fatal.

remark42 migration

--export-comments writes a WXR XML file consumable by remark42's WordPress importer. --export-posts and --export-comments are independent — runs whichever combination the operator selects.

What gets exported

Only comment_approved='1' comments on post_status='publish' AND post_type='post' rows. Pending, spam, and comments on pages / attachments / drafts are excluded. Pass --include-pages to also pull comments from post_type='page' rows (off by default — matches remark42's default of not attaching comments to page URLs).

Posts whose post_name fails the slug check are dropped with a WARN; their approved comments are not in the output (mirrors the post-pass behavior).

URLs

Each <item>'s <link> is the URL remark42 attaches the comment to. By default this is HUGO_BASEURL + /<--target's last segment>/:year/:month/:slug/ — e.g. --target=content/articles produces /articles/:year/:month/:slug/. Set HUGO_PERMALINK to override the pattern entirely (supports :year, :month, :slug; leading / auto-prepended). Pass --use-wp-url to use the original WordPress permalink (wp_posts.guid) instead — then HUGO_BASEURL is not required.

The WXR file always lands at <root>/comments.xml. An existing file is skipped with a WARN — never overwritten (idempotent).

After export, run remark42's import pointing at <root>/comments.xml:

remark42 import --file comments.xml --provider wordpress --site blog

Or pass --remark42-url https://comments.example.com (with --remark42-site and REMARK42_SECRET in env) to POST the file directly. See umputun/remark42 backend/app/rest/api/migrator.go for the endpoint contract.

Run summary

The post-pass prints html-detected: N/M, would-write: N, skipped (existing): N, warned (unsafe slug): N, shortcode hits: N, and target root: … on stderr. The comment pass adds: comments-exported: N, comments-skipped (existing): N, comments-skipped (unsafe slug): N.

Validating the export

--validate-comments reads <root>/comments.xml and checks each <item><link> against Hugo markdown files under --root/--target. Independent of --export-comments — pre-flight-check a file produced earlier, or combine with --export-comments to validate the just-written file. No DB connection required.

For each <comment> whose parent <item> URL has no matching .md file, a WARN is printed with the count of <comment> entries that will be silently dropped. Stderr summary:

validate: 36 comments hit across 3 content files, 0 comments missed
  • comments hit — <comment> entries whose parent <item> URL matched a .md file
  • content files — unique .md files with at least one URL hit
  • comments missed — <comment> entries whose parent URL had no match (silently dropped by remark42)

Exit codes: 0 when every <comment> hits a content file; 1 when any <comment> misses; 1 (via fatal) on hard errors (file not found, malformed XML, missing content tree). With --use-wp-url, the validator prints validate: skipped (...) and exits 0.

The validator uses the same HUGO_BASEURL / HUGO_PERMALINK / --target as the exporter — they call the same URL-derivation helpers with the same config, so they cannot drift apart.

Connections

DB_TYPE selects the driver (mysql or postgres). All other connection settings come from DB_* env vars — no flag for connection settings.

Var Default Notes
DB_TYPE mysql Driver selector; mysql or postgres
DB_HOST localhost MySQL TCP host / PG Unix-socket dir
DB_PORT driver default (mysql 3306, pg 5432) Empty = driver default
DB_USER (none) Required
DB_NAME (none) Required
DB_PASSWORD "" Local installs need none
DB_TABLE_PREFIX wp_ WordPress default; set explicitly for any other prefix
HUGO_BASEURL (none) Required by --export-comments and --validate-comments unless --use-wp-url is set (fatal if unset)
HUGO_PERMALINK (auto) Consulted by --export-comments and --validate-comments. Defaults to /<--target's last segment>/:year/:month/:slug/; overrides auto derivation
REMARK42_SECRET (none) Only consulted with --remark42-url. Env-only (not a flag) to keep the admin secret out of shell history / ps

The MySQL DSN carries parseTime=true&loc=UTC&charset=utf8mb4&tls=false so date columns scan into time.Time directly and full-Unicode content round-trips. The PostgreSQL DSN keeps sslmode=disable and skips the password= token when empty (a lib/pq parser quirk on MacPorts-local installs). MySQL 8 with caching_sha2_password over plain TCP needs tls=preferred (or a CA cert) — switch to that DSN if you hit "unable to deserialize".

Legacy PGHOST / PGUSER / PGPASSWORD / PGDATABASE / PGPORT env vars, when set without a corresponding DB_HOST, produce a fatal with a clear migration message. When DB_HOST is also set, the legacy vars are warned about and ignored.

About

Imports WordPress posts and approved comments directly from the database into Hugo markdown plus remark42-ready WXR.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages