---
description: Move WordPress uploads to Cloudflare R2, import a WXR export into Studio, and connect a public media domain.
---

# Migrate from WordPress to Studio

> **Audience:** the WordPress site owner or operator setting up ZeroPress Studio

This walkthrough follows the path used for a ZeroPress dogfooding site:
copy WordPress uploads to R2, export the content, install Studio, import the
WXR file, and connect a public media domain. It is a practical starting point
that will be refined as more migrations are tried.

The migration has two parts. **AWS CLI copies the files; Studio imports the
content and media records.** A successful WXR import does not confirm that the
corresponding files exist in R2.

## Before you begin

You need access to the WordPress dashboard and server, a Cloudflare account,
and a domain you can connect to the media bucket. Keep a backup of WordPress
and its uploads, and keep the existing site available while checking the new
one. Start with a fresh Studio installation for this single WordPress source.

The examples use these values. Replace them with your own:

| Purpose | Example |
| --- | --- |
| WordPress directory | `/path/to/wordpress` |
| Original uploads URL prefix | `https://blog.example.com/wp-content/uploads/` |
| R2 bucket | `zeropress-public-media` |
| Imported object prefix | `imported/` |
| Public media address | `https://media.example.com` |

This guide assumes the usual WordPress `wp-content/uploads/` layout. If your
media is offloaded elsewhere or uses a custom URL prefix, identify its actual
location and review the resolved prefix during import.

## 1. Create the R2 bucket and access keys

In Cloudflare, create the bucket that will hold the site's public media. The
dogfooding example uses `zeropress-public-media`. This must be the same bucket
that you later connect to Studio through its `MEDIA_BUCKET` binding.

