DEV Community

Cover image for Build a Custom Strapi Plugin with Medium and Dev.to APIs - Part 1
Theodore Kelechukwu Onyejiaku for Strapi

Posted on • Originally published at strapi.io

Build a Custom Strapi Plugin with Medium and Dev.to APIs - Part 1

Introduction to Strapi Plugins

Strapi plugins allow you to add extra functionalities and custom features to power up your Strapi application.

One of the features of the Strapi CMS is the ability it gives you to unlock the full potential of content management, thus allowing you to build custom features for yourself and the community. Victor Coisne, the VP of marketing at Strapi, explained this in his article "Building Communities That Drive Growth".

"Strapi builds trust and shows that member input matters. They also foster transparency through forums, AMAs, and regular updates that keep members informed and valued."

In this article, we will build a custom Strapi plugin that will allow us publish contents to Medium and Dev.to using their APIs from the Strapi admin panel.

Tutorial Outline

This article is divided into two parts:

  • Part 1: Initializing the plugin, content types, and Dev.to API integration.
  • Part 2: Medium API integration, pagination, search functionality, and injection zones.

What you Will Learn

In this comprehensive tutorial, you will learn about the following:

  • Initializing a Strapi plugin
  • Utilizing a Strapi Lifecycle function
  • Creating a Content Type for a Strapi plugin and Storing Data
  • Passing data from the server to the admin of a Strapi plugin
  • Integrating and consuming Medium and Dev APIs with Strapi plugin
  • Injecting a Strapi Plugin to a specific collection type with injection zones
  • Adding Pagination and Search functionality to a Strapi plugin

Prerequisites

To follow along with this tutorial, please consider the following:

  • A basic knowledge of React is required. Strapi plugins are developed using React.js.
  • The latest Node.js runtime installed on your machine.
  • A Dev account.
  • A Medium account.
  • An API platform like Postman.

What We Will Be Building

One of the features of the Strapi CMS is the ability it gives you to unlock the full potential of content management. This means that contents like blog posts can also be published from Strapi to platforms like Medium and Dev.

In this article, we will make use of Dev API and Medium API, integrate them with Strapi headless CMS to allow us publish contents from the Strapi admin panel to these platforms.

Build a Custom Strapi Plugin

What is a Strapi Plugin?

Strapi plugins allow you to extend Strapi core features.

They can be in 4 forms.

  • Built-in plugins like the Upload plugin, Users and Permissions plugin, Email plugin, etc.
  • 3rd-party plugins developed by the community and can be found in the Strapi Marketplace. Examples include the Redirects, Algolia, AWS S3, etc.
  • Custom plugins that you can develop on your own and share on the Strapi Marketplace or use alone. An example is the one for this tutorial.
  • Extended Strapi plugin. This is when you extend the features of an existing plugin.

To learn more about Strapi plugins, please visit its Strapi documentation page.

Strapi Design System

To develop a Strapi plugin, you will be making use of the Strapi design system. This is a collection of typographies, colors, components, icons, etc. that are in line with Strapi brand design.

The Strapi design system allows you to make Strapi's contributions more cohesive and to build plugins more efficiently.

This comes with a new Strapi app.

Install Strapi and Create New Collection Types

Install a Strapi 5 Application

Install a new Strapi 5 application by running the command below. For this project, we will be using NPM; you can choose any package manager of your choice:

# npm
npx create-strapi-app@latest # npm

# yarn
yarn create strapi # yarn

# pnpm
pnpm create strapi # pnpm
Enter fullscreen mode Exit fullscreen mode

The name of our project is plugin-project as shown below. The terminal will ask you a few questions. Choose a Yes or a No depending on your setup options.

Install Strapi.png

From the installation process, the name of our project is plugin-project. Feel free to give it a name of your choice.

Start Strapi Application

After the successful installation of your Strapi project, you should be able to start your Strapi application.

CD into your Strapi project:

cd plugin-project
Enter fullscreen mode Exit fullscreen mode

Run the command below to start your Strapi development server:

npm run develop
Enter fullscreen mode Exit fullscreen mode

The command above will redirect you to the Strapi admin registration page. Ensure you create a new admin user.

Create New Strapi Content Types

Strapi content types or models are data structures for the content we want to manage.

Create the Strapi collection types Blog and Tag. Later on, we will create a collection type called Post for our Strapi plugin.

Blog refers to entries we want to publish. Post on the other hand are entries containing details of a blog we want to publish such as the blog link, Medium link, etc. A Post entry will be created automatically using Strapi Life Cycle upon creating a Blog entry.

If you are new to Strapi, learn about content-types here.

To create a new Strapi collection type, navigate to the Content-type Builder and click on the "+ Create new collection type" as shown in the first image below. After that, proceed to enter the name of the Collection type and click "Continue" to create fields for each collection type as shown in the second image below.

Locate the Content-type Builder.png
Locate the Content-type Builder

Enter Collection Name and Click Continue.png
Enter Collection Name and Click "Continue"

Now, create the following collection types.

Tag Collection Type
This represents the category a blog post belongs to. It is required for both the Medium and Dev API. As we will see in the Blog collection type below, it has a "many to one" relation with the Blog collection, i.e., a blog entry can have more than one tag.

