Migrating a WordPress Blog to Strapi

February 9th 2020

There is only one thing in the world that changes faster than the weather, and that’s technology. Everyday there are new technology stacks and projects that are advancing the industry forward. One new buzzword you have been hearing of is “headless CMS”. A headless CMS decouples the front-end theme/view/UI from the backend API/CMS and server code. One of the emerging players in the headless CMS space is Strapi.io. Strapi is a headless CMS/API scaffolding framework that is built on the popular koa.js server. It comes with some stunning features like the admin dashboard that allows you to create and configure content types, set permissions and add strapi plugins such as a GraphQL server. This makes Strapi a perfect option to move from a traditional CMS like WordPress to a headless CMS model. We are going to walk through migrating a WordPress site into a Strapi CMS. Let’s get started!

First we’ll create a local MongoDB database that we can use to store our Strapi content/data. From the command line we’ll log into the mongo shell:

mongo
use strapi
db.createUser({ user: "blog", roles: [{ "role": "dbOwner", "db": "strapi" }], pwd: "YOUR_PASSWORD" })

This should create a database strapi with a user blog. Make sure to change out YOUR_PASSWORD for the password you want to use. Now let’s create a new strapi project. Navigate to the working directory of your choice and run the command:

npx create-strapi-app strapi-blog

We’ll get prompt to choose the installation type and we can choose “Custom (manual settings)”. Then we will walk through the prompts to configure. We will obviously choose “mongo” for our database of choice:

Choose your default database client mongo
Database name: strapi
Host: 127.0.0.1
+srv connection: false
Port (It will be ignored if you enable +srv): 27017
Username: blog
Password: ****************
Authentication database (Maybe "admin" or blank): strapi
Enable SSL connection: No

Once our database is connected to Strapi will complete the installation and give us some instructions on how to start the strapi app. We can cd strapi-blog && yarn develop. This will open a browser window to create our strapi admin user. Go ahead and create your user for the admin dashboard. If your browser doesn’t open you can navigate to http://localhost:1337/admin/

We are in and you should see the dashboard screen. Our first order of business will be to create a new content type that will be our “posts” that we can migrate from WordPress into Strapi. Navigate to the “Content Type Builder” in the sidebar. Then we can click “Create New Content Type”. In the Display Name field we can put Post making sure that is singular. At this point you will probably want to open your WordPress blog in a a new tab and navigate to a current post so you can identify the fields we need to map over. We will at a minimum want to have: title, content, slug. Other potential fields you might have would be author, image. We will be mapping our WordPress “featured_image” to the image field we create. Let’s create those fields in Strapi.

Click “Add Field” and select “text” and in the field name you can type title click on Advanced Settings tab in upper right and select “Required”. Then click “Add another field”. You can repeat for slug, content and image making sure you are selecting the correct field type and the Advanced Settings you would like. Once you are done you can click “Finished” and Strapi will restart and you will see you have a new Content Type “Posts” in the sidebar. Let’s navigate over to that and see what it looks like. At this point if you are following along you should see the four fields we created: title, slug, content and image. Looks good. We’ll need to do one more thing here to make our migration easier. In the left hand sidebar we need to click on “Roles and Permissions” and then click on the “Public” role. We’re going to enable “All” permissions for our Public role 😱. Why would we do this, couldn’t anyone create a post on our blog? Yes, but since we’re in development locally we are going to do this for migration and then change the settings back to “Read Only” when we’re done migrating! This is so we don’t have to handle authentication headers when posting to our API during development. Speaking of API, did you know that since we created a content type that Strapi automatically scaffolded an API for us? Let’s pop over to Postman and check that out. In postman we’ll run a GET request to http://localhost:1337/posts and you will see that we have and empty array [] as expected since we haven’t created any posts. Let’s create one for fun! Navigate back to Content Manager > Post > Add New Post and create a dummy post. After we save we can run the GET request in postman again and we should see

[
    {
        "_id": "5e3ef1ad0e2ef432c5b7ea3e",
        "title": "Some Dummy",
        "slug": "some-dummy",
        "content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
        "createdAt": "2020-02-08T17:36:45.828Z",
        "updatedAt": "2020-02-08T17:36:45.828Z",
        "__v": 0,
        "image": [],
        "id": "5e3ef1ad0e2ef432c5b7ea3e"
    }
]

Perfect, we have a post. Let’s go a head and delete that post via postman. Send DELETE request to http://localhost:1337/posts/{post_id} making sure you replace post_id with your post’s id such as http://localhost:1337/posts/5e3ef1ad0e2ef432c5b7ea3e

