In the previous part of this tutorial, we learned about how to develop Strapi plugins. We also delved into initializing and customizing a Strapi plugin. Furthermore, we looked at how to create content types for a Strapi plugin and pass data in a Strapi plugin. Finally, we consumed the Dev.to API using our Strapi plugin to publish posts to Dev.
This is the final part, where you will be able to publish posts to Medium, add pagination and search features.
Tutorial Outline
This article is in 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
Publish To Medium Using Medium API
With Strapi headless CMS, we can do much more. Let's publish a post to Medium from Strapi CMS!
Server Side: Create Plugin Service and Modify Plugin Controller Method
1. Create a Service for Publishing to Medium
We will create a service that will help us publish posts to Medium. Locate the service file ./src/plugins/content-publisher/server/src/services/service.ts
and add a new service method called publishPostToMedium
as shown below:
// Path: ./src/plugins/content-publisher/server/src/services/service.ts
// ...other codes
/**
* Publish Post to Medium
*/
async publishPostToMedium(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 medium
const mediumPayload = {
title,
content: `${title}\n![Image banner](${banner})\n${content}`,
canonicalUrl,
tags: blogTags,
contentFormat: "markdown",
};
// post
const response = await axios.post(
`https://api.medium.com/v1/users/${process.env.MEDIUM_USER_ID}/posts`,
mediumPayload,
{
headers: {
Authorization: `Bearer ${process.env.MEDIUM_API_TOKEN}`,
"Content-Type": "application/json",
},
},
);
// get the medium url
const mediumUrl = response.data?.data?.url;
// update the post with the medium link
await strapi.documents("plugin::content-publisher.post").update({
documentId: post.documentId,
data: {
mediumLink: mediumUrl,
} as Partial<any>,
});
// return the response
return response.data;
} catch (error) {
throw error;
}
}
π See complete code of the snippet above
Similar to publishPostToDevTo
, the publishPostToMedium
function above does the following:
- Takes a post object as an argument.
- Extracts relevant information from the
post.blog
object, includingtitle
,content
,canonicalUrl
,tags
, andbanner
. - Prepares a payload for the Medium API, formatting the content and tags appropriately.
- Makes a POST request to the Medium API to create a new post.
- After successfully posting to Medium, it retrieves the URL of the newly created Medium post.
- Uses Strapi's Document Service API to update the post entry. It calls the update method on the
strapi.documents
service for the "plugin::content-publisher.post
content-type. It provides thedocumentId
of the post to update. In the data object, themediumLink
field is set to the URL of the newly created Medium post.
2. Update publishPostToMedium
Controller
Update the plugin controller method publishPostToMedium
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) {
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('content-publisher')
.service('service')
.publishPostToDevTo(ctx.request.body);
},
// publish a blog post to medium
async publishPostToMedium(ctx) {
ctx.body = await strapi
.plugin('content-publisher')
.service('service')
.publishPostToMedium(ctx.request.body);
},
// search for a post
async getSearchQuery(ctx) {},
// delete a post
async deletePost(ctx) {},
// get a single post
async getSinglePost(ctx) {},
});
export default controller;
The publishPostToMedium
controller method is now modified to call the publishPostToMedium
service we created above.
Now, go ahead and publish a post to Medium.
Inject a Strapi Plugin to a Collection Type Using Injection Zones
So far, we have only been able to publish a post from the home page of our plugin. In this section, let's demonstrate the use of injection zones in a Strapi plugin.
The injection zone feature allows plugins to add custom UI elements to specific areas of the Strapi admin panel. In this case, our content-publisher
plugin will add a component to the right side of the edit view in the Content Manager. This is so we can publish directly from the Blog content manager.
Here is what it should look like:
As shown in the image above, we injected a component to help us publish a blog directly in the content manager.
Step 1: Create a Component for Injection
We will start by creating the component we want to inject into the component manager.
Mind you, we only want this component to be visible on the Blog collection type. So, we will need to use the unstable_useContentManagerContext
hook, which is a replacement for the useCMEditViewDataManager. This hook accesses data and functionality related to the Edit View in the Content Manager such as the ID and slug of the content type.
Inside the ./src/plugins/content-publisher/admin/src/components
folder, create the file InjectedComponent.tsx
and add the following code:
// Path: ./src/plugins/content-publisher/admin/src/components/InjectedComponent.tsx
import React from 'react';
import { Box, Flex, Typography, Divider } from '@strapi/design-system';
import { unstable_useContentManagerContext } from '@strapi/strapi/admin';
import axios from 'axios';
import { useEffect, useState } from 'react';
import PublishButton from './PublishButton';
import MediumIcon from './MediumIcon';
import DevToIcon from './DevToIcon';
export default function InjectedComponent() {
// get the blog id
const { slug, id } = unstable_useContentManagerContext();
const [post, setPost] = useState({
mediumLink: '',
devToLink: '',
blog: null,
});
// fetch single post
const fetchSinglePost = async () => {
const post = await axios.get(`/content-publisher/single-post?blogId=${id}`);
setPost(post.data);
};
// fetch single post
useEffect(() => {
fetchSinglePost();
}, []);
// check if the slug is not blog
if (slug !== 'api::blog.blog') return null;
return (
<Box>
{post ? (
<>
<Typography variant="beta" padding={30}>
Publish to:
</Typography>
<Box marginTop={5}>
<Flex
gap={{
large: 2,
}}
direction={{
initial: 'row',
}}
alignItems={{
initial: 'center',
}}
>
<Typography variant="sigma">Medium</Typography>
</Flex>
<PublishButton post={post} type="medium" />
<Divider />
</Box>
<Box padding={30}>
<Divider marginBottom={4} />
<Flex
gap={{
large: 2,
}}
direction={{
initial: 'row',
}}
alignItems={{
initial: 'center',
}}
>
<Typography variant="sigma">Dev.to</Typography>
</Flex>
<PublishButton post={post} type="devto" />
</Box>
</>
) : null}
</Box>
);
}
Let's break down the code above:
- The component uses the hook
unstable_useContentManagerContext
from Strapi's admin panel which retrieves theid
andslug
of the blog entry. - It fetches data about a single post using the API endpoint
/content-publisher/single-post
, which we created in the first part of this tutorial series. We will soon create the service and update the controller for this. It sends a request by adding the parameterblogId=${id}
. Recall that theid
was gotten using theunstable_useContentManagerContext()
hook. - The component only renders content with the
slug
asapi::blog.blog
, which corresponds to Blog collection type. - We imported the
PublishButton
, which accepts the post we fetched as a parameter. This will allow us to be able to publish to Dev and Medium from the Blog collection type.
Step 2: Create Service and Update Controller To Fetch Single Post
Update getSinglePost
Controller Method
Do you recall the getSinglePost
controller method we created in Part 1 of this tutorial that should be called when we make a GET
request to the /single-post
endpoint? We will update it 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) {
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('content-publisher')
.service('service')
.publishPostToDevTo(ctx.request.body);
},
// publish a blog post to medium
async publishPostToMedium(ctx) {
ctx.body = await strapi
.plugin('content-publisher')
.service('service')
.publishPostToMedium(ctx.request.body);
},
// get a single post
async getSinglePost(ctx) {
ctx.body = await strapi
.plugin('content-publisher')
.service('service')
.getSinglePost(ctx.request.query);
},
// search for a post
async getSearchQuery(ctx) {},
// delete a post
async deletePost(ctx) {},
});
export default controller;
In the code above:
- We used Strapi's plugin API to access a service method.
strapi.plugin('content-publisher')
accesses the "content-publisher" plugin. -
.service('service')
retrieves the main service of the plugin. -
getSinglePost(ctx.request.query)
calls thegetSinglePost
method of the service, which we will create soon, passing in the query parameters from the request. Recall that the query parameter is the blog ID (blogId=${id}
) as explained in the injected component above. - The result of the service method call is then assigned to
ctx.body
, which sets the response body for the HTTP request.
Create getSinglePost
Service
Inside the the service file, create the getSinglePost
service method.
// ./src/plugins/content-publisher/server/src/services/service.ts
// ... other codes
/**
* FETCH SINGLE Post
*/
async getSinglePost(query: any) {
// get blogId from query
const { blogId } = query;
try {
// find the post
const post = await strapi.documents('plugin::content-publisher.post').findFirst({
populate: {
blog: {
populate: ['tags'],
},
},
// filter the post by blogId
filters: {
blog: {
documentId: {
$eq: blogId,
},
},
},
});
// return the post
return post;
} catch (error) {
throw error;
}
}
// ... other codes
π See complete code of the snippet above.
Here is what the code above does:
- The
getSinglePost
method takes a query parameter of type any. - It extracts the
blogId
from the query parameter. The main functionality is wrapped in a try-catch block for error handling. -
strapi.documents('plugin::medium-publisher.post')
accesses the Post content-type of the 'content-publisher' plugin. -
findFirst()
is used to find the first document matching the given criteria. It populates the'blog'
field and its'tags'
. It looks for a post where the related blog'sdocumentId
equals the providedblogId
using the$eq
operator. Learn more about filtering. - The found post is then returned.
- If an error occurs, it's caught in the catch block and re-thrown.
Step 1: Register an Injection Zone in Strapi
Inside the ./src/plugins/content-publisher/admin/src/index.ts
file, which is the Admin Panel API entry file, register an injection zone:
// ./src/plugins/content-publisher/admin/src/index.ts
// from this
app.registerPlugin({
id: PLUGIN_ID,
initializer: Initializer,
isReady: false,
name: PLUGIN_ID,
});
// to this
app.registerPlugin({
id: PLUGIN_ID,
initializer: Initializer,
isReady: false,
name: PLUGIN_ID,
injectionZones: {
editView: {
"right-links": [],
},
},
});
We modified the registerPlugin
function by declaring an injection zone named 'right-links' in the 'editView' of the Content Manager. It's initially empty (an empty array). This will allow us to inject the component on the right side of the Blog content manager.
Step 2: Inject Component Using bootstrap()
Lifecycle
Let's inject the InjectedComponent
into the 'right-links' zone of the 'editView' using the bootstrap
function.
async bootstrap(app: any) {
app.getPlugin("content-manager").injectComponent("editView", "right-links", {
name: "content-publisher",
Component: () => <InjectedComponent />,
});
}
This injects the InjectedComponent
into the 'right-links' zone of the 'editView'. The InjectedComponent
component will now be rendered in the injection zone.
Here is the full code of our modified Admin Panel API entry file:
// Path: ./src/plugins/content-publisher/admin/src/index.ts
import React from 'react';
import { getTranslation } from './utils/getTranslation';
import { PLUGIN_ID } from './pluginId';
import { Initializer } from './components/Initializer';
import { PluginIcon } from './components/PluginIcon';
import InjectedComponent from './components/InjectedComponent';
export default {
register(app: any) {
app.addMenuLink({
to: `plugins/${PLUGIN_ID}`,
icon: PluginIcon,
intlLabel: {
id: `${PLUGIN_ID}.plugin.name`,
defaultMessage: PLUGIN_ID,
},
Component: async () => {
const { App } = await import('./pages/App');
return App;
},
});
app.registerPlugin({
id: PLUGIN_ID,
initializer: Initializer,
isReady: false,
name: PLUGIN_ID,
injectionZones: {
editView: {
'right-links': [],
},
},
});
},
async bootstrap(app: any) {
app.getPlugin('content-manager').injectComponent('editView', 'right-links', {
name: 'content-publisher',
Component: () => <InjectedComponent />,
});
},
async registerTrads({ locales }: { locales: string[] }) {
return Promise.all(
locales.map(async (locale) => {
try {
const { default: data } = await import(`./translations/${locale}.json`);
return { data, locale };
} catch {
return { data: {}, locale };
}
})
);
},
};
Step 3: Publish content directly from the Content Manager
Now, we can publish content directly from the Blog content manager of the admin panel.
ποΈ NOTE: If you can't get your component to be displayed. You can follow the instructions below.
Instruction on How to Display Injected Component
- Try changing the
./src/plugins/content-publisher/server/src/index.ts
file to./src/plugins/content-publisher/server/src/index.tsx
- change.ts
to.tsx
. - Correspondingly, modify
"source": "./admin/src/index.ts"
inside thepackage.json
file of your plugin(./src/plugins/content-publisher/package.json
) ... to"source": "./admin/src/index.tsx"
- change.ts
to.tsx
.
// ... other codes
"exports": {
"./package.json": "./package.json",
"./strapi-admin": {
"types": "./dist/admin/src/index.d.ts",
"source": "./admin/src/index.tsx", // change ts => tsx
"import": "./dist/admin/index.mjs",
"require": "./dist/admin/index.js",
"default": "./dist/admin/index.js"
},
// ... other codes
- Build your Strapi application by stopping the development server and running
strapi build
. Do the same for the plugin. - Restart your Strapi dev server with
npm run develop
. CD into the plugin folder and start your plugin in watch mode withnpm run watch
.
Add Pagination to Strapi Plugin
Now that we can publish posts to external platforms such as Medium and Dev, what happens when we have so many posts to publish. Let's add pagination in order to solve this issue.
Strapi Plugin Pagination Logic
We want to display only 5 posts per page. So, we will create the following variables:
-
currentPage
: Tracks the current page number. -
pageCount
: Total number of pages, calculated based on the total posts and posts per page. -
postsPerPage
: Number of posts to display per page (set to 5).
We will also create the function handleFetchPosts
to calculate the start
index for the API request based on the currentPage
. Then, an API request will be made to /content-publisher/posts
with query parameter for pagination: start=${start}
.
Lastly, we will render buttons based on the pagination number.
Here is what our pagination should look like:
Step 1: Update getPosts
Controller Method for Pagination
Let's update the getPosts
controller method to accept the parameters for pagination.
// ./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(ctx.query);
},
// publish a blog post to dev.to
async publishPostToDevTo(ctx) {
ctx.body = await strapi
.plugin('content-publisher')
.service('service')
.publishPostToDevTo(ctx.request.body);
},
// publish a blog post to medium
async publishPostToMedium(ctx) {
ctx.body = await strapi
.plugin('content-publisher')
.service('service')
.publishPostToMedium(ctx.request.body);
},
// get a single post
async getSinglePost(ctx) {
ctx.body = await strapi
.plugin('content-publisher')
.service('service')
.getSinglePost(ctx.request.query);
},
// search for a post
async getSearchQuery(ctx) {},
// delete a post
async deletePost(ctx) {},
});
export default controller;
In the code above, we updated the getPosts
controller method to accept query parameters.
Step 2: Update getPosts
Service for Pagination
Update the getPosts
service method with the code below in order to return paginations.
// ./src/plugins/content-publisher/server/src/services/service.ts
// ... other codes
const postPerPage = 5;
// ... other codes
/**
* GET Posts with Pagination
*/
async getPosts(query: any) {
try {
// get start from query params
const { start } = query;
// get total posts count
const totalPosts = await strapi.documents('plugin::content-publisher.post').count({});
// get posts
const posts = await strapi.documents('plugin::content-publisher.post').findMany({
populate: {
blog: {
populate: ['tags'],
},
},
// return only 5 posts from the start index
start,
limit: postPerPage,
});
// return the posts and total posts
return { posts, totalPosts };
} catch (error) {
throw error;
}
},
// ... other codes
π See complete code of the snippet above
We created a constant postPerPage
and gave it a value of 5
, the number of posts we want to see for a page. We updated the getPosts
method.
Here is what the getPosts
method does:
- Takes a
query
parameter, which we get thestart
index for pagination. - Counts the total number of posts using the
count
method and assigns the value tototalPosts
. - Retrieves the posts based on
findMany
. - The
findMany
takes parameterspopulate
, which populates theblog
relation and further populates thetags
relation within the blog. It also takes parametersstart
, the starting index for pagination, and parameterlimit
, which is the number of posts to return (defined by thepostPerPage
variable).
Step 3: Update PublishingTable
for Pagination
Here, we will update the PublishingTable
component to render as a pagination.
Import Pagination Components from Strapi Design System
Locate the ./src/plugins/content-publisher/admin/src/components/PublishingTable.tsx
file. We will start by importing components for pagination.
-
PageLink
for a page we want to visit. -
previousLink
for a previous page. -
Pagination
as parent component for all other components.
// Path: ./src/plugins/content-publisher/admin/src/components/PublishingTable.tsx
import {
// ... other components
PageLink,
Pagination,
PreviousLink,
NextLink,
} from '@strapi/design-system';
// ... other codes
Create Pagination Variables
We will create state variables pageCount
, currentPage
, and postsPerpage
as we mentioned earlier in our pagination logic.
// Path: `./src/plugins/content-publisher/admin/src/components/PublishingTable.tsx`
// ... other codes
const [pageCount, setPageCount] = useState(1);
const [currentPage, setCurrentPage] = useState(1);
const postsPerPage = 5;
// ... other codes
Update handleFetchPosts
function
We will update the handleFetchPosts
function to make a request with the start
query parameter.
/// Path: ./src/plugins/content-publisher/admin/src/components/PublishingTable.tsx
// ... other codes
const handleFetchPosts = async (page: number) => {
// Calculate the start index
const start = (page - 1) * postsPerPage;
try {
const response = await axios.get(
`/content-publisher/posts?start=${start}`,
);
setPosts(response.data.posts);
setPageCount(Math.ceil(response.data.totalPosts / postsPerPage));
} catch (error) {
console.error("Error fetching posts:", error);
}
};
// .. other codes
Create Function to Handle Page Change
We want a function to handle page changes when we switch between pagination tabs. Create the handlePageChange
function below.
// ./src/plugins/content-publisher/admin/src/components/PublishingTable.tsx
// ... other codes
const handlePageChange = (e: FormEvent, page: number) => {
// Prevent the default behavior of the link
if (page < 1 || page > pageCount) return;
setCurrentPage(page);
handleFetchPosts(page);
};
// ... other codes
The handlePage
function takes a parameter which is the page number and the event. It prevents the default behaviour of the button in which it will be invoked, sets current page to the page number clicked and then fetches posts.
Render Pagination
// ./src/plugins/content-publisher/admin/src/components/PublishingTable.tsx
// ... other codes
{
posts.length > 0 ? (
<Pagination activePage={currentPage} pageCount={pageCount}>
<PreviousLink
onClick={(e: FormEvent) => handlePageChange(e, currentPage - 1)}
>
Go to previous page
</PreviousLink>
{Array.from({ length: pageCount }, (_, index) => (
<PageLink
key={index}
number={index + 1}
onClick={(e: FormEvent) => handlePageChange(e, index + 1)}
>
Go to page {index + 1}
</PageLink>
))}
<NextLink
onClick={(e: FormEvent) => handlePageChange(e, currentPage + 1)}
>
Go to next page
</NextLink>
</Pagination>
) : null;
}
// ... other codes
Here is what we did in the code above:
- The
<Pagination>
component takes two props:activePage
andpageCount
-
<PreviousLink>
is used to navigate to the previous page. When clicked, it callshandlePageChange
with the current page number minus 1. - An array of page links is generated using Array.from().
- For each page, a
<PageLink>
component is created. It's given a unique key based on the index. The number prop is set to the page number. When clicked, it callshandlePageChange
with the corresponding page number. -
<NextLink>
is used to navigate to the next page. When clicked, it callshandlePageChange
with the current page number plus 1.
Let's see this in action:
Congratulations! We have added pagination. Next, let's add the Search functionality.
π See full code of the snippet for the PublishingTable
Component above.
Add Search Feature to Plugin
Pagination has now been added to our plugin, but what happens when we want to find a particular post by blog title? For this reason, let's add a search functionality.
Search Feature Logic
We want to implement a search feature to filter posts based on user input. So, we will create the following:
-
searchValue
: A variable that tracks the user's search input. -
handleSearchPost
: A function to make an API request to/content-publisher/search
with the search term (searchValue
) and fetch matching posts. The request is sent using the query parameter:search=${searchValue}
. If the search input is empty, the function will exit early to avoid unnecessary API calls. - The returned results will update the posts state to display only the matching posts.
Here is what the search bar should look like:
Step 1: Update getSearchQuery
Controller Method for Search
We will update the getSearchQuery
Controller method to accept query parameters.
// 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) {
ctx.body = await strapi.plugin('content-publisher').service('service').getPosts(ctx.query);
},
// publish a blog post to dev.to
async publishPostToDevTo(ctx) {
ctx.body = await strapi
.plugin('content-publisher')
.service('service')
.publishPostToDevTo(ctx.request.body);
},
// publish a blog post to medium
async publishPostToMedium(ctx) {
ctx.body = await strapi
.plugin('content-publisher')
.service('service')
.publishPostToMedium(ctx.request.body);
},
// get a single post
async getSinglePost(ctx) {
ctx.body = await strapi
.plugin('content-publisher')
.service('service')
.getSinglePost(ctx.request.query);
},
// search for a post
async getSearchQuery(ctx) {
ctx.body = await strapi
.plugin('content-publisher')
.service('service')
.getSearchQuery(ctx.request.query);
},
// delete a post
async deletePost(ctx) {},
});
export default controller;
Step 2: Create a getSearchQuery
Service Method for Search Feature
Inside the ./src/plugins/content-publisher/server/src/services/service.ts
file, we will create the getSearchQuery
method to help us fetch the requested query.
// Path: ./src/plugins/content-publisher/server/src/services/service.ts
// ... other codes
/**
* Search Query
*/
async getSearchQuery(query: { search: string; start: number }) {
try {
// get search and start from query
const { search, start } = query;
// find total posts
const totalPosts = await strapi
.documents("plugin::medium-publisher.post")
.findMany({
filters: {
blog: { title: { $contains: search } },
},
});
// find posts
const posts = await strapi
.documents("plugin::medium-publisher.post")
.findMany({
populate: {
blog: {
populate: ["tags"],
},
},
// filter only blog titles that contains the search query
filters: {
blog: { title: { $contains: search } },
},
// return only 5 posts from the start index
start: start * postPerPage,
limit: postPerPage,
});
// return the posts and total posts
return { posts, totalPosts: totalPosts.length };
} catch (error) {
throw error;
}
},
// ... other codes
π See complete code of the snippet above
Here is what the method above does:
- Takes a query object as an argument, which contains
search
(the search term) andstart
(the starting index for pagination) properties. - Uses the
$contains
filter to find posts where the blog title contains the search term. - The second query includes pagination (
start
andlimit
) and populates the blog relation along with its tags. - The method then returns an object containing the found posts and the total number of posts matching the search criteria.
Step 3: Update PublishingTable
Component for the Search Feature
Let's update the PublishingTable
component file ./src/plugins/content-publisher/admin/src/components/PublishingTable.tsx
so as to implement and display the search feature.
Import Search Bar Components
We will import components from the Strapi design system that exists for the search user interface.
// Path: ./src/plugins/content-publisher/admin/src/components/PublishingTable.tsx
// ... other codes
import {
// ... other codes
SearchForm,
Searchbar,
}
// ... other codes
In the code above, we imported the SearchForm
component which is the form for the search. Also, we imported the Searchbar
component which will serves as the input of our search value or key.
Create handleSearchPost
Method
Create a method called handleSearchPost
, which will handle search requests for posts.
// Path: ./src/plugins/content-publisher/admin/src/components/PublishingTable.tsx
// ... other codes
const handleSearchPost = async (event: FormEvent, page: number | null) => {
event.preventDefault(); // Prevent the default form submission behavior
let start;
if (page === null) {
start = 0;
} else {
start = (page - 1) * postsPerPage; // Calculate the start index for the paginated search
}
// If the search input is empty, return early
if (!searchValue.trim()) {
return;
}
try {
// Make a GET request to the search endpoint
const response = await axios.get(
`/content-publisher/search?start=${start}&search=${searchValue}`,
);
setPosts(response.data.posts); // Assuming the API returns matching posts
setPageCount(Math.ceil(response.data.totalPosts / postsPerPage));
} catch (error) {
console.error("Error searching posts:", error);
}
};
// ... other codes
In the code above, the handleSearchPost
sends a search query to /content-publisher/search
with pagination (start and search query parameters). It updates posts
and pageCount
based on the API response.
Modify handlePageChange
Method to Include Search
Let's modify the handlePageChange
method to include the handleSearchPost
function.
// Path: ./src/plugins/content-publisher/admin/src/components/PublishingTable.tsx
// ... other codes
const handlePageChange = (e: FormEvent, page: number) => {
if (page < 1 || page > pageCount) return;
setCurrentPage(page);
if (searchValue) {
handleSearchPost(e, page);
} else {
handleFetchPosts(page);
}
};
// ... other codes
The handlePageChange
now updates currentPage
and fetches data for the selected page. It determines whether to fetch default posts (handleFetchPosts
) or search results (handleSearchPost
) based on searchValue
.
Render Search Bar
Let's render the search bar in the PublishingTable
component.
// Path: ./src/plugins/content-publisher/admin/src/components/PublishingTable.tsx
// ... other codes
<SearchForm
onSubmit={(e: FormEvent) => {
handleSearchPost(e, null);
}}
>
<Searchbar
size="M"
name="searchbar"
onClear={() => {
setSearchValue("");
handleFetchPosts(1);
}}
value={searchValue}
onChange={(e: any) => setSearchValue(e.target.value)}
clearLabel="Clearing the plugin search"
placeholder="e.g: blog title"
>
Searching for a plugin
</Searchbar>
</SearchForm>;
// ... other codes
The code above renders a search form with a search bar to filter posts based on user input. When the form is submitted, it triggers the handleSearchPost
function to fetch filtered results. The onClear
handler resets the search value and fetches the default posts for page 1, while onChange
updates the searchValue
state as the user types.
Here is how the search works:
π See full code snippet for the PublishingTable
Component
Add Delete Feature
What happens when we don't need a particular post entry?
For this reason, we want to be able to delete a post. So, let's implement a delete feature. All we need to do is send a DELETE
request with the post ID as a query parameter to the /delete-post
endpoint.
Update deletePost
Controller Method
In the controller file ./src/plugins/content-publisher/server/src/controllers/controller.ts
, update the deletePost
controller method to accept query parameters:
// 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) {
ctx.body = await strapi.plugin('content-publisher').service('service').getPosts(ctx.query);
},
// publish a blog post to dev.to
async publishPostToDevTo(ctx) {
ctx.body = await strapi
.plugin('content-publisher')
.service('service')
.publishPostToDevTo(ctx.request.body);
},
// publish a blog post to medium
async publishPostToMedium(ctx) {
ctx.body = await strapi
.plugin('content-publisher')
.service('service')
.publishPostToMedium(ctx.request.body);
},
// get a single post
async getSinglePost(ctx) {
ctx.body = await strapi
.plugin('content-publisher')
.service('service')
.getSinglePost(ctx.request.query);
},
// search for a post
async getSearchQuery(ctx) {
ctx.body = await strapi
.plugin('content-publisher')
.service('service')
.getSearchQuery(ctx.request.query);
},
// delete a post
async deletePost(ctx) {
ctx.body = await strapi
.plugin('content-publisher')
.service('service')
.deletePost(ctx.request.query);
},
});
export default controller;
Create a Service For Deleting a Post
Inside the ./src/plugins/content-publisher/server/src/services/service.ts
, create a service to delete a post.
// Path: ./src/plugins/content-publisher/server/src/services/service.ts
// ... other codes
/**
* DELETE Post
*/
async deletePost(query: { postId: string }) {
try {
// get postId from query
const { postId } = query;
// delete the post
await strapi
.documents("content::medium-publisher.post")
.delete({ documentId: postId });
return "Post deleted";
} catch (error) {
console.error("Error deleting post:", error);
throw error;
}
},
// ... other codes
π See complete code of the snippet above
In the deletePost
function above, we did the following:
- Takes a query parameter with a
postId
property of type string. - Inside a try-catch block, it extracts the
postId
from the query object using destructuring. - It then uses Strapi's Document Service API to delete the post.
Update PublishingTable
for Deletion of Posts
Inside the PublishingTable
component, we will have to create a method to handle the deletion of posts. Create the method below:
// Path: ./src/plugins/content-publisher/admin/src/components/PublishingTable.tsx
// ... other codes
const handleDeletePost = async (id: string) => {
const response: Response = await axios.delete(
`/content-publisher/delete-post?postId=${id}`,
{},
);
handleFetchPosts(1)
};
// ... other codes
Now, add the onClick
event listener to the Trash
icon to call the handleDeletePost
function when it is clicked.
// Path: ./src/plugins/content-publisher/admin/src/components/PublishingTable.tsx
// ... other codes
<Trash
onClick={() => {
handleDeletePost(post.documentId);
}}
style={{ cursor: "pointer", color: "red" }}
width={20}
height={20}
/>;
// ... other codes
Here is what deleting a post looks like:
π See code to full code of the snippet above for the PublishingTable
component
What's Next?
Congratulations on building your first Strapi CMS plugin. Here are some suggestions on how to improve the Content Publisher plugin.
- Implement batch publishing and deletion.
- Add toast notifications when actions are performed.
- Handle errors properly and display them to users.
- Add other external platforms of your choice.
We can't wait to see you build amazing plugins.
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. The main
branch will hold future changes to this project.
Conclusion
In this final part of the Strapi plugin tutorial, we have learnt how to publish posts to Medium, add pagination, implement search functionality, and use injection zones in the Strapi admin panel and more.
What are you waiting for? Let's start building plugins!
Aside from building amazing plugins, Strapi headless CMS offers a range of features tailored to meet your business needs. Try Strapi today.
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
Strapi 5 is available now! Start building today!
Top comments (0)