CMS for 0 rubles

Hello everyone! My name is Alexander, I am a frontend developer at KTS. Today I will tell you about Strapi CMS, analyze its use cases using specific examples and share ways to simplify working with it.

Let me start with a little background. Our company often develops projects where it is necessary to regularly update and customize the site content. For such cases, we use various CMS systems that allow you to work with content using a graphical interface.

When using such a CMS system, you want it to be responsible exclusively for content and not impose any restrictions on the UI, so when choosing, we focused on Headless CMS. Headless CMS are systems that provide content administration functionality (admin panel) and generate an API to which any client can be connected (web application, mobile application, etc.). Since Headless CMS is just an API, they do not limit the client in any way and do not affect the implementation of other backend services.

We chose Strapi because it is one of the most popular solutions today. The system is open source, and although it has a paid version, the free functionality is quite sufficient for work. We use the Community Edition (62k+ stars and almost 7k forks on github). In this article, I will share our experience working with Strapi and clearly describe how it can be used to solve practical problems.

Table of contents:

What Strapi Can Do

With Strapi, a developer can define collections of data that will be accessible via API from the CMS, and also give them a specific structure. Strapi automatically generates the entire boilerplate for performing CRUD operations on these collections of data, namely:

Once developers have set up the data structure, CMS users (such as content managers) can populate collections with content. This data can then be queried via the API.

The sequence of work with Strapi can be represented in the form of the following diagram:

How Strapi Works

How Strapi Works

How to create a project on Strapi

To create a new project, you need to run the following command:

yarn create strapi-app kts-strapi-project --quickstart

You will get a project with the following structure:

Project structure after creation

Project structure after creation

The following folders will contain the key logic for working with Strapi and data collections:

To run the project, run the following command:

yarn develop

The system will prompt you to register. After registration, you will be able to log in using the created credentials:

Registration in CMS

Registration in CMS

Strapi Operating Modes

Strapi has 2 modes: Content-Type Builder And Content Manager.

Content-Type Builder is only available in dev mode. Dev mode is a locally running Strapi server. On it, developers configure the structure of collections, determine what properties and attributes (in other words, fields) they have. After saving the collection, code generation of json schemas, migrations for the database and the server boilerplate occurs.

Content-Type Builder mode

Content-Type Builder mode

Content Manager is available in both dev and prod modes. Prod mode is a mode in which the structure of collections cannot be changed, it exists for filling collections with data.

Content Manager Mode

Content Manager Mode

How to create collections in Strapi

For clarity, let's consider the work of Strapi using the example of a personal account project for a university student. First, we will create collections with a minimum set of properties, and then supplement them as we get acquainted with the capabilities of Strapi. First, we will introduce some collections that will be used on our site.

  1. Collection “Student” with the following fields:

  2. Collection “Specialty” with the following fields:

Types of data collections

There are three types of collections in Strapi – Single Type, Collection Types And Component.
Each of them is suitable for a specific purpose.

Single Type is suitable if you are sure that the given collection is stored in a single copy. For example, you will probably need the following single type collections:

Collection Types suitable if the collection is a list of data.
For example, these could be:

Components – these are auxiliary entities. They cannot be requested from the API as a separate list, but can be included as a field for a Single Type, Collection Type, or another component. Examples of Components can be:

Data types

Let's create two collections of the Collections Types type: “Student” and “Specialty”.

When creating, you need to specify Display Name (display name of the collection in Strapi), and API ID in singular and plural – they will be used further to perform CRUD operations with collections.

Creating Collection Type

Creating Collection Type “Student”

When creating a collection, you must assign a type to each property:

Data types of properties when creating a collection

Data types of properties when creating a collection

Example of creating a collection

Let's create a “Student” collection with the structure we designed above – add the fields “name”, “surname” and “date of birth”:

Structure Collection Type “Student”

Structure Collection Type “Student”

Next, we will create the “Specialty” collection in a similar way:

Collection Type Structure

Collection Type Structure “Specialty”