Field Type Description
blogTag Text Tag used for blog categorization

Blog Collection Type
The Blog collection type represents the blog post. It has fields like canonicalUrl and tags, which are required parameters when making requests to the Medium API or the Dev API.

Field Type Description
title Text The title of the content
content Rich text (Markdown) The main body content
canonicalUrl Text The canonical URL for SEO.
tags Relation with Tag collection type. => "Blog has many Tags" Associated tags for categorization.
banner Text URL or path for the banner image

πŸ–οΈ NOTE: Ensure you disable "Draft and publish" for the Blog collection type.

Make sure to disable the "Draft and Publish" feature for the Blog collection type by unchecking the option.

Disable Draft and Publish.png
Disable Draft and Publish

At this point, your Strapi app is ready to be powered up with a plugin! Next, we will learn how to generate a Strapi plugin.

Step 5: Enable API Permission for Tag Collection Type

Because we want to be able to use the tags of blog entries when publishing to Medium and Dev, we want to make sure that the tags of a blog are returned when we make a request to get a blog entry.

Ensure API permission is enabled for the Tag collection type.
Enable API Permission for Tag Collection.png
Enable API Permission for **Tag* Collection*

Initialize a Strapi Plugin Using The Plugin SDK

The Strapi plugin SDK is used to develop and publish Strapi plugins for NPM.

Initialize a new Strapi plugin using the command below. Ensure you are in your Strapi project directory content-publisher. The name of our plugin is content-publisher.

npx @strapi/sdk-plugin init content-publisher
Enter fullscreen mode Exit fullscreen mode

πŸ–οΈ NOTE: You can use the --force option after npx if you experience peer dependency error.

Make sure to answer the prompts that follow as shown below:

βœ” plugin name … content-publisher
βœ” plugin display name … content-publisher
βœ” plugin description … This is a plugin that allows you to publish contents from Strapi to Dev.to, Medium.com and many more in the future
βœ” plugin author name … Theodore Kelechukwu Onyejiaku
βœ” plugin author email … theodoreonyejiaku@gmail.com
βœ” git url … 
βœ” plugin license … MIT
βœ” register with the admin panel? … yes
βœ” register with the server? … yes
βœ” use editorconfig? … yes
βœ” use eslint? … yes
βœ” use prettier? … yes
βœ” use typescript? … yes
Enter fullscreen mode Exit fullscreen mode

πŸ–οΈ NOTE: Make sure to answer "yes" to register with admin panel and server. This is because we want to have our plugin server and frontend.

If the plugin initialization is successful, you should see the success message "Plugin generated successfully" and instructions on how to enable the plugin, as shown below.

Enable Plugin.png
Enable Plugin

Head over to the plugin configuration file ./config/plugin.ts and add the code below:

// Path: ./config/plugin.ts

