From WordPress to Strapi and Remix

Roger Stringer • August 13, 2022

6 min read

This blog 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.

Hold up!

This example doesn't get into hosting Strapi, just adding the content-types and importing and later reading from it, I recommend looking at Railway 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:

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 [email protected]

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:

  1. app/routes/blog.tsx, a new file.
  2. app/routes/blog.$slug.tsx, which is new.
app/routes/blog.tsx
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.

app/routes/blog.$slug.tsx
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.

Tagged in: #Code
Like this blog post? Share it on twitter!