Now with that cleaned up we’ll go a head and create a new route (endpoint) and controller in our Post api so we import the posts. Naavigate to strapi-blog/api/post/config and edit the routes.json file. We will add:

{
  "routes": [
    {
      "method": "GET",
      "path": "/posts/import",
      "handler": "post.import",
      "config": {
        "policies": []
      }
    },
    ...
  ]
}

to the top of the “routes” array. We can then navigate to strapi-blog/api/post/controllers and edit post.js. Let’s add this for now to make sure our endpoint is working:

module.exports = {
  import: async ctx => {
    ctx.send('imported')
  }
};

Then we will have to go back to our Roles and Permissions and make sure our new controller can be accessible. Under the “public” role you can select Post and check import and save the Role. Now let’s navigate back to Postman and test: http://localhost:1337/posts/import you should see a response imported. Perfect, we have our route connected to our controller action!

Let’s start reworking our action so we can start importing the WordPress posts and save them to our Strapi project. First we’ll need to install a package called axios for getting our post requests from our WordPress json endpoint. Install axios yarn add axios and we will require it at the top of our controller:

const axios = require('axios')

module.exports = {
  import: async ctx => {
    ctx.send('imported')
  }
};

Now in our import action we can send a GET request to our WordPress rest endpoint. WordPress sites expose access to posts via /wp-json/wp/v2/posts endpoint. You can see what this returns by opening up the terminal and running: curl https://example.com/wp-json/wp/v2/posts and you will see it the output of posts returned in json format. We can use axios to make this request.

const axios = require('axios')

module.exports = {
  import: async ctx => {
    const { data } = await axios.get('https://www.example.com/wp-json/wp/v2/posts?per_page=3');
  }
};

Now we will have to loop through our data to create our posts in Strapi. We can take advantage of Promise.all() and map through our data array returning a Promise that will resolve when each new post is created. It should look like this:

const axios = require('axios')