Next, we need to add a relationship for the Student and Specialty collections so that we can request all students from a specific specialty from the API or find out the specialty in which a particular student is studying. To do this, we will add a specialty field with the Relation type to the Student collection. Each specialty has many students, so the relationship will be one-to-many.

First, let's add a “specialty” field of type Relation:

Adding the

Adding the “specialty” field

For the Student collection, we will add the “photo” field with the Media type. It is worth noting that when adding a Media type field, you can set restrictions on data formats. By default, there are any files. You can limit: only images, only videos, or only pdf:

Collection Type

Collection Type “Student” structure after adding the “specialty” field

Let's assume that each specialty has a link to a website with its detailed description. Let's add the “link” field to the specialty collection. A link is a combination of a title and an address. As discussed earlier, a link needs to be made a component because it does not exist on its own and does not need to be retrieved from the API. We also need to add the “specialty_link” field to the “Specialty” collection. To do this, we will create a corresponding “Link” component in advance, which will consist of a title and a heading:

Creating a Link Component

Creating a Link Component

Structure of the

Structure of the “Link” component

Next, we reuse the created component in the “Specialty” collection:

Reusing the Link component inside the Specialty Collection Type

Reusing the Link component inside the Specialty Collection Type

Code generation

After saving each of the collections, the code generation of the schemes is automatic. In fact, the code is generated according to the principles of the Model-Routes-Controllers-Service architecture, only instead of Model the directory is called content-types. You can read more about this architecture Here.

The logic for getting collections is also generated, and all of this is stored in the following folders:

  • content-types: this is where the collection schema that comes with the API is stored;

  • controllers: this is where the logic is stored that processes the HTTP request when accessing the API: validation and parsing of request parameters occurs, the structure of the response from the server is configured. Controllers use services internally to access the database. Controllers, unlike services, cannot be reused;

  • routes: This is where the list of endpoints of this collection that are available for retrieval is stored;

  • services: here the logic of accessing the database is configured. Also, it is in services that some specific business logic for the application should be written: for example, sending a letter to the user's email (this is easy to do with plugins). Services are used inside controllers, and can also reuse each other.

Autogenerated files for the Student collection

Autogenerated files for the Student collection

The result is the following files:

  • specialty files:

  • student files:

It is worth noting that after creating a collection, its Singular ID and Plural ID cannot be changed from the interface – they can only be changed by editing the code in the files above, but such a change often leads to errors in migrations. If you still need to change the identifiers for a collection, the most reliable way is to delete it and re-create it.

Example of Singular Id and Plural ID for the Student collection

Example of Singular Id and Plural ID for the Student collection

How to work with API

Once the code is generated, you can move on to working with the API and filling the collections with content. In this section, we will continue to consider the functionality of Strapi using the example of a student's personal account project.

Plugin for autogenerating swagger

Strapi allows you to install plugins that make it easier to configure and test collections. One such plugin, strapi documentation, allows you to get an auto-generated swagger. To install it, run the following command:

yarn strapi install documentation

After installation, documentation will be available at:

http://localhost:{PORT}/documentation/v1.0.0.

Swagger project

Swagger project

Adding data to a collection

Using the Content Manager mode, we will create several instances of the Student collection and fill them with data. It is important to note here that by default, an instance of the collection is created in the state Draft – this means that it will not be accessible via the API. In order to open access to the created instance, you need to go to its editing mode and click Publish.

Adding data to the Student collection

Adding data to the Student collection

OnSetting up access

By default, Strapi makes all collections private – you can only get them by sending a generated token in the header. To make a collection public, you need to open the “Roles” section in the settings. It will be located at:

http://localhost:1338/admin/settings/users-permissions/roles.

In this section you can choose which collections will be accessible to authorized and unauthorized users.

Privacy settings for HTTP requests

Privacy settings for HTTP requests

Public Requests

Let's specify what types of operations an unauthorized user can perform with the Student collection. Reading the collection and a single instance is enough for us, so we'll check the find and findOne flags.