export default {
  // ...
  'content-publisher': {
    enabled: true,
    resolve: './src/plugins/content-publisher'
  },
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Start The Strapi Plugin

To start building our plugin, we need to make sure that any changes and rebuilds are updated by using the watch command.

First, CD into the plugin directory of your Strapi project, ./src/plugins/content-publisher. After that, run the command below:

npm run watch
Enter fullscreen mode Exit fullscreen mode

You should now see your Strapi plugin in two ways

Find the content-publisher plugin.png
Click on *Settings > Plugins** and find the content-publisher plugin*

Locate and click the new custom plugin icon..png
Locate and click the new custom plugin icon.

Strapi Plugin Structure

The Strapi plugin structure is divided into 2 parts.

  • Admin panel: This contains codes/files that will be visible on the Strapi admin panel. Examples include components, etc. This is found in the /admin directory of the plugin.
  • Backend server: This contains codes/files that are related to the server of the plugin. Examples include middlewares, controllers, routes, etc. This is found in /server directory of the plugin.
src/
┣ ...
┣ plugins/
┃ β”— content-publisher/
┃  ┣ admin/
┃  ┃ ┣ src/
┃  ┃ ┣ custom.d.ts
┃  ┃ ┣ tsconfig.build.json
┃  ┃ β”— tsconfig.json
┃  ┣ server/
┃  ┃ ┣ src/
┃  ┃ ┣ tsconfig.build.json
┃  ┃ β”— tsconfig.json
┃  β”— ...
β”— index.ts
Enter fullscreen mode Exit fullscreen mode

For a comprehensive understanding of the Strapi plugin structure, please visit this Strapi documentation page: Plugin structure.

Update Strapi Plugin Icon and Page Content

Let's get busy!

We will modify the icon and page content of our plugin so that it can be seen live!

Update Plugin Icon

Locate the current plugin icon inside the components folder. This is available Inside the ./src/plugins/content-publisher/admin/src/components/PluginIcon.tsx file. Currently, it uses the puzzle-piece icon.

Update this file with the code below:

// Path: src/plugins/content-publisher/admin/src/components/PluginIcon.tsx

import { Sparkle } from '@strapi/icons';

const PluginIcon = () => <Sparkle />;

export { PluginIcon };
Enter fullscreen mode Exit fullscreen mode

Now, the plugin icon has been updated to a sparkle icon, as shown in the image below. The current plugin icon can be updated to any icon you prefer. You could use an icon from the Strapi design system or an SVG of your choice that aligns with the Strapi design system.

Update Plugin Page Content

At the moment, the content of the plugin displays "Welcome to content-publisher.plugin.name". Let's update it.

Update the code inside the ./src/plugins/content-publisher/admin/src/pages/HomePage.tsx file with the following code:

// Path: src/plugins/content-publisher/admin/src/pages/HomePage.tsx

import { Main, Box, Typography } from '@strapi/design-system';

const HomePage = () => {
  return (
    <Main padding={5}>
      <Box paddingBottom={4} margin={20}>
        <Typography variant="alpha">Welcome To Content Publisher</Typography>
        <Box>
          <Typography variant="epsilon">Publish blog posts to medium, dev.to, etc.</Typography>
        </Box>
      </Box>
    </Main>
  );
};

export { HomePage };
Enter fullscreen mode Exit fullscreen mode

In the code above, we used some some components from the Strapi design system to update the content and structure of the content of our plugin home page.

The image below shows what our new plugin home page and icon look like after the update.

Home page of Content Publisher custom plugin.png
Home page of Content Publisher custom plugin

Create Content Type for Strapi Plugin

Since we want our custom plugin to be able to publish posts to Medium and Dev, we need to keep track of posts that have been published to these platforms or are ready for publishing.

Generate Post Collection Type for Strapi Plugin

We need to create a new collection type for our plugin. It will be called Post. It will have the following fields:

  • mediumLink: This will be used to store the link to the published post on Medium. It will be of type Text.
  • devToLink: This will be used to store the link to the published post on Dev. It will be of type Text.
  • blog: This will point to blog content from the Blog collection type in our Strapi backend. It will be a one-to-one relationship with the Blog collection type.

To generate the collection type for our plugin, we can do it in two ways. You can learn more by checking out how to store and access data from a Strapi plugin.

  1. Use the generate command as shown below:
# npm
npm run strapi generate content-type
Enter fullscreen mode Exit fullscreen mode
  1. Or create a file called post.ts inside server/src/content-types directory, and add the following code
// content-types/post.ts

const schema = {
  kind: 'collectionType',
  collectionName: 'posts',
  info: {
    singularName: 'post',
    pluralName: 'posts',
    displayName: 'Post',
  },
  options: {
    draftAndPublish: false,
  },
  pluginOptions: {
    'content-manager': {
      visible: true,
    },
    'content-type-builder': {
      visible: true,
    },
  },
  attributes: {
    mediumLink: {
      type: 'text',
    }
  },
};

export default {
  schema,
};
Enter fullscreen mode Exit fullscreen mode

Below is an explanation of the code above:

  • We define a Strapi content type schema for Post and specify it as a collection type. We also set up basic metadata, disabled draft and publish functionality, and ensured the content type was visible in both the Content Manager and Content-Type Builder plugins of the Strapi admin panel.

  • We add attribute "mediumLink" as a text field. We will add the remaining fields devToLink and blog soon using the Content-type Builder on the Strapi admin panel.

  • Next, import this new content type so you can see it on the Strapi admin panel. Locate the ./src/plugins/content-publisher/server/src/content-types/index.ts and update it with the following code:

import post from './post';

export default {
  post,
};

Enter fullscreen mode Exit fullscreen mode

Now, we should be able to see the Post collection with the mediumLink field for our plugin:

Post collection with mediumLink.png
Post collection with mediumLink

Add the devToLink and blog fields by clicking the "Add another field to the collection type". Remember to make sure that the blog field of the Post collection type is related to Blog collection in a one-to-one relation as "Post has and belongs to one Blog." See image below:

Relation between Post and Blog collection types.png
Relation between **Post* and Blog collection types*

Strapi Plugin Server Customization: Routes and Controllers

The Post collection type for our plugin is ready; we have to customize the backend of our plugin by creating routes, controllers, and services.

  • Routes: These handle requests sent to Strapi. You can find the routes for our plugin here: ./src/plugins/content-publisher/server/src/routes/index.ts
  • Controllers: These are actions or methods performed when a request is made to a route. You can find the controllers for our plugin here: ./src/plugins/content-publisher/server/src/controllers/controller.ts
  • Services: Reusable functions are often called by controllers. You can find the services for our plugin here: ./src/plugins/content-publisher/server/src/services/service.ts.

Here is an example:

// route GET route to fetch blogs
export default [
  {
    method: 'GET',
    path: '/blogs',
    // name of the controller file & the method.
    handler: 'controller.getBlogs',
    config: {
      policies: [],
    },
  },
];

// controller with a controller method or action
const controller = ({ strapi }: { strapi: Core.Strapi }) => ({
  async getBlogs(ctx) {
    ctx.body = await strapi.plugin('medium-publisher').service('service').getBlogPosts(); // invokes `getBlogposts()` service
  },
});

// A reusable service called by controller above
const service = ({ strapi }: { strapi: Core.Strapi }) => ({
  // GET Blog Posts
  async getBlogPosts() {
    try {
      const blogPost = await strapi.documents('api::blog.blog').findMany();
      return blogPost;
    } catch (error) {
      throw error;
    }
  },
});
Enter fullscreen mode Exit fullscreen mode

The above is a route that handles requests to /blogs. It is handled by a controller method called getBlogs. This method on the other hand invokes the getBlogPosts service.

In order to proceed, we need to customize our Strapi plugin server. Hence, we will customize the routes and controllers.

Create Strapi Plugin Routes

Strapi plugins can have two kinds of routes.

  1. External or Content-Type Route: This will be accessible to requests within or outside the admin panel. This is declared by adding type:'content-api' to the route. An example is /api/blogs.
  2. Admin Route: These are hidden from the general API router and can only be accessed in the admin panel. This is done by adding type:'admin' to the route. An example is plugin-name/posts.

Inside the ./src/plugins/content-publisher/server/src/routes, create two files, admin.ts and contentApi.ts respectively and add the codes below for each.

Create Private Routes for Plugin
Inside the ./src/plugins/content-publisher/server/src/routes/admin.ts file, add the following code. The following are routes accessible only to the admin panel and not the general or external API.

// Path: ./src/plugins/content-publisher/server/src/routes/admin.ts

import policies from 'src/policies';

export default [
  {
    method: 'GET',
    path: '/posts',
    handler: 'controller.getPosts',
    config: {
      policies: [],
      auth: false,
    },
  },
  {
    method: 'GET',
    path: '/single-post',
    handler: 'controller.getSinglePost',
    config: {
      policies: [],
      auth: false,
    },
  },
  {
    method: 'POST',
    path: '/publish-to-medium',
    handler: 'controller.publishPostToMedium',
    config: {
      policies: [],
      auth: false,
    },
  },
  {
    method: 'POST',
    path: '/publish-to-devto',
    handler: 'controller.publishPostToDevTo',
    config: {
      policies: [],
      auth: false,
    },
  },
  {
    method: 'GET',
    path: '/search',
    handler: 'controller.getSearchQuery',
    config: {
      policies: [],
      auth: false,
    },
  },
  {
    method: 'DELETE',
    path: '/delete-post',
    handler: 'controller.deletePost',
    config: {
      policies: [],
      auth: false,
    },
  },
];

Enter fullscreen mode Exit fullscreen mode

In the code above, we created an array of admin routes that our plugin will use. Note that we added auth=false. This means the routes will be publicly accessible without requiring any authentication.

πŸ–οΈ NOTE: We added auth=false to each route above because we want the routes to be accessible without requiring any authentication.

Here is what each route above does:

Method Path Handler Description
GET /posts controller.getPosts Retrieves a list of posts
GET /single-post controller.getSinglePost Retrieves a single post
POST /publish-to-medium controller.publishPostToMedium Publishes a post to Medium
POST /publish-to-devto controller.publishPostToDevTo Publishes a post to Dev
GET /search controller.getSearchQuery Performs a search query
DELETE /delete-post controller.deletePost Deletes a post

The routes above will be avialable to our plugin admin panel on the endpoint /content-publisher which is the name of our plugin. For example /content-publisher/posts.

We will create the corresponding controllers for each of these routes shortly.

Create Public Route
In order to demonstrate that we can make some routes private to the admin panel and others accessible externally, we will create a publicly availabe route.

Inside the ./src/plugins/content-publisher/server/src/routes/contentApi.ts file, add the following:

export default [
  {
    method: 'GET',
    path: '/blogs',
    // name of the controller file & the method.
    handler: 'controller.getBlogs',
    config: {
      policies: [],
    },
  },
];

Enter fullscreen mode Exit fullscreen mode

This will be publicly accessible on api/blogs as we will see when testing our routes.

Import Private and Public Routes
Head over to ./src/plugins/content-publisher/server/src/routes/index.ts and import the routes we created above.

'use strict';

import admin from './admin';
import contentApi from './contentApi';

export default {
  'content-api': {
    type: 'content-api',
    routes: [...contentApi],
  },
  admin: {
    type: 'admin',
    routes: [...admin],
  },
};
Enter fullscreen mode Exit fullscreen mode

As seen in the code above, we have routes that should be accessible only on the admin panel. We specified this with the type: 'admin'. For the route that is available to the general API router, we specified type: 'content-api'.

Create Strapi Plugin Controllers

At this point, our app should break because the controllers we mentioned in the routes above haven't been created. Let's create them.

Update the controller file in ./src/plugins/content-publisher/server/src/controllers/controller.ts with the following code:

// Path: ./src/plugins/content-publisher/server/src/controllers/controller.ts

import type { Core } from '@strapi/strapi';

const controller = ({ strapi }: { strapi: Core.Strapi }) => ({
  // get blog entries
  async getBlogs(ctx) {},

  // get posts 
  async getPosts(ctx) {},

  // publish a blog post to medium
  async publishPostToMedium(ctx) {},

  // publish a blog post to dev.to
  async publishPostToDevTo(ctx) {},

  // search for a post
  async getSearchQuery(ctx) {},

  // delete a post
  async deletePost(ctx) {},

  // get a single post
  async getSinglePost(ctx) {},
});

export default controller;

Enter fullscreen mode Exit fullscreen mode

In the code above, we added the controller methods, which we will update and use later on in this tutorial.

Here is what each of the controller methods do:

  • getBlogs: This will help us fetch blogs that have been created.
  • getPosts: This will help us fetch posts for publishing.
  • publishPostToMedium: We will use this to publish a post to Medium.
  • publishPostToDevTo: With this, we can publish a post to Dev.
  • getSearchQuery: When we send a query request to fetch a post, this method will return the search result.
  • getSinglePost: This will allow us to get a single post.

Create Environment Variables

Inside the root of your project folder, locate the .env file and add the following environment variables to the existing ones:

MEDIUM_API_TOKEN="your medium api key"
MEDIUM_USER_ID="your medium user id"

DEVTO_API_KEY="your dev.to api key"
Enter fullscreen mode Exit fullscreen mode

In the code above, we created environment variables for the Medium and Dev APIs. The Medium API requires a user ID and an API token. Meanwhile, the Dev API requires an API key.

Let's obtain these credentials.

Obtain Medium and Dev API Keys

Before we continue with the next sections, we need to have some credentials in order to consume the Medium and Dev APIs.

How to Obtain Dev API Key

To obtain your Dev API key, follow the following instructions below:

  • Log in to your Dev account
  • Click on your profile picture and click settings
  • Click the Extensions tab
  • Scroll down to "DEV Community API Keys"
  • Generate a new API key

Generate Dev API Key.png
Generate Dev API Key

Generate a new key and add the value to the environment variable DEVTO_API_KEY.

How to Obtain Obtain Medium API Key

Obtain your Medium API key by following the instructions below:

  • Log in to your Medium account
  • Click on your profile picture and click settings
  • Click on the "Security and apps" tab
  • Scroll down to the bottom and click on "Integration tokens".
  • This will open a modal for you to copy your API key.
    Copy Medium API Key.png
    Copy Medium API Key

  • Get the token above and add it as a value to the environment variable MEDIUM_API_TOKEN.

  • With the API token above, we can now get the value for MEDIUM_USER_ID. Hence, to obtain your user ID, make a GET request to https://api.medium.com/v1/me. Ensure you add an authorization header to the request, which should be your API token.

Here is an example below

GET /v1/me HTTP/1.1
Host: api.medium.com
Authorization: Bearer 181d415f34379af07b2c11d144dfbe35d
Content-Type: application/json
Accept: application/json
Accept-Charset: utf-8
Enter fullscreen mode Exit fullscreen mode

Using Postman HTTP client:

Add API Key to request.png
Add API Key to request

Here is a successful response:
Successful Response.png
Successful Response

In the response above, the user ID, represented by id, is returned along with other user details. Get the user ID and add it as a value to the MEDIUM_USER_ID environment variable.

How to Create a New Entry Automatically Using Strapi Lifecycle Function

When we create a blog entry, we want to automatically create a corresponding post entry. As we said before, a post entry will contain details about the publishing of the blog entry, like the live Medium and Dev links, and relation to the blog entry from which it was created.

Navigate to the register lifecycle function file ./src/plugins/content-publisher/server/src/register.ts and replace the code inside with the following:

// Path: ./src/plugins/content-publisher/server/src/register.ts
import type { Core } from '@strapi/strapi';

const register = ({ strapi }: { strapi: Core.Strapi }) => {
  strapi.db.lifecycles.subscribe({
    // only for the blog collection type
    models: ['api::blog.blog'],
    // after a blog post is created
    async afterCreate(event) {
      // create new data
      const newData = {
        blog: event.result.documentId,
        mediumLink: null,
        devToLink: null,
      };

      // create new post
      await strapi.documents('plugin::medium-publisher.post').create({
        data: newData,
      });
    },
  });
};

export default register;

Enter fullscreen mode Exit fullscreen mode

The code above is a Strapi 5 lifecycle function that listens for events related to the Blog API collection type models: ['api::blog.blog']. Specifically, it subscribes to the afterCreate event, which triggers after a new blog entry is created.

When triggered, it creates a new entry in the content-publisher.post, which is the Post collection type of our plugin, with references to the blog post and placeholder values for mediumLink and devToLink

Create a new Blog Entry
Let's see if our lifecycle event works. Start by creating a new blog entry.

Create a Blog Entry.png
Create a Blog Entry

After creating the blog entry, navigate to the Post collection, and you will find a new entry automatically created upon creating the blog entry.

A Post Entry is Automatically created.png
A Post Entry is Automatically created

As you can see, new post entries are created automatically.

Now, create more blog entries so that we can fetch and display posts for publishing.

Fetch and Display Posts in Strapi Plugin

Posts have now been created. Let's display them on the admin panel of our plugin.

Step 1: Update Controller Method to Fetch Posts

Do you recall the getPosts() controller method we created some time ago inside the controller file ./src/plugins/content-publisher/server/src/controllers/controller.ts, let's modify it with the following:

... 
// get posts
async getPosts(ctx) {
  ctx.body = await strapi
    .plugin("content-publisher")
    .service("service")
    .getPosts();
}

Enter fullscreen mode Exit fullscreen mode

The controller method above retrieves posts by calling the getPosts() service, which we will create shortly, of our plugin "content-publisher", and sets the response body ctx.body to the result.

Step 2: Create a Service to Fetch Posts

Inside the ./src/plugins/content-publisher/admin/src/components directory, create a file called PublishingTable.tsx, and add the following code inside with the following:

// Path: ./src/plugins/content-publisher/admin/src/components/PublishingTable.tsx

import {
  Table,
  Thead,
  Tbody,
  Tr,
  Td,
  Th,
  Typography,
  Box,
  Link,
  Flex,
} from '@strapi/design-system';

import { Trash } from '@strapi/icons';

import axios from 'axios';
import { useState, useEffect, FormEvent } from 'react';

import formattedDate from '../utils/formattedDate';
import PublishButton from './PublishButton';
import MediumIcon from './MediumIcon';
import DevToIcon from './DevToIcon';

const PublishingTable = () => {
  const [posts, setPosts] = useState([]);

  return (
    <Box>
      <Box padding={8} margin={20}>
        <Table colCount={7} rowCount={posts.length + 1}>
          <Thead>
            <Tr>
              <Th>
                <Typography variant="sigma" textColor="neutral600">
                  Blog ID
                </Typography>
              </Th>
              <Th>
                <Typography variant="sigma" textColor="neutral600">
                  Date Created
                </Typography>
              </Th>
              <Th>
                <Typography variant="sigma" textColor="neutral600">
                  Blog Title
                </Typography>
              </Th>
              <Th>
                <Typography variant="sigma" textColor="neutral600">
                  Blog Link
                </Typography>
              </Th>
              <Th>
                <Flex gap={2} direction="row" alignItems="center">
                  {/* Medium icon */}
                  <MediumIcon />
                  <Typography variant="sigma">Medium</Typography>
                </Flex>
              </Th>
              <Th>
                <Flex gap={2} direction="row" alignItems="center">
                  {/* Dev.to icon */}
                  <DevToIcon />
                  <Typography variant="sigma">Dev.to</Typography>
                </Flex>
              </Th>
              <Th></Th>
            </Tr>
          </Thead>
          <Tbody>
            {posts.map((post: any) => (
              <Tr key={post.id}>
                <Td>
                  <Typography textColor="neutral800">{post.id}</Typography>
                </Td>
                <Td>
                  <Typography textColor="neutral800">
                    {formattedDate(post.blog?.updatedAt)}
                  </Typography>
                </Td>
                <Td>
                  <Typography textColor="neutral800">{post.blog.title.slice(0, 30)}...</Typography>
                </Td>
                <Td>
                  <Typography textColor="neutral800">
                    <Link
                      href={`http://localhost:1337/admin/content-manager/collection-types/api::blog.blog/${post.blog.documentId}`}
                    >
                      {post.blog.title.slice(0, 30)}...
                    </Link>
                  </Typography>
                </Td>
                <Td>
                  <PublishButton post={post} type="medium" />
                </Td>
                <Td>
                  <PublishButton post={post} type="devto" />
                </Td>
                <Td>
                  <Trash style={{ cursor: 'pointer', color: 'red' }} width={20} height={20} />
                </Td>
              </Tr>
            ))}
          </Tbody>
        </Table>
      </Box>
    </Box>
  );
};

export default PublishingTable;
Enter fullscreen mode Exit fullscreen mode

The PublishingTable React component above renders a table to manage and display posts. It uses the @strapi/design-system library for UI components and displays blog data such as ID, creation date, title, and links, along with publishing options for Medium and Dev.to via PublishButton component which we will create soon. It also contains components MediumIcon and DevToIcon which we will create soon.

The table dynamically maps over a posts state array (managed by useState) to populate rows, and includes a utility function formattedDate() (we will create this shortly) that format dates and a delete button represented by a trash icon.

So, let's create the PublishButton component and the formattedDate() utility function.

Step 4: Create a Date Formatter Utility Function

By default, Strapi returns dates in ISO 8601 format, e.g. 2025-01-06T22:09:08.655Z. However, we want to display the dates in a much friendlier way, like January 6, 2025.

Inside the ./src/plugins/content-publisher/admin/src/utils directory, create a file called formattedDate.ts and add the following code:

// Path: ./src/plugins/content-publisher/admin/src/utils/formattedDate.ts

const formattedDate = (isoDate: Date) => {
  // Create a Date object from the ISO string
  const date = new Date(isoDate);

  const result = date.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  });

  return result;
};

