Operand

no leg, asea.

gram: page

> ./2025-07-07.wordpress.dump.md

Lenses
(coming soon!)


---
name: Dump your WordPress.
composer: c4lliope
summary: Phase one in ending a long and arduous relationship.
labels:
  - upgrade
  - wordpress
  - blog
---

So, you're managing WordPress?  
That's a popular thing to be doing!  
How long have you had this running?  

Oh, how rude of me to ask;
probably much longer than you'd like to admit.

Here is how you can quickly back up a WordPress domain -
focusing on the content of the pages,
rather than the themes & style.

Throughout this guide we'll be making use of
the [WordPress REST API][api].
Our code samples are prepared using [Nushell][nu].

[api]: https://developer.wordpress.org/rest-api/
[nu]: https://nushell.sh

- - -

## Discover the API.

WordPress relies on a [discovery] mechanism
to communicate each site's public API to one another.

[discovery]: https://developer.wordpress.org/rest-api/using-the-rest-api/discovery/

In this example, we use [wordpress.org][wp] -
the same code applies, in theory, to any wordpress domain.

[wp]: https://wordpress.org

```nu
def "wp base" [domain: string] {
  http get --full $domain |
  get headers.response |
  where name == link |
  get value.0 |
  parse '<{link}>; rel="https://api.w.org/"' |
  get link.0
}

let base = wp base https://wordpress.org | tee { print }
```

Solid! We have a base URL.

Using the [reference], we see that `/wp/v2/posts`
is the path we need to use.

Depending on how your WordPress has been managed,
other nice places to look are:

* `/wp/v2/pages`
* `/wp/v2/tags`
* `/wp/v2/categories`
* `/wp/v2/comments`
* `/wp/v2/media`
* `/wp/v2/users`

Again, see the [reference] for a more thorough appraisal.

[reference]: https://developer.wordpress.org/rest-api/reference/

To help us progress here, let's make a couple quick helper functions:

```nu
def "wp dump" [base: string, path: string, --form (-f): string = yml] {
  let node = [. ($base | url parse | get host) $"($path).($form)" ] |
    path join | path expand
  mkdir ($node | path dirname)
  try { http get ([ $base wp v2 $path ] | path join) | save -f $node }
}
```

Ready? Here goes.

```
let base = wp base 'https://wordpress.org'

timeit { [
 posts pages comments media users
 categories tags taxonomies types statuses
 settings themes search plugins
 block-types blocks block-renderer block-directory/search
] | each {|label| wp dump $base $label } }

# for a colleague's domain:
# 21sec 895ms 964µs 817ns
```

Of course, if you prefer a non-yml dump,
pass in one of [these extensions][nu:save] under `-f`:

[nu:save]: https://www.nushell.sh/commands/docs/save.html

```nu
➜ scope commands
    | where name starts-with "to "
    | insert extension { get name | str replace -r "^to " "" | $"*.($in)" }
    | select extension name
    | rename extension command

╭────┬────────────┬─────────────╮
│  # │ extension  │   command   │
├────┼────────────┼─────────────┤
│  0 │ *.csv      │ to csv      │
│  1 │ *.html     │ to html     │
│  2 │ *.json     │ to json     │
│  3 │ *.md       │ to md       │
│  4 │ *.msgpack  │ to msgpack  │
│  5 │ *.msgpackz │ to msgpackz │
│  6 │ *.nuon     │ to nuon     │
│  7 │ *.text     │ to text     │
│  8 │ *.toml     │ to toml     │
│  9 │ *.tsv      │ to tsv      │
│ 10 │ *.xml      │ to xml      │
│ 11 │ *.yaml     │ to yaml     │
│ 12 │ *.yml      │ to yml      │
╰────┴────────────┴─────────────╯
```

For completeness, here is a multi-format dump:

```nu
let base = wp base 'https://wordpress.org'

let labels = [
 posts pages comments media users
 categories tags taxonomies types statuses
 settings themes search plugins
 block-types blocks block-renderer block-directory/search
]

let forms = [ csv json yml ]

timeit { $labels | each {|l| $forms | each {|f|
  wp dump -f $f $base $l
} } }
```

Yeah! Such a nice backup already.

```nu
➜ tree wordpress.org
wordpress.org
├── block-directory
├── blocks.csv
├── blocks.json
├── blocks.yaml
├── blocks.yml
├── categories.json
├── categories.yaml
├── categories.yml
├── comments.json
├── comments.yaml
├── comments.yml
├── media.json
├── media.yaml
├── media.yml
├── pages.json
├── pages.yaml
├── pages.yml
├── posts.json
├── posts.yaml
├── posts.yml
├── search.json
├── search.yaml
├── search.yml
├── statuses.json
├── statuses.yaml
├── statuses.yml
├── tags.json
├── tags.yaml
├── tags.yml
├── taxonomies.json
├── taxonomies.yaml
├── taxonomies.yml
├── types.json
├── types.yaml
├── types.yml
├── users.json
├── users.yaml
└── users.yml
```

If you have a local copy of `zip`, yay!

```nu
zip -r wordpress.zip wordpress.org/*
```

Since I'm on NixOS I'm going to use a small prefix here:

```nu
nix-shell -p zip --run 'zip -r wordpress.zip wordpress.org/*'
```

Now, I'm going through this process to help a colleague,
so this is a good place to email off the ZIP file,
to see if she has any issues with how the records came back.

I'm eager to press ahead and look at a few newer CMS options for her!
A couple ones I already have my eye on:

- [Beacon](https://hexdocs.pm/beacon/your-first-site.html)
- [Solid](https://www.solidjs.com/) & [MDX](https://mdxjs.com/)

Plus, the [Awesome SelfHosted page](https://github.com/awesome-selfhosted/awesome-selfhosted?tab=readme-ov-file#content-management-systems-cms)
has 40 more options for a CMS upgrade - of course, including WordPress.