From WordPress to Strapi and Remix
This site has been around since 2003, and it's been on a lot of platforms over the years.
Recently, I decided to change it up, and move from WordPress, where it's sat for the last 4 years and instead move it to Strapi as a headless CMS and Remix as the frontend.
This post will walk through how I did this from exporting from WordPress into Strapi and then setting up the initial blog in Remix and deploying it to Vercel.
This post will be in four sections:
- Setting up Strapi Posts Content-Type
- Exporting from Wordpress and importing into Strapi
- Setting up Remix app
- Deploying to Vercel
1. Setting up Strapi Posts Content-Type
To start, we have our posts
content type, this is set up with the following fields:
title
: short text, unique, requiredcontent
: rich textslug
: UUID field linked to titleexcerpt
: load textdate
: date field
This is keeping our posts pretty straight forward and we can expand on these later as needed.
Create an API Key in Strapi as well, that has full-admin access for the migration.
You'll also want to set some permissions, go to roles
, and select public
, then go to posts
and set find
and findOne
to allow.
This example doesn't get into hosting Strapi, just adding the content-types and importing and later reading from it, I recommend looking at Railyway for hosting Strapi but there are many many options for where to host it.
2. Exporting from Wordpress and importing into Strapi
For the export, I used the Wordpress JSON api and wrote a script that read each post and then saved it into Strapi.
To start, you'll need to install a few modules:
yarn add strapi-sdk-js date-fns turndown
Now create a file called wp.js
:
import axios from 'axios'; import Strapi from "strapi-sdk-js"; import { format } from 'date-fns'; const TurndownService = require("turndown"); const turndownService = new TurndownService(); const strapi = new Strapi({ url: "https://YOUR-STRAPI-URL, prefix: "/api", }); const token = "YOUR-API-KEY"; strapi.axios.defaults.headers.common["Authorization"] = `Bearer ${token}`; const start = async () => { const { data } = await axios.get('https://YOUR-WP-SITE/wp-json/wp/v2/posts?offset=0&per_page=100'); const posts = await Promise.all(data.map(post => new Promise(async (resolve, reject) => { const { title: { rendered: titleRendered }, slug, content: { rendered: contentRendered }, date, } = post; try{ const postData = { title: titleRendered, content: turndownService.turndown(contentRendered), slug, date: format( new Date(date), 'yyyy-MM-dd'), createdAt: date, }; const created = await strapi.create('posts', postData); resolve(created) }catch(err){ console.log(err.error.details); } }))); console.log(posts.length); } start();
This will process the first 100 posts and save them in strapi, if you have more than 100 posts, change the offset=0
to offset=101
and run for the next 100, and repeat for each 100. Don't do more than 100 posts to a time.
Your posts should now be in your Strapi app.
Inside Strapi, go to permissions and make the posts public.
3. Setting up Remix app
The easiest way to start a Remix app is to create it:
npx create-remix@latest
When it prompts for options, choose basic
and vercel
as the deployment platform, then choose Typescript
.
For other libraries, we're going to keep this simple and all you need is to run:
yarn add marked qs
Which is a markdown to html processor and a querystring processor.
You also want an ENV variable:
STRAPI_API_URL=http://localhost:1337
This will be the URL for strapi, so change it to whatever you've installed it at.
We're going to use two files:
app/routes/blog.tsx
, a new file.app/routes/blog.$slug.tsx
, which is new.
import qs from "qs" import type { LoaderFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { marked } from "marked"; import * as React from "react"; type Post = { title: string; content: string; createdAt: string; updatedAt: string; publishedAt: string; }; type PostData = { id: string; attributes: Post }[]; type PostResponse = { data: PostData; meta: { pagination: { page: number; pageSize: number; pageCount: number; total: number; }; }; }; export const loader: LoaderFunction = async () => { const queryString = qs.stringify({ sort: 'publishedAt:desc', populate: "*", pagination: { start: 0, limit: 100} }); const url = `${STRAPI_API_URL}/api/posts?${queryString}`; const response = await fetch(url); const postResponse = (await response.json()) as PostResponse; return json( postResponse.data.map((post) => ({ ...post, attributes: { ...post.attributes, content: marked(post.attributes.content), }, })) ); }; const Posts: React.FC = () => { const posts = useLoaderData(); return ( <> {posts.map((post) => { const { title, content, createdAt } = post.attributes; const date = new Date(createdAt).toLocaleString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric", }); return ( <article key={post.id}> <Link to={`/blog/${post.slug}`}> <h1>{title}</h1> <time dateTime={createdAt}>{date}</time> <div dangerouslySetInnerHTML={{ __html: content }} /> </Link> </article> ); })} </> ); }; export default Posts;
This is our blog index page, it shows a list of blog posts, clicking any post will load the single post page which we'll make now.
import qs from "qs" import type { LoaderFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { marked } from "marked"; import * as React from "react"; type Post = { title: string; content: string; createdAt: string; updatedAt: string; publishedAt: string; }; type PostData = { id: string; attributes: Post }[]; type PostResponse = { data: PostData; meta: { pagination: { page: number; pageSize: number; pageCount: number; total: number; }; }; }; export const loader: LoaderFunction = async ({params}) => { const queryString = qs.stringify({ filters: { slug: { $eq: params.slug } }, populate: "*", pagination: { start: 0, limit: 1} }); const url = `${STRAPI_API_URL}/api/posts?${queryString}`; const response = await fetch(url); const postResponse = (await response.json()) as PostResponse; const post = post.data[0]; return json({ ...post, attributes: { ...post.attributes, content: marked(post.attributes.content), }, ); }; const Post: React.FC = () => { const post = useLoaderData(); const { title, article, createdAt } = post.attributes; const date = new Date(createdAt).toLocaleString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric", }); return ( <article key={post.id}> <h1>{title}</h1> <time dateTime={createdAt}>{date}</time> <div dangerouslySetInnerHTML={{ __html: content }} /> </article> ); }; export default Post;
4. Deploying to Vercel
Once your set up, you only need to import your Git repository into Vercel, and it will be deployed.
Remember to add the ENV variable when you set it up.
If you'd like to avoid using a Git repository, you can also deploy the directory by running Vercel CLI:
npm i -g vercel vercel
It is generally recommended to use a Git repository, because future commits will then automatically be deployed by Vercel, through its Git Integration.
5. Closing Thoughts
I've kept things simple, we didn't make sitemaps or rss feeds here, it would be pretty simple to make based on the blog.tsx
file so I'll let you work out how to do that for yourself.
You can find the repo here if you'd prefer to follow along that way.