export default formattedDate;
Enter fullscreen mode Exit fullscreen mode

The formattedDate() function converts an ISO date string such as "2025-01-06T22:09:08.655Z" into a human-readable format (e.g., "January 1, 2025") using the toLocaleDateString method with U.S. English formatting.

Step 3: Create Other Components

In the PublishingTable component we created above, we imported PublishButton, MediumIcon and DevToIcon. Let's create them.

Create Publish Button Component
Inside the ./src/plugins/content-publisher/admin/src/components directory, create a new file called PublishButton.tsx and add the following code. This component will be responsible for publishing posts to different platforms.

// Path: ./src/plugins/content-publisher/admin/src/components/PublishButton.tsx

import { Box, Button, Typography, LinkButton, Flex, Link } from '@strapi/design-system';
import { Play, Check, Cursor } from '@strapi/icons';

const PublishButton = ({ post, type }: { post: any; type: string }) => {
  return (
    <Box>
      <Button style={bigBtn} size="S" startIcon={<Play />} variant="default">
        <Typography variant="pi">start</Typography>
      </Button>
    </Box>
  );
};

const bigBtn = {
  width: '100px',
};

export default PublishButton;
Enter fullscreen mode Exit fullscreen mode

It takes two parameters, the Post entry and the type "medium" or "devto".