module.exports = {
  import: async ctx => {
    const { data } = await axios.get('https://www.example.com/wp-json/wp/v2/posts?per_page=3');
    const posts = await Promise.all(data.map(post => new Promise(async (resolve, reject) => {
      //resolve when the post is created
    }))
  }
};

We can destructure the fields from each of our post objects:

const axios = require('axios');

module.exports = {
  import: async ctx => {
    const { data } = await axios.get('https://www.example.com/wp-json/wp/v2/posts?per_page=3');
    const posts = await Promise.all(data.map(post => new Promise(async (resolve, reject) => {
      const { title: { rendered: titleRendered }, slug, content: { rendered: contentRendered }, date, featured_image } = post;
    }));
  }
};

Now that we have our content from the post we can structure it to create our post in our Strapi project. The tricky part of the migration is getting the featured_image from our WordPress blog migrated into an entry in our Strapi uploads plugin. We are going to have to create some helper functions in order to achieve this. Strapi beta exposes a filesystem configuration to create helper functions in /config/functions folder so we can create two helper functions: 1. to download our featured_image into a temporary folder on our host (in this case localhost) and 2. using the file upload plugin we can use the filepath of the featured_image we downloaded to create an upload entry in our Strapi project and associate (create a relationship) with our post entry that we will be creating. Let’s create those two files: touch config/functions/download.js && touch config/functions/upload.js. Great, let’s dig into these files. First, we’ll start with our download.js helper function. Let’s open that up and edit. We need to require a few packages:

const axios = require('axios');
const path = require('path');
const fs = require('fs');

module.exports = async (url) => {
}

Our helper function is going to take a parameter url which is the external resource to our featured_image link. Great, now the steps we need to take are: Make a GET request to the resource using axios. This will be a responseType of “stream” and we will pipe the stream to our node fs.writeStream instance. We will return a promise that resolves when the ‘finish’ event of the writeStream is emitted. The path to the image file will be returned/resolved in our promise. Let’s see what that looks like. You can read the comments in the code for further explanation:

const axios = require('axios');
const path = require('path');
const fs = require('fs');

module.exports = async (url) => {
  // get the filename such as `image01.jpg`
  const name = path.basename(url);
  // we need to set a file path on our host where the image will be
  // downloaded to
  const filePath = `/tmp/${name}`;
  // create an instance of fs.writeStream
  const writeStream = fs.createWriteStream(filePath);
  // make a GET request and create a readStream to the resource
  const { data } = await axios.get(url, { responseType: 'stream' });

  // pipe the data we receive to the writeStream
  data.pipe(writeStream);
  // return a new Promise that resolves when the event writeStream.on
  // is emitted. Resolves the file path
  return new Promise((resolve, reject) => {
    writeStream.on('finish', () => {
      resolve(filePath)
    });
    writeStream.on('error', reject);
  });
}

That should take care of our downloading our featured_image now all we have to do to call that helper function from anywhere in our strapi project is use await strapi.config.functions.download(featured_image); and it will return the file path to the image we downloaded. Now remember that is just a temp download cause we need the image on the same host/disk that Strapi is on so we can then upload it and create a upload entry. For this we’ll turn our attention to our upload.js helper function. Once again we need a few packages:

const path = require('path');
const fs = require('fs').promises;

module.exports = async (imgPath) => {
}

This time we’re using the node fs.promises library so we can await the fs.readFile function to make sure we have all of our file information (in Buffer form) before we pass it off to the uploads plugin. We then use a couple service functions from the File Uploads plugin: strapi.plugins.upload.services.upload.bufferize() and strapi.plugins.upload.services.upload.upload() to help us upload and create an image entry for us. If we were just to move the image resource that we downloaded into our Strapi /public/uploads folder it would be accessible but the entry (and reference to it) would not be stored in our database and our post entry would have no way of associating to that file. So here we go:

const path = require('path');
const fs = require('fs').promises;

module.exports = async (imgPath) => {
  // we want the file type without the "." like: "jpg" or "png"
  const ext = path.extname(imgPath).slice(1);
  // name of the file like image01.jpg
  const name = path.basename(imgPath);
  // read contents of file into a Buffer
  const buffer = await fs.readFile(imgPath);
  // get the buffersize using service function from upload plugin
  const buffers = await 
  strapi.plugins.upload.services.upload.bufferize({
    path: imgPath,
    name,
    type: `image/${ext}`,
    size: buffer.toString().length 
  });
  // pass the `buffers` variable to the service function upload which 
  // returns a promise that gets resolved upon upload
  return strapi.plugins.upload.services.upload.upload(
    buffers,  {
        provider: 'local',
        enabled: true,
        sizeLimit: 10000000
    }
  );
}

Now we are able to use this upload helper function anywhere in our Strapi project by calling await strapi.config.functions.upload(downloaded); it returns a promise so we can await it and when it resolves it returns the reference to the entry so we can destructure the entity id such as:

const [{ id: fileId }] = await strapi.config.functions.upload(downloaded);

Whew, the hard part is over! Now we can turn our attention back to our import action within our post.js controller. Let’s finish that up using the helper functions we have now built out:

const axios = require('axios');

module.exports = {
  import: async ctx => {
    const { data } = await axios.get('https://www.example.com/wp-json/wp/v2/posts?per_page=3');
    const posts = await Promise.all(data.map(post => new Promise(async (resolve, reject) => {
      const { title: { rendered: titleRendered }, slug, content: { rendered: contentRendered }, date, featured_image } = post;
      try{
        // featured_image functionality here that we built
        // out with our helper functions
        const downloaded = await strapi.config.functions.download(featured_image);
        const [{ id: fileId }] = await strapi.config.functions.upload(downloaded);
          
        // now that we have fileId we can complete our postData object
        const postData = {
            title: titleRendered,
            content: contentRendered,
            slug,
            image: [fileId],
            createdAt: date
        };
        // use the strapi services create function to create entry
        const created = await strapi.services.post.create(postData);
        resolve(created)
      }catch(err){
        reject(err)
      }
    })));
  }
};

Sweet, this should handle the creation of all of our posts and using Promise.all() we can await until all the posts are created until we send a response back to the client. To finish up all we have to do is:

const axios = require('axios');

module.exports = {
  import: async ctx => {
    const { data } = await axios.get('https://www.example.com/wp-json/wp/v2/posts?per_page=4');
    const posts = await Promise.all(data.map(post => new Promise(async (resolve, reject) => {
      const { title: { rendered: titleRendered }, slug, content: { rendered: contentRendered }, date, featured_image } = post;
      try{
        const downloaded = await strapi.config.functions.download(featured_image);
        const [{ id: fileId }] = await strapi.config.functions.upload(downloaded);
        
        const postData = {
          title: titleRendered,
          content: contentRendered,
          slug,
          image: [fileId],
          createdAt: date
        };
        const created = await strapi.services.post.create(postData);
        resolve(created)
      }catch(err){
        reject(err)
      }
    })));
    
    ctx.send(posts);
  }
};

With any luck we should now be able to head back to Postman and make a request to our import route/controller which should create our posts from our WordPress blog as entries into our Strapi Blog: http://localhost:1337/posts/import

There you have it, a migration from WordPress to Strapi that you can use on one or multiple sites to move from a traditional CMS model to a headless model. Until next time, stay curious, stay creative!