Setting Operation Types for HTTP Requests

Setting Operation Types for HTTP Requests

Private requests

If we want some API requests to be available only to authorized users, we should create an API Token, which will have to be sent with each request. This can be done at:

http://localhost:1338/admin/settings/api-tokens/create.

Creating an authorization token

Creating an authorization token

The value of the generated token must be passed in the header for each request. Authorizationin swagger this is done by pressing a button Authorize):

Sending Authorization http header inside swagger

Sending Authorization http header inside swagger

Request parameters

Let's look at the query parameters for getting the Student collection. They fall into two categories.

The first category is those parameters that are related exclusively to pagination.

query parameters of the Collection Type

query parameters of the query of Collection Type “Student” related to pagination

The second category is parameters that help you customize which fields you want to query from collection instances and their order.

query parameters of the query for Collection Type

query parameters of the Collection Type “Student” related to sorting and filters

I suggest taking a closer look at the parameters of this category:

You can read more about the parameters in Strapi documentation.

Example request

Let's make a request for a list of students:

http://localhost:1338/api/students

And we get the following answer:

Reply to request
{
  "data": [
    {
      "id": 1,
      "attributes": {
        "name": "Арсений",
        "surname": "Ковалев",
        "birthday_date": "2004-07-14T20:00:00.000Z",
        "createdAt": "2024-07-17T09:16:31.088Z",
        "updatedAt": "2024-07-21T15:41:59.989Z",
        "publishedAt": "2024-07-17T15:43:49.701Z"
      }
    }
  ],
  "meta": {
    "pagination": {
      "page": 1,
      "pageSize": 25,
      "pageCount": 1,
      "total": 1
    }
  }
}

Please note: our collection instance is wrapped in a structure of the form { id, attributes }. Strapi does this with each instance of the collection, as well as with the nested entities we talked about above.

You can also notice that in the response to the request we did not receive the fields speciality And image. This is because they are nested entities. As I mentioned earlier, Strapi does not add them to the response by default.

To get information about a student's specialty and photo from the API, you need to add a parameter populate and specify in it what specific properties you want to obtain. Here you can read about how to correctly describe populate.

Let's add information about the fields we need to the query, and we'll get a query like this:

http://localhost:1338/api/students?populate=speciality,photo

With parameter populate The answer has changed to include fields speciality And photo.