Create Medium Icon Component
Inside the ./src/plugins/content-publisher/admin/src/components directory, create a new file called MediumIcon.tsx and add the following code:

// Path: ./src/plugins/content-publisher/admin/src/components/MediumIcon.tsx

import React from "react";

export default function MediumIcon() {
  return (
    <svg
      width="24px"
      height="24px"
      viewBox="0 0 24 24"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g id="SVGRepo_bgCarrier" stroke-width="0"></g>
      <g
        id="SVGRepo_tracerCarrier"
        stroke-linecap="round"
        stroke-linejoin="round"
      ></g>
      <g id="SVGRepo_iconCarrier">
        {" "}
        <path
          d="M13 12C13 15.3137 10.3137 18 7 18C3.68629 18 1 15.3137 1 12C1 8.68629 3.68629 6 7 6C10.3137 6 13 8.68629 13 12Z"
          fill="#0F0F0F"
        ></path>{" "}
        <path
          d="M23 12C23 14.7614 22.5523 17 22 17C21.4477 17 21 14.7614 21 12C21 9.23858 21.4477 7 22 7C22.5523 7 23 9.23858 23 12Z"
          fill="#0F0F0F"
        ></path>{" "}
        <path
          d="M17 18C18.6569 18 20 15.3137 20 12C20 8.68629 18.6569 6 17 6C15.3431 6 14 8.68629 14 12C14 15.3137 15.3431 18 17 18Z"
          fill="#0F0F0F"
        ></path>{" "}
      </g>
    </svg>
  );
}
Enter fullscreen mode Exit fullscreen mode