Create R2 S3 credentials with **Object Read & Write** permission scoped to
that bucket. Save the **Access Key ID**, **Secret Access Key**, and S3 API
endpoint shown in Cloudflare. Use the R2 S3 credentials for AWS CLI, rather
than a general Cloudflare API token. See [R2 authentication](https://developers.cloudflare.com/r2/api/tokens/).

Leave public access disabled until the files are ready. This bucket is for
public media; keep database backups, WXR exports, and credentials elsewhere.

## 2. Copy WordPress uploads with AWS CLI

Install [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)
on the existing WordPress server using the instructions for its operating
system. Confirm the installation:

```sh
aws --version
```

Create a dedicated profile:

```sh
aws configure --profile zeropress-r2
```

Enter the R2 Access Key ID and Secret Access Key when prompted. Set the default
region to `auto` and output format to `json`. Cloudflare documents this setup in
its [AWS CLI guide](https://developers.cloudflare.com/r2/examples/aws/aws-cli/).

Set the endpoint to the value from your bucket's Cloudflare settings. The usual
form is below; a bucket with a specific jurisdiction may use a different host.

```sh
ZP_R2_ENDPOINT="https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com"
cd /path/to/wordpress/wp-content
```

First review the copy plan:

```sh
aws s3 sync uploads/ s3://zeropress-public-media/imported/ \
  --endpoint-url "$ZP_R2_ENDPOINT" \
  --profile zeropress-r2 \
  --region auto \
  --dryrun
```

Then copy the files:

```sh
aws s3 sync uploads/ s3://zeropress-public-media/imported/ \
  --endpoint-url "$ZP_R2_ENDPOINT" \
  --profile zeropress-r2 \
  --region auto
```

This is the dogfooding command `aws s3 sync uploads
s3://zeropress-public-media/imported` with the R2 connection made explicit.
Keep `--endpoint-url` unless you have configured an equivalent R2 endpoint;
the `s3://` destination alone does not select Cloudflare R2.

The directory contents map directly into `imported/`:

| WordPress file | R2 object key |
| --- | --- |
| `wp-content/uploads/2024/06/photo.jpg` | `imported/2024/06/photo.jpg` |
| `wp-content/uploads/2024/06/photo-300x200.jpg` | `imported/2024/06/photo-300x200.jpg` |

Keep filenames, subdirectories, and generated image sizes intact. Do not add
another `uploads/` level under `imported/`, or copy all of `wp-content/`.

`sync` copies new or changed files and can overwrite matching destination
objects. The commands above omit `--delete`, so destination-only files are
retained. If the copy is interrupted, inspect the result and rerun the same
command. See the [AWS sync reference](https://docs.aws.amazon.com/cli/latest/reference/s3/sync.html).

## 3. Export the WordPress content

In WordPress, open **Tools → Export**, select **All content**, and choose
**Download Export File**. Save the resulting WXR XML file. See the
[WordPress export guide](https://wordpress.org/documentation/article/tools-export-screen/).

Studio accepts WXR 1.2. The export carries content and attachment metadata;
the uploads copied in the previous step provide the actual media bytes.
WordPress themes, plugins, and their settings are not recreated by this import.

## 4. Install Studio with the same media bucket

Follow [Quick Start](../index.md) and [Worker Secrets](../worker-secrets/index.md)
to install Studio and complete the first administrator sign-in.

When preparing the Worker, bind `MEDIA_BUCKET` to the bucket used in step 1.
The access keys configured on the old WordPress server are for the copy
operation; Studio accesses R2 through its Worker binding.

If you need to migrate reader comments, prepare ZeroPress Edge and enable its
Studio integration before importing. With Edge disabled or unavailable, the
preflight reports skipped comments; other supported content can still be
imported.

## 5. Import the WXR file into Studio

Sign in as an administrator and open **Import from WordPress** at
`/import/wordpress`.

1. Choose the WXR XML file and let Studio prepare the preflight summary.
2. Under **Media storage strategy**, select **Use Studio R2**. The alternative,
   **Keep existing storage**, leaves media dependent on the old public URLs.
3. Review **WordPress uploads URL prefix** and the resolved source prefix.
   For this example it is `https://blog.example.com/wp-content/uploads/`.
   Enter the original prefix explicitly if it cannot be inferred. This is
   the source URL, not the new R2 media address.
4. Review the content counts, warnings, and HTML editing compatibility.
   Content that needs the HTML source editor is still imported; source mode
   is not an import failure.
5. Review the proposed site settings and permalink structure. Set the
   canonical site URL to the intended public website origin, not the Studio
   admin address or media domain. Check the inferred routes against existing
   WordPress URLs.
6. Confirm the reviewed plan and keep the page open until the import finishes.
   Inspect the **Created**, **Updated**, **Unchanged**, **Skipped**, and
   **Failed** results, including individual row failures.

With **Use Studio R2**, attachment locations are registered under
`imported/`, and matching body URLs become managed media references. Studio
does not copy files or check whether those objects exist. The public media
address configured below resolves those references when Preview Data is
generated.

Retrying the same source export is supported. Matching imported records can
be updated, including changes made since the last import, so review the scope
before rerunning. This workflow does not merge unrelated WordPress sites.

## 6. Connect the bucket's public media domain

In the R2 bucket settings, open **Custom Domains**, add `media.example.com`,
review the DNS change, and connect it. The domain's zone must be in the same
Cloudflare account as the bucket. Wait for the connection to become active.
See [Cloudflare's custom domain instructions](https://developers.cloudflare.com/r2/buckets/public-buckets/#connect-a-bucket-to-a-custom-domain).

Check a real copied object, for example:

```text
https://media.example.com/imported/2024/06/photo.jpg
```

Use a filename that exists in your bucket. Opening the domain root is not a
useful file check: R2 does not provide a public directory listing there. A
custom domain serves the bucket root, so `/imported/` remains part of each
object's URL.

## 7. Set the media address in Studio

Open **Site settings → Media** at `/settings/site/media` and save:

| Setting | Value for a direct R2 custom domain |
| --- | --- |
| Media address | `https://media.example.com` |
| Delivery mode | **Standard media URLs** |

Enter the origin only, without `/imported/`, a filename, query, or fragment.
An object stored at `imported/2024/06/photo.jpg` will then be delivered at
`https://media.example.com/imported/2024/06/photo.jpg`.

**ZeroPress media domain** is a separate delivery option for a service that
supports ZeroPress image variants. Connecting an ordinary R2 custom domain
does not provide that capability; use **Standard media URLs** for this journey.

Saving the address changes delivery settings, not the files in R2. Generate
fresh Preview Data after this step so the public build uses the new address.

## 8. Check the result before switching sites

Review a few representative posts and pages in Studio, then build a preview
of the public site:

- Check body text, author bylines, categories, tags, page hierarchy, and menus.
- Open featured images, inline images, resized images, and attachment links.
  Confirm that expected migrated media uses the new domain and loads.
- Review source-mode HTML, shortcodes, embeds, and any plugin-dependent
  content that needs manual adaptation.
- Review skipped and failed items. WordPress scheduled posts and other
  unsupported statuses may arrive as drafts; Studio does not schedule them.
- Check comments if Edge was part of the migration, and compare important
  page URLs with WordPress before deciding which redirects are needed.

Open **Publish**, generate and download Preview Data, and use it with a theme
in the separate [ZeroPress Build workflow](https://zeropress.dev/). Studio
does not build or deploy the public site itself.

If WordPress stays active during the trial, plan a final editing pause, sync
new uploads, export the latest WXR, and repeat the reviewed import before the
final build. Account for any edits already made in Studio. Switch the public
site only after checking the output, and keep the original backup available.

## Common points to check

| Symptom | Check |
| --- | --- |
| AWS CLI cannot connect or returns an authorization error | Confirm the R2 endpoint, profile credentials, bucket scope, and object permissions. |
| Import succeeds but an image returns 404 | Check that the object exists at the exact `imported/...` key in Studio's bound bucket. Import success does not verify file transfer. |
| Images still use the old WordPress domain | Check the media strategy and exact original uploads URL prefix; unmatched or external URLs may remain. |
| Images point at the public site's `/imported/` path | Set the Studio Media address, then generate fresh Preview Data and rebuild. |
| A media address with `/imported/` is rejected | Enter only the origin, such as `https://media.example.com`. |
| Comments are skipped | Check the import summary and Edge readiness; import other content first if comments are not yet in scope. |

