Skip to content

Latest commit

 

History

28 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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