Create Dev Icon Component
Inside the ./src/plugins/content-publisher/admin/src/components directory, create a new file called MediumIcon.tsx and add the following code:

// Path: ./src/plugins/content-publisher/admin/src/components/MediumIcon.tsx

import React from "react";

export default function DevToIcon() {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      aria-label="dev.to"
      role="img"
      viewBox="0 0 512 512"
      width="24px"
      height="24px"
      fill="#000000"
    >
      <g id="SVGRepo_bgCarrier" stroke-width="0"></g>
      <g
        id="SVGRepo_tracerCarrier"
        stroke-linecap="round"
        stroke-linejoin="round"
      ></g>
      <g id="SVGRepo_iconCarrier">
        <rect width="512" height="512" rx="15%"></rect>
        <path
          fill="#ffffff"
          d="M140.47 203.94h-17.44v104.47h17.45c10.155-.545 17.358-8.669 17.47-17.41v-69.65c-.696-10.364-7.796-17.272-17.48-17.41zm45.73 87.25c0 18.81-11.61 47.31-48.36 47.25h-46.4V172.98h47.38c35.44 0 47.36 28.46 47.37 47.28zm100.68-88.66H233.6v38.42h32.57v29.57H233.6v38.41h53.29v29.57h-62.18c-11.16.29-20.44-8.53-20.72-19.69V193.7c-.27-11.15 8.56-20.41 19.71-20.69h63.19zm103.64 115.29c-13.2 30.75-36.85 24.63-47.44 0l-38.53-144.8h32.57l29.71 113.72 29.57-113.72h32.58z"
        ></path>
      </g>
    </svg>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create a Function to Fetch Posts

Implement a function to fetch posts. Add the following code to the PublishingTable component.

// ... other codes

const handleFetchPosts = async () => {
  try {
    // Get posts from content-publisher plugin
    const response = await axios.get(`/content-publisher/posts`);
    setPosts(response.data);
  } catch (error) {
    console.error("Error fetching posts:", error);
  }
};

useEffect(() => {
  handleFetchPosts();
}, []);

// ... other codes

Enter fullscreen mode Exit fullscreen mode

The handleFetchPosts() function above fetches blog posts from the /content-publisher/posts endpoint which we already created in our routes, using axios and updates the posts state with the response data, handling any errors by logging them to the console. It is invoked inside a useEffect hook to run once when the component mounts.

Here is what our plugin admin panel should look like:

Content Publisher Table.png
Content Publisher Table

πŸ‘‰ See the complete code of PublishingTable component here.

Publishing to Dev

Now, let's publish a post to Dev!

Server Side: Create Plugin Service and Modify Plugin Controller Method

Let's start by implementing this on the server side of our plugin.
1. Create a Service For Publishing Post to Dev
We will start by creating a service called publishPostToDevTo. Add the code below to the service file ./src/plugins/content-publisher/server/src/services/service.ts with the following code:

// ... other codes

/**
 * Publish Post to Dev.to
 */
async publishPostToDevTo(post: any) {
  try {
    // destructuring the post object
    const { title, content, canonicalUrl, tags, banner } = post.blog;
    // get the blog tags
    const blogTags = tags.map((tag) => tag.blogTag);

    // payload to be sent to dev.to
    const devToPayload = {
      article: {
        title,
        body_markdown: content,
        published: true,
        series: null,
        main_image: banner,
        canonical_url: canonicalUrl,
        description:
          content.length > 140 ? `${content.slice(0, 140)}...` : content,
        tags: blogTags,
        organization_id: null,
      },
    };

    // post
    const response = await axios.post(
      `https://dev.to/api/articles`,
      devToPayload,
      {
        headers: {
          "Content-Type": "application/json",
          "api-key": process.env.DEVTO_API_KEY,
        },
      },
    );

    // get the dev.to url
    const devToUrl = response.data?.url;

    // update the post with the dev.to link
    await strapi.documents("plugin::content-publisher.post").update({
      documentId: post.documentId,
      data: {
        devToLink: devToUrl,
      } as any,
    });

    // return the response
    return response.data;
  } catch (error) {
    return error.response.data
  }
}

// ... other codes
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ See complete code of the snippet above

Here is what the code above does:

  • Takes a Post entry as a parameter which will be sent from the admin panel or frontend.
  • Destructures the Post entry to get details of the blog such as title, content, canonicalUrl, tags, and banner
  • Maps through the tags object to return an array containing the tags as strings.
  • Creates a Dev API payload using the Dev request body schema. Learn more here.
  • Sends a POST request to https://dev.to/api/articles endpoint with the Dev API Key.
  • Retrieves the live link to the published post and updates the Post entry of our plugin if the API request is successful. Otherwise, an error is returned.

2. Update Plugin Controller
Update the plugin controller method publishPostToDevTo with the following code:

// ./src/plugins/content-publisher/server/src/controllers/controller.ts

import type { Core } from "@strapi/strapi";

const controller = ({ strapi }: { strapi: Core.Strapi }) => ({
  // get blog entries
  async getBlogs(ctx) {},

  // get posts
  async getPosts(ctx) {
    ctx.body = await strapi
      .plugin("content-publisher")
      .service("service")
      .getPosts();
  },

  // publish a blog post to dev.to
  async publishPostToDevTo(ctx) {
    ctx.body = await strapi
      .plugin("medium-publisher")
      .service("service")
      .publishPostToDevTo(ctx.request.body);
  },

  // publish a blog post to medium
  async publishPostToMedium(ctx) {},

  // search for a post
  async getSearchQuery(ctx) {},

  // delete a post
  async deletePost(ctx) {},

  // get a single post
  async getSinglePost(ctx) {},
});

export default controller;

Enter fullscreen mode Exit fullscreen mode

The publishPostToDevTo controller method is now modified to call the publishPostToDevTo service we created earlier.

Admin Panel

In the admin section, we will have to update the publish button to be able to publish post to Dev or Medium.

Update The Publish Button

// ... other codes

// Function to handle publishing the post
  const handlePublishPost = async () => {
    try {
      // Set loading to true
      setLoading(true);
      let endpoint;

      // Check if the type is medium or devto
      if (type === 'medium') {
        endpoint = '/content-publisher/publish-to-medium';
      } else {
        endpoint = '/content-publisher/publish-to-devto';
      }

      // Post the data
      await axios.post(endpoint, post);

      // Reload the page
      window.location.reload();
    } catch (error) {
      console.log(error);
    } finally {
      // Set loading to false
      setLoading(false);
    }
  };

// ... other codes
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ See complete code of the snippet above.

Here is what the code above does:

  • We updated the PublishButton button to handle publishing a post to either Medium or Dev.to based on the type prop.
  • It manages a loading state with useState and uses an axios.post request to send the post data to the appropriate plugin endpoint /content-publisher/publish-to-medium or /content-publisher/publish-to-devto. Recall the private routes we created earlier for our plugin.
  • The button dynamically updates its appearance and behavior, showing different states (e.g., "published," "publishing," or "start") and enabling users to visit the post's live link if already published.

Here is a demo of what we did above:

Publish to Dev With Strapi Plugin Demo

As seen above, we can now publish posts to Dev. Amazing!

Github Project Repo

You can find the complete project for this tutorial in this Github repo.

In the repo, you will find branches part-1 and part-2. They both represent the complete code for each part of this tutorial.

Conclusion

In this tutorial, we learned how to develop Strapi plugins. We have delved into initializing and customizing a Strapi plugin. We also looked at how to create content types for a Strapi plugin and pass data in a Strapi plugin. Finally, we consumed the Dev API using our Strapi headless CMS plugin to publish posts to dev.to.

In the next part of this tutorial, we will be able to publish posts to medium.com. Also, we will inject our plugin into the Blog collection type and implement search and pagination. See you in the next part.

Strapi Open Office Hours

If you have any questions about Strapi 5 or just would like to stop by and say hi, you can join us at Strapi's Discord Open Office Hours Monday through Friday at 12:30 pm - 1:30 pm CST: Strapi Discord Open Office Hours

Top comments (0)