From WordPress to Strapi and Remix

Roger Stringer
August 12, 2022
8 min read

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:

  1. Setting up Strapi Posts Content-Type
  2. Exporting from Wordpress and importing into Strapi
  3. Setting up Remix app
  4. 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:

  1. title: short text, unique, required
  2. content: rich text
  3. slug: UUID field linked to title
  4. excerpt: load text
  5. date: 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:

bash
1yarn add strapi-sdk-js date-fns turndown
2

Now create a file called wp.js:

// wp.js:
1import axios from 'axios';
2import Strapi from "strapi-sdk-js";
3import { format } from 'date-fns';
4
5const TurndownService = require("turndown");
6const turndownService = new TurndownService();
7
8const strapi = new Strapi({
9  url: "https://YOUR-STRAPI-URL,
10  prefix: "/api",
11});
12
13const token = "YOUR-API-KEY";
14strapi.axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
15
16const start = async () => {
17    const { data } = await axios.get('https://YOUR-WP-SITE/wp-json/wp/v2/posts?offset=0&per_page=100');
18    const posts = await Promise.all(data.map(post => new Promise(async (resolve, reject) => {
19        const {
20            title: { rendered: titleRendered },
21            slug,
22            content: { rendered: contentRendered },
23            date,
24        } = post;
25        try{
26            const postData = {
27                title: titleRendered,
28                content: turndownService.turndown(contentRendered),
29                slug,
30                date: format( new Date(date), 'yyyy-MM-dd'),
31                createdAt: date,
32            };
33            const created = await strapi.create('posts', postData);
34            resolve(created)
35        }catch(err){
36            console.log(err.error.details);
37        }
38    })));
39    console.log(posts.length);
40}
41
42start();
43

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:

bash
1npx create-remix@latest
2

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:

bash
1yarn add marked qs
2

Which is a markdown to html processor and a querystring processor.

You also want an ENV variable:

1STRAPI_API_URL=http://localhost:1337
2

This will be the URL for strapi, so change it to whatever you've installed it at.

We're going to use two files:

  1. app/routes/blog.tsx, a new file.
  2. app/routes/blog.$slug.tsx, which is new.
// app/routes/blog.tsx:
1import qs from "qs"
2import type { LoaderFunction } from "@remix-run/node";
3import { json } from "@remix-run/node";
4import { useLoaderData } from "@remix-run/react";
5import { marked } from "marked";
6import * as React from "react";
7
8type Post = {
9  title: string;
10  content: string;
11  createdAt: string;
12  updatedAt: string;
13  publishedAt: string;
14};
15
16type PostData = { id: string; attributes: Post }[];
17
18type PostResponse = {
19  data: PostData;
20  meta: {
21    pagination: {
22      page: number;
23      pageSize: number;
24      pageCount: number;
25      total: number;
26    };
27  };
28};
29
30export const loader: LoaderFunction = async () => {
31
32  const queryString = qs.stringify({
33    sort: 'publishedAt:desc',    
34    populate: "*", 
35    pagination: { start: 0, limit: 100} 
36  });
37  
38  const url = `${STRAPI_API_URL}/api/posts?${queryString}`;
39  const response = await fetch(url);
40  const postResponse = (await response.json()) as PostResponse;
41
42  return json(
43    postResponse.data.map((post) => ({
44      ...post,
45      attributes: {
46        ...post.attributes,
47        content: marked(post.attributes.content),
48      },
49    }))
50  );
51};
52
53const Posts: React.FC = () => {
54  const posts = useLoaderData();
55
56  return (
57    <>
58      {posts.map((post) => {
59        const { title, content, createdAt } = post.attributes;
60        const date = new Date(createdAt).toLocaleString("en-US", {
61          weekday: "long",
62          year: "numeric",
63          month: "long",
64          day: "numeric",
65        });
66
67        return (
68          <article key={post.id}>
69          	<Link to={`/blog/${post.slug}`}>
70	            <h1>{title}</h1>
71	            <time dateTime={createdAt}>{date}</time>
72	            <div dangerouslySetInnerHTML={{ __html: content }} />
73            </Link>
74          </article>
75        );
76      })}
77    </>
78  );
79};
80
81export default Posts;
82
83

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.

// app/routes/blog.$slug.tsx:
1import qs from "qs"
2import type { LoaderFunction } from "@remix-run/node";
3import { json } from "@remix-run/node";
4import { useLoaderData } from "@remix-run/react";
5import { marked } from "marked";
6import * as React from "react";
7
8type Post = {
9  title: string;
10  content: string;
11  createdAt: string;
12  updatedAt: string;
13  publishedAt: string;
14};
15
16type PostData = { id: string; attributes: Post }[];
17
18type PostResponse = {
19  data: PostData;
20  meta: {
21    pagination: {
22      page: number;
23      pageSize: number;
24      pageCount: number;
25      total: number;
26    };
27  };
28};
29
30export const loader: LoaderFunction = async ({params}) => {
31
32  const queryString = qs.stringify({
33	   filters: {
34          slug: {
35	        $eq: params.slug
36          }
37        },
38    populate: "*", 
39    pagination: { start: 0, limit: 1} 
40  });
41  
42  const url = `${STRAPI_API_URL}/api/posts?${queryString}`;
43
44  const response = await fetch(url);
45  const postResponse = (await response.json()) as PostResponse;
46  const post = post.data[0];
47  return json({
48      ...post,
49      attributes: {
50        ...post.attributes,
51        content: marked(post.attributes.content),
52      },
53  );
54};
55
56 const Post: React.FC = () => {
57 const post = useLoaderData();
58  
59  const { title, article, createdAt } = post.attributes;
60  const date = new Date(createdAt).toLocaleString("en-US", {
61		weekday: "long",
62		year: "numeric",
63		month: "long",
64		day: "numeric",
65  });
66  
67  return (
68	<article key={post.id}>
69		<h1>{title}</h1>
70		<time dateTime={createdAt}>{date}</time>
71		<div dangerouslySetInnerHTML={{ __html: content }} />
72	</article>
73  );
74};
75export default Post;
76

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:

bash
1npm i -g vercel
2vercel
3

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.

Do you like my content?

Sponsor Me On Github