A blog integration for your MedusaJS admin page, enabling you to create and manage blog articles directly from the admin interface. This plugin extends the capabilities of MedusaJS, a powerful headless commerce platform, by adding a dedicated blogging feature. With this integration, store administrators can effortlessly create, edit, and publish blog posts to enhance their content marketing strategy, engage customers, and improve SEO.
The Medusa-Plugin-Blogger is designed to provide a seamless user experience, utilizing modern tools and libraries for rich text editing and tag management. By incorporating this plugin into your MedusaJS setup, you can maintain a cohesive content and commerce environment, streamlining your workflow and ensuring consistency across your brand's digital presence.
Run the following command in the directory of the Medusa backend:
yarn add medusa-plugin-blogger
In medusa-config.js
add the following to the plugins
array:
const plugins = {
///...other plugins
{
resolve: 'medusa-plugin-blogger',
options: {
enableUI: true,
},
}
}
Run the following command from the root of the project to update database with a new table required for storing product variant
npx medusa migrations run
A file service is required for this plugin to work.
The max size of the body that an endpoint can receive is 10000000B
which is the equivalent to 10MB
, enough to store tens of academic papers inside an article object. This is enough to store approximately 1.600.000 words In case you go over this threshold with your article, you will not be able to save it.
GET /store/blog/articles
Returns a json object of all the articles respecting the conditions passed as query parameters, this endpoint accepts query parameters to do a conditional search using TypeORM find parameter, blog_articles
can be search using any filter found in the PostgreSQL documentation, because only query parameters are accepted, a function to convert objects to query parameters is provided down here:
export const objectToQueryString = (obj) => {
return Object.keys(obj)
.map(key => {
if (Array.isArray(obj[key]) || typeof obj[key] == "object") {
return encodeURIComponent(key) + '=' + encodeURIComponent(JSON.stringify(obj[key]))
} else {
return encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])
}
}).join('&');
}
Here is an example with a commonly found object:
const example_object = {
"where": {
"id": "01HZHPGPY4MTR97EVX6FDDEXZE"
},
"take": 7,
"skip": 2,
"select": ["title", "subtitle", "body"],
"order": {
"created_at": "ASC"
}
}
console.log(objectToQueryString(example_object))
Output: where=%7B%22id%22%3A%2201HZHPGPY4MTR97EVX6FDDEXZE%22%7D&take=7&skip=2&select=%5B%22title%22%2C%22subtitle%22%2C%22body%22%5D&order=%7B%22created_at%22%3A%22ASC%22%7D
This output may look strange at first, almost impossible to understand, but the api routes already parse this url properly into the object that will be passed to search the database.
Find operators: if you want to use a specific find operator like Like
or ILike
you can send an object like this:
{
"where": {
"title": {
"find_operator": "Like",
"value": "%Hello World%"
}
}
}
The supported find operators are: ILike, Like, Raw, LessThan, LessThanOrEqual, MoreThan, MoreThanOrEqual
, you can find the meaning of these operators in the official TypeORM documentation. If no supported find operator is found in the object the value will be searched as it is without throwing any error.
In the where
field if you search for created_at
and updated_at
key values, the plugin will automatically try to convert the string values into dates using new Date(string_value)
, if no date is created than the value searched will be a string, even if it will not give any results because the type of created_at
and updated_at
is DATE, be mindful when using these two fields as you'll need to provide a correct date to receive an appropriate result.
See the Typeorm documentation to understand better what every of these parameters does, keep in mind that the behavior of where is a little bit different from the one in the documentation, there are some things to take into consideration:
- The search works using an equal condition, for keys where the value is not
id
ortags
, for example if you want to search for an element that has the title "I like pizza", the query parameters that you'll need to send in the request is{ where { title: "I like pizza" } }
- IDs can be fetched with these two formats
blog_article_01HZHPGPY4MTR97EVX6FDDEXZE
and01HZHPGPY4MTR97EVX6FDDEXZE
, both versions are valid - When adding a
tags
key to the body, they must be an array and the database will be searched for an element that has at least all the tags inside thetags
value in the query parameters - We do not yet support searches over the body of the article
- If you want an OR condition in your
where
value you need a list where you need to put all the separate conditions like this[ {"title" : "Hello World!", "subtitle": "Awesome article"}, {"title" : "Example", "subtitle": "Awesome example"} ]
- If you want an AND condition in your
where
value for a specific field, you need to list all the conditions for that key, let's say you want a date range you can do it like this{ "created_at": [{"find_operator": "LessThan","value": "2024-06-14"},{"find_operator": "MoreThanOrEqual","value": "2024-06-12"}]}
- The body is sanitized automatically from SQL injections
Returns a json object of all the articles respecting the conditions passed as query parameters, it works that same way as the homonymous store API routes.
Create a new blog article.
NOTE: No server validation is done to check if the url-slug
value is a valid url as this is already done in the frontend and it is assumed that the admin would not do anything to harm its store.
Return a json object of the article having the id in the url.
Modify an already existing blog article, this route requires the new BlogArticle
object as well as the id in the url because the old object if completely overwritten with the new one passed over the body.
Delete an article having the id in the url.
The way Medusa Plugin Blogger is developed makes it easy to integrate the plugin with the functionality of MedusaJS and your storefront, there are only two simple steps that you need to follow:
- Fetch
GET /store/blog/articles
with the conditions that you like (like shown above) - You don't need to process further any part of the article because they are already in a form that can be displayed in HTML (for example
title
is already a string that can be displayed,tags
is already an array that can be displayed) except for the body which you'll need to process into HTML, the editor used for the body is EditorJS which provides a clear JSON object for the body https://editorjs.io/saving-data/
EditorJS divides the body into blocks, which you can easily iterate through and add your logic for every type of block that you use. Depending on what features of the editor you are going to use you can implement text highlighting, code blocks, images, headings, and much more... Everything is left up to your implementation!
To make this more clear here is an example a body with the result after:
{
"time": 1718394016975,
"blocks": [
{
"id": "bmQCD-2wEi",
"data": {
"text": "Databases play a crucial role\n in the development process, but how exactly do they work? In this \narticle, we’ll dive into the internal architecture of MongoDB, a popular\n no-SQL database, to provide a comprehensive explanation."
},
"type": "paragraph"
},
{
"id": "pbwAdmpETJ",
"data": {
"text": "Understanding how MongoDB works behind the scenes can be incredibly useful for developers looking to leverage its unique features and capabilities. So let’s dive in!"
},
"type": "paragraph"
},
{
"id": "-B3th1nCj5",
"data": {
"text": "Index",
"level": 1
},
"type": "header"
},
{
"id": "VkmuZ6qh-h",
"data": {
"items": [
{
"items": [],
"content": "What is a database?"
},
{
"items": [],
"content": "What is MongoDB?"
},
{
"items": [],
"content": "SQL vs no-SQL databases"
},
{
"items": [],
"content": "What is JSON?"
},
{
"items": [],
"content": "What the hell is BSON?"
},
{
"items": [],
"content": "What is Mongoose?"
}
],
"style": "ordered"
},
"type": "list"
}
],
"version": "2.29.1"
}
This is the body of the article below:
Medusa-Plugin-Blogger relies on several key dependencies to provide a rich user experience and robust functionality:
-
Editor.js: A block-styled editor that allows for rich text content creation. Editor.js is highly modular and extensible, and the plugin leverages several Editor.js tools including:
-
React Dropzone: A simple React component for creating file upload zones. This is used in the blog plugin to facilitate image and file uploads directly within the admin interface.
-
Tagify: A powerful tagging library that provides an easy-to-use interface for adding and managing tags. Tagify ensures that blog articles can be tagged efficiently, enhancing content categorization and searchability.
The BlogArticle entity requires only draft as a mandatory column, this is already handled by the store frontend but there might be need for a custom implementation if working with API routes directly. The choice of not making more columns mandatory was made because the implementation and use the plugin depends strictly on your storefront.
@Entity()
export class BlogArticle extends BaseEntity {
@PrimaryGeneratedColumn()
id: string;
@Column({ nullable: true })
author: string;
@Column('text', { array: true, nullable: true })
tags: string[];
@Column({ nullable: true, unique: true })
seo_title: string;
@Column({ nullable: true })
seo_keywords: string;
@Column({ nullable: true, unique: true })
url_slug: string;
@Column({ nullable: true, unique: true })
seo_description: string;
@Column({ nullable: true })
thumbnail_image: string;
@Column({ nullable: true })
title: string;
@Column({ nullable: true })
subtitle: string;
@Column('json', { nullable: true, array: false })
body: any; // Assuming body will be a complex JSON structure
@Column("text", { array: true, nullable: true})
body_images: string[];
@Column({ nullable: false })
draft: boolean;
@BeforeInsert()
private beforeInsert(): void {
this.id = generateEntityId(this.id, "blog_article")
}
}
This project is licensed under the MIT License. See the LICENSE file for details.