New response to request
{
  "data": [
    {
      "id": 1,
      "attributes": {
        "name": "Арсений",
        "surname": "Ковалев",
        "birthday_date": "2004-07-14T20:00:00.000Z",
        "createdAt": "2024-07-17T09:16:31.088Z",
        "updatedAt": "2024-07-21T20:27:58.544Z",
        "publishedAt": "2024-07-17T15:43:49.701Z",
        "speciality": {
          "data": {
            "id": 1,
            "attributes": {
              "createdAt": "2024-07-17T09:17:03.596Z",
              "updatedAt": "2024-07-17T15:17:34.613Z",
              "publishedAt": "2024-07-17T14:59:42.231Z",
              "name": "Прикладная математика и информатика",
              "code": "01.03.02",
              "duration": 4
            }
          }
        },
        "photo": {
          "data": {
            "id": 1,
            "attributes": {
              "name": "2024-06-30 12.45.19.jpg",
              "alternativeText": null,
              "caption": null,
              "width": 960,
              "height": 1280,
              "formats": {
                "thumbnail": {
                  "name": "thumbnail_2024-06-30 12.45.19.jpg",
                  "hash": "thumbnail_2024_06_30_12_45_19_9e691ba632",
                  "ext": ".jpg",
                  "mime": "image/jpeg",
                  "path": null,
                  "width": 117,
                  "height": 156,
                  "size": 4.82,
                  "sizeInBytes": 4815,
                  "url": "/uploads/thumbnail_2024_06_30_12_45_19_9e691ba632.jpg"
                },
                "small": {
                  "name": "small_2024-06-30 12.45.19.jpg",
                  "hash": "small_2024_06_30_12_45_19_9e691ba632",
                  "ext": ".jpg",
                  "mime": "image/jpeg",
                  "path": null,
                  "width": 375,
                  "height": 500,
                  "size": 32.88,
                  "sizeInBytes": 32882,
                  "url": "/uploads/small_2024_06_30_12_45_19_9e691ba632.jpg"
                },
                "medium": {
                  "name": "medium_2024-06-30 12.45.19.jpg",
                  "hash": "medium_2024_06_30_12_45_19_9e691ba632",
                  "ext": ".jpg",
                  "mime": "image/jpeg",
                  "path": null,
                  "width": 563,
                  "height": 750,
                  "size": 68.55,
                  "sizeInBytes": 68547,
                  "url": "/uploads/medium_2024_06_30_12_45_19_9e691ba632.jpg"
                },
                "large": {
                  "name": "large_2024-06-30 12.45.19.jpg",
                  "hash": "large_2024_06_30_12_45_19_9e691ba632",
                  "ext": ".jpg",
                  "mime": "image/jpeg",
                  "path": null,
                  "width": 750,
                  "height": 1000,
                  "size": 112.06,
                  "sizeInBytes": 112055,
                  "url": "/uploads/large_2024_06_30_12_45_19_9e691ba632.jpg"
                }
              },
              "hash": "2024_06_30_12_45_19_9e691ba632",
              "ext": ".jpg",
              "mime": "image/jpeg",
              "size": 156.6,
              "url": "/uploads/2024_06_30_12_45_19_9e691ba632.jpg",
              "previewUrl": null,
              "provider": "local",
              "provider_metadata": null,
              "createdAt": "2024-07-21T20:27:55.626Z",
              "updatedAt": "2024-07-21T20:27:55.626Z"
            }
          }
        }
      }
    }
  ],
  "meta": {
    "pagination": {
      "page": 1,
      "pageSize": 25,
      "pageCount": 1,
      "total": 1
    }
  }
}

How can you customize Strapi?

Let's consider the following situation. Each specialty has a detailed page on the university website, which has approximately the following URL:

https://project-example.ru/speciality/{serial_id}

serial_id is an identifier that is automatically assigned to each instance of the collection when it is created. Strapi uses it to obtain information about a single instance of the collection. For example, you can obtain detailed information about a specialty using the following command:

GET https://strapi-example.ru/api/speciality/<id>

In order to improve SEO, each detailed page of a specialty needs to have a slug (a readable URL) of approximately the following format:

To do this, Strapi allows you to rewrite the logic of the controller and routes that are responsible for the rules by which the request to the API will be executed. To do this, in the Content-Type Builder mode, we will add a slug field of the UID type to the structure of the “Specialty” collection. Now the slug of each specialty will be unique:

Adding a slug field to the Specialty Collection

Adding a slug field to the Specialty Collection

Then, in Content Manager mode, we assign each instance of the collection its own slug and publish the entire collection:

Filling in the slug field for the Collection

Filling in the slug field for the Collection “Specialty”

Now we need to rewrite the logic of the controller so that we can get the required instance of the collection by its slugTo do this, you will have to perform the following transformations for the “Specialty” collection:

  1. Add custom route /api/specialties/get-by-slug/{slug}.

  2. Add to file services function findOneBySlugso that the SQL query is processed correctly at the Strapi level.

  3. Improve the logic of the controller so that it correctly retrieves from query parameters slug And populate and passed them to the function findOneBySlug.

First, let's add the utility kts-strapi-project/src/utils/extendCoreRouter.jsto extend the logic of the default router:

const extendCoreRouter = (innerRouter, extraRoutes = []) => {
 let routes;
 return {
   get prefix() {
     return innerRouter.prefix;
   },
   get routes() {
     if (!routes) routes = [...extraRoutes, ...innerRouter.routes];
     return routes;
   },
 };
};


module.exports =  { extendCoreRouter };

Then we will finalize the file kts-strapi-project/src/api/speciality/services/speciality.js with a query to the database:

Was
'use strict';


/**
* speciality service
*/


const { createCoreService } = require('@strapi/strapi').factories;


module.exports = createCoreService('api::speciality.speciality');
It became
"use strict";


const { createCoreService } = require("@strapi/strapi").factories;


module.exports = createCoreService(
 "api::speciality.speciality",
 ({ strapi }) => ({
   async findOneBySlug(slug, { populate }) {
       
     return strapi.db.query("api::speciality.speciality").findOne({
       where: {
         slug: slug,
       },
       populate,
     });
   },
 })
);

We will do the same with other files.

kts-strapi-project/src/api/speciality/controllers/speciality.js:

Was
'use strict';


/**
* speciality controller
*/


const { createCoreController } = require('@strapi/strapi').factories;


module.exports = createCoreController('api::speciality.speciality');
It became
"use strict";


const { createCoreController } = require("@strapi/strapi").factories;


module.exports = createCoreController(
 "api::speciality.speciality",
 ({ strapi }) => ({
   async findOneBySlug(ctx) {
     const { slug } = ctx.params;


     const sanitizedQuery = await this.sanitizeQuery(ctx);


     const result = await strapi
       .service("api::speciality.speciality")
       .findOneBySlug(slug, {
         populate: sanitizedQuery.populate,
       });


     const sanitizedResults = await this.sanitizeOutput(result, ctx);


     return this.transformResponse(sanitizedResults);
   },
 })
);

kts-strapi-project/src/api/speciality/routes/speciality.js:

Was
'use strict';


/**
* speciality router
*/


const { createCoreRouter } = require('@strapi/strapi').factories;


module.exports = createCoreRouter('api::speciality.speciality');
It became
"use strict";


const { extendCoreRouter } = require("../../../utils/extendCoreRouter");


const { createCoreRouter } = require("@strapi/strapi").factories;


const defaultRouter = createCoreRouter("api::speciality.speciality");


module.exports = extendCoreRouter(defaultRouter, [
 {
   method: "GET",
   path: "/specialties/get-by-slug/:slug",
   handler: "speciality.findOneBySlug",
   config: {
     auth: false,
   },
 },
]);

So we added a custom endpoint /api/specialties/get-by-slug/{slug}.

The examples above use the function sanitizeQueryit is used to clean the request parameters from errors and unsafe values. You can read more about sanitize Here.

Now let's query some instance from the “Specialty” collection using slug. Let's make the following query:

http://localhost:1338/api/specialties/get-by-slug/prikladnaya-matematika-i-informatika

And we get the following answer:

{
  "data": {
    "id": 1,
    "attributes": {
      "createdAt": "2024-07-17T09:17:03.596Z",
      "updatedAt": "2024-07-23T17:38:09.042Z",
      "publishedAt": "2024-07-17T14:59:42.231Z",
      "name": "Прикладная математика и информатика",
      "code": "01.03.02",
      "duration": 4,
      "slug": "prikladnaya-matematika-i-informatika"
    }
  },
  "meta": {

  }
}

Now we can easily get collection instances not by serial_id, but by slug.

However, it is important to note that Swagger will not be able to pull up the types of the parameters received, despite the change in the receiving logic. Therefore, it will not be possible to make a request using slug via Swagger:

Example of validation error in swagger

Example of validation error in swagger

In this case, it will be possible to make a request, for example, via postman:

Example of getting a specialty using

Example of getting a specialty using “slug” via Postman

Conclusion

In this article, we looked at Strapi in general terms and determined what it is used for. We created several collections and got them using the API and added a custom route to get collection instances using slug. However, the functionality of the system, of course, does not end there.

If you're eager to dive deeper, stay tuned. In the next article, we'll dive deeper into Strapi and talk about:

And to brighten up the wait, I suggest you read other materials on our blog, no less useful for front-end developers:

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *