Like WordPress, Payload is fully open-source. However, it's written in TypeScript instead of PHP and is configured through code rather than a GUI. This approach speeds up setup and extension.
As a headless CMS, Payload allows you to choose your frontend framework. While Payload 3.0 is Next.js native, you can also use Vue, Astro, or any other framework. Payload supports extensive features, including custom file storage, database options, multiple collections, and authentication, offering the power of a backend framework.
Keep in mind, Payload is not a no-code or low-code tool; it's configured through code, providing immense flexibility.
WordPress | Payload |
---|---|
Pages | Pages Collection |
Posts (with ACF Plugin) | Posts Collection |
Post Authors | Users Collection |
Post Categories | Post Categories Collection |
Gutenberg Blocks | Payload Blocks, added to pages & posts collection |
Media | Media collection, uploads enabled |
Menus | Header / Footer Global |
WordPress uses Pages and Posts with Gutenberg layout blocks. In Payload, we'll migrate these to individual “Collections”: Pages and Posts. Payload keeps it very simple. Collections are structured data groups, like pages, blog posts, or users. Globals are single-document collections.
We'll use the Users Collection for post authors, create a Blog Post Categories Collection, and utilize blocks similar to Gutenberg blocks for layout. We'll also create a Media Collection with uploads enabled. For navigation menus, we'll create header and footer globals.
Pages and Posts in WordPress will be migrated to Payload Collections. We'll create schemas for blocks like Cover, Paragraph (RichText), and Image blocks, and a block for Recent Blog Posts. Each block will have a schema defining its fields.
For example, a Cover block might have fields for heading and subheading, while an Image block would have an upload field linking to the Media Collection.
In addition to a block schema, we need to create a React component to display the block data. Since we're using NextJS 15, those components will be server components by default, but you can nest your own client components in them.
Remember that Payload is a headless CMS, so it provides the data, but you have to build the frontend blocks yourself. In part two of this tutorial, we'll explore how to do just that.
For this tutorial, we’ll focus on the creation of the schemas for the blocks.
WordPress | Payload |
---|---|
Cover Block | Cover Block |
Paragraph Block | Paragraph Block |
Image Block | Image Block |
Latest Blog Articles Block | |
Header Block | |
Footer Block |
We will start with three blocks in our “existing” WordPress project: Cover, Paragraph, and Image. For each, we’ll create corresponding blocks within Payload.
In WordPress, we might have an automated page that just shows our recent blog articles. What we do in Payload is create one of those blocks that fetches all recent posts, and then we can put that block anywhere we want.
In addition to that, we’ll have the Header block and the Footer block which will live globally on the entire page.
One recommendation I give is to manually migrate standard Pages such as home, about, etc. That will give you the opportunity to restructure and clean-up your project.
Since the majority of a website’s pages are auto-generated from posts, products, etc., you will save 90% of the effort. And the remaining 10% is worth doing manually.
Install Payload in your local terminal:
npx create-payload-app@beta
wp-migration
).Once your dependencies are installed, you can cd into your folder at cd wp-migration
and open up VS Code (code .
)
Once you’re in VS Code, you will be able to npm run dev
to start the development server, and opening localhost:3000/admin
in your browser should prompt you to create your first user in Payload.
And now we have an admin panel!
We’ll get started by creating Pages.ts under collections.
Then we’ll create BlogPosts.ts, which follows the same structure but includes authors.
And now we’ll create BlogCategories.ts.
Before we continue, we need to add them to our Payload config ('payload.config.ts').
Your Payload dashboard should now include the following collections:
After we’ve successfully created our Collections, we’ll continue with the creation of our blocks.
Create a blocks
folder inside the src
directory and then create the block schemas. (Note: You can in theory put these wherever you want; we place them into their own subdirectory for less chaos).
Create a cover
folder file within the blocks
folder, and a schema.ts
file within the cover folder.
To continue, we'll paste in the other blocks, including the Image block (blocks > image > schema.ts), Richtext block (blocks > richtext > schema.ts), and recentBlogPosts block ( (blocks > recentBlogPosts > schema.ts).
For images, it’s a simple schema with one field with type: upload. This is similar to a relationship field but you can actually upload a new media element within this field—it links to a media collection where we have uploads enabled.
Then we have our richtext block, which is just the content field.
And finally, the recentBlogPosts block; this is doesn’t have any fields at all because by default we just want to fetch the most recent blog posts. Optionally, you could add a special field that just specifies which posts the component should surface.
We’re now going to update Pages.ts and Posts.ts to include our new blocks.
Pages.ts:
Under BlogPosts.ts, we do the same:
Now if we navigate back to our admin panel and create a new page, we should see all of our blocks added:
Next, we’re going to migrate media from your existing Wordpress website:
This involves going into your WordPress Dashboard, and navigating to: Tools > Export > Media > Download Export File.
Save as media.xml
locally, and paste it into your VS Code project folder.
To migrate your media, we’re going to use a custom script (below) that accesses the local Payload API and creates media elements for us.
Note: What the exported XML contains is links to our resources in Wordpress—so it’s important that your site is still online and reachable so we can just fetch the data and upload it to Payload!
You will need the below mediaMigration.js and migrationWrapper.js files inside your project.
mediaMigration.js:
migrationWrapper.js:
You should install the following dependencies now. Although migrating media requires just XMLParser
and Mime
, the dependencies below are necessary for ultimately all content we’ll end up migrating over to Payload.
npm install cheerio
npm i @wordpress/block-serialization-default-parser
npm i mime
npm i fast-xml-parser
npm i lexical
npm i @lexical/html
npm install --save @lexical/headless
npm i jsdom
For details on this part of the project, especially if you need to manipulate the migration script for any particular use case, click here.
Now we’re going to call the migration wrapper by running node migrationWrapper.js
, and begin to see the script migrating the data.
Upon restarting your server (npm run dev
) and navigating to your admin panel's media section, you should now see the migrated media appear.
We have just two pages in our sample WordPress project: Home and Blog.
In Payload, we’re going to:
index
. Later on, we’ll replace this with an empty string.Next, we'll do the Blog page:
blog
.Remember that this block could be on any page, including the Home page, to list all blog posts.
Finally, we’ll migrate all of our Wordpress blog posts that use ACF (Advanced Custom Fields). Although in our example we only have two blog posts (in which case you’d just move those manually), it might be more common to have dozens if not hundreds or more.
This migration will be a bit more complex than the media migration.
We’ll use the following migration.js script to assist with moving our content.
First, we’ll paste in our migration script (below). We’ll call it migration.js.
Next, to ensure the Wordpress users migrate correctly to Payload, we need to adjust the Users.ts collection and make the following changes:
This change will allow the WordPress username to migrate and map accordingly to the slug. Although you can do this manually, this collection will still require a slug.
In the next step, we’ll create a categories section in our BlogPosts.ts file.
Next, we’ll create the Categories we need in Payload. According to our sample Wordpress project, they are named, “Knowledge Base” and “News.”
We’ll simply replicate each by clicking, “Blog Categories” and “Create new Blog Category” in the Payload admin panel.
Upon creation, Payload generates an ID for each.
In our migration.js script, we need to ensure each ID of a category is accounted for.
Example:
Finally, we need to export the blog posts from Wordpress: Tools > Export > Posts > Download Export File. Save as 'wp-data.xml' locally, and paste it into your project.
As mentioned earlier, before you run the migration script, ensure you have all the necessary dependencies installed!
For details on how the migration script and functions work, please consult the full YouTube video.
Run node migrationWrapper.js
That might take a bit of time depending on how many blog posts you have. Once it’s complete, navigate to your admin panel to confirm the blog posts and content — including images — have been migrated accordingly.
In a follow up tutorial, we’ll show you how to complete this project by building a frontend with Next.js.