Всем привет! Меня зовут Александр, я фронтенд-разработчик в KTS. Сегодня я расскажу о Strapi CMS, разберу сценарии ее использования на конкретных примерах и поделюсь способами упрощения работы в ней.

Начну с небольшой предыстории. Наша компания часто разрабатывает проекты, где необходимо регулярно обновлять и настраивать контент сайта. Для таких случаев мы используем различные CMS-системы, позволяющие работать с контентом при помощи графического интерфейса.
При использовании такой CMS-системы хочется, чтобы она отвечала исключительно за контент и не накладывала никаких ограничений на UI, поэтому при выборе мы ориентировались на Headless CMS. Headless CMS – это системы, которые предоставляют функционал администрирования контента (админка) и генерируют API, к которому можно подключить любой клиент (веб-приложение, мобильное приложение и т.д.). Поскольку  Headless CMS – это просто API, они никак не ограничивают клиент и не влияют на реализацию других сервисов бэкенда.
Мы выбрали Strapi, поскольку это одно из самых популярных на сегодняшний день решений. Система является опенсорсной, и, несмотря на то, что у нее есть и платная версия, бесплатного функционала вполне хватает в работе. Мы используем версию Community Edition (62k+ звездочек и почти 7k форков на github). В этой статье я поделюсь нашим опытом работы со Strapi и наглядно опишу, как с его помощью можно решать практические задачи.
Оглавление:
Что умеет Strapi
С помощью Strapi разработчик может назначать коллекции данных, которые будут доступны с помощью API из CMS, а также задавать им конкретную структуру. Strapi автоматически генерирует весь бойлерплейт для совершения CRUD-операций с этими коллекциями данных, а именно:
создает коллекции в БД;
формирует REST API (или GraphQL) для этих коллекций;
позволяет настраивать ограничения на обращение к эндпоинтам;
дает возможность изменять логику на любом уровне с помощью программирования (кастомизировать или добавлять новые эндпоинты, изменять обращения к базе и т.д.).
После того, как разработчики настроят структуру данных, пользователи CMS (например, контент-менеджеры) смогут заполнять коллекции контентом. Затем эти данные можно будет запросить через API.
Последовательность работы со Strapi можно представить в виде следующей схемы:

Как создать проект на Strapi
Чтобы создать новый проект, нужно выполнить следующую команду:
yarn create strapi-app kts-strapi-project --quickstart
У вас получится проект со следующей структурой:

В следующих папках будет расположена ключевая логика для работы со Strapi и коллекциями данных:
src/api– здесь будет храниться структура коллекций в виде json-файлов (иначе говоря – схемы), а также сгенерированные js-файлы с кодом логики этих коллекций;src/components– здесь хранятся схемы компонентов. Это утилитарные сущности, и для них, в отличие от коллекций, не генерируется API. Компонент можно подключить в качестве поля другого компонента или коллекции. Когда вы запросите у API коллекцию, вы получите связанные с ней компоненты;src/extensions– здесь хранятся расширения, добавленные для удобства работы с CMS. Вы можете написать свое расширение или подключить его из магазина расширений Strapi.
Чтобы запустить проект, выполните следующую команду:
yarn develop
Система предложит зарегистрироваться. После регистрации вы сможете авторизоваться в ней под созданными учетными данными:

Режимы работы Strapi
В Strapi есть 2 режима: Content-Type Builder и Content Manager.
Content-Type Builder доступен только в dev-режиме. Dev-режим – это запущенный локально сервер Strapi. На нем разработчики настраивают структуру коллекций, определяют, какими свойствами и атрибутами (иначе говоря, полями) они обладают. После сохранения коллекции происходит кодогенерация json-схем, миграций для БД и бойлерплейта сервера.

Content Manager доступен и в dev-, и в prod-режимах. Prod-режим – это режим, в котором нельзя изменять структуру коллекций, он существует для заполнений коллекций данными.

Как создавать коллекции в Strapi
Для наглядности рассмотрим работу Strapi на примере проекта личного кабинета студента университета. Сначала мы будем создавать коллекции с минимальным набором свойств, а затем дополнять их в процессе знакомства с возможностями Strapi. Первым делом введем некоторые коллекции, которые будут использоваться на нашем сайте.
- 
Коллекция «Студент» со следующими полями:
имя;
фамилия;
дата рождения.
 - 
Коллекция «Специальность» со следующими полями:
название специальности;
код специальности;
длительность обучения (в годах).
 
Виды коллекций данных
В Strapi есть три вида коллекций – Single Type, Collection Types и Component.
Каждый из них подходит для определенной цели.
Single Type подходит, если вы уверены, что данная коллекция хранится в единственном экземпляре. Например, вам наверняка понадобятся такие коллекции single type:
- 
Documents – коллекция документов в приложении со следующими полями:
user_agreement;
privacy_policy;
other;
 Main Page – коллекция для главной страницы приложения;
Header – коллекция для управления пунктами главного меню;
Footer.
Collection Types подойдет, если коллекция представляет собой список данных.
Например, это могут быть:
студенты;
специальности;
кафедры;
адреса институтов на карте.
Components – это вспомогательные сущности. Их нельзя запросить из API отдельным списком, но можно подключить в качестве поля для Single Type, Collection Type или в другой компонент. Примерами Components могут быть:
ссылка, которая состоит из текста для ссылки и ее url-адреса;
карточка, которая состоит из заголовка, изображения и описания;
координата, которая состоит из широты и долготы.
Типы данных
Создадим две коллекции вида Collections Types: «Студент» и «Специальность».
При создании нужно указать Display Name (отображаемое название коллекции в Strapi), а также API ID в единственном и множественном числах – они будут использоваться в дальнейшем для совершения CRUD-операций с коллекциями.

При создании коллекции необходимо присвоить тип каждому свойству:
Text – строка;
Rich text (blocks) – текст с форматированием (можно сделать полужирным, курсивным и т.д.);
Number – число;
Date – дата в формате date, datetime или time;
Media – изображение или видео в json-формате, хранит ссылку на файл в хранилище S3;
Relation – тип данных «связь». Нужен для того, чтобы задать связи между коллекциями (но не компонентами). Например, на каждой специальности учится много студентов, поэтому у студента будет поле Relation с ссылкой на Специальность с типом связи «один-ко-многим»;
Boolean – логический тип данных;
JSON – данные в формате JSON;
Email – соответствует строке, но валидируется на формат адреса электронной почты в рамках Strapi;
Password – нельзя запросить из API, но можно использовать, если кастомизировать запросы. Мы пока не нашли применение для этого типа данных, поскольку хранить пароли в БД – не лучшая идея;
Enumeration – выбор из ограниченного списка текстовых значений;
UID – на клиент приходит как строка, но на стороне Strapi происходит проверка на уникальность;
Component – переиспользуемый компонент из коллекции Components;
Dynamic Zone: предположим, у нас есть сущность «Статья», которая состоит из заголовка и контента. Контент, в свою очередь, является массивом произвольных компонентов из определенного набора (например, картинка, текст, видео и опрос). Dynamic Zone позволяет формировать такие динамические списки компонентов.

Пример создания коллекции
Создадим коллекцию «Студент» со структурой, которую мы спроектировали выше – добавим поля «имя», «фамилия» и «дата рождения»:

Далее по аналогии создадим коллекцию «Специальность»:

Далее необходимо добавить связь для коллекций «Студент» и «Специальность», чтобы из API можно было запросить всех студентов с конкретной специальности или узнать специальность, на которой обучается отдельный студент. Для этого для коллекции «Студент» добавим поле «speciality» с типом Relation. На каждой специальности учится много студентов, поэтому связь будет «один-ко-многим».
Сначала добавим поле «speciality» типа Relation:

Для коллекции «Студент» добавим поле «photo» с типом Media. Стоит отметить, что при добавлении поля типа Media можно настроить ограничения на форматы данных. По умолчанию там любые файлы. Можно ограничить: только изображения, только видео или только pdf:

Предположим, что у каждой специальности есть ссылка на сайт с её детальным описанием. Давайте добавим поле «link» в коллекцию специальность. Ссылка — это комбинация заголовка и адреса. Как и обсуждали ранее, ссылку нужно сделать именно компонентом, потому что она не существует сама по себе, и её не нужно получать из API. В коллекцию «Специальность» нам также необходимо добавить поле «speciality_link». Для этого мы заранее создадим соответствующий компонент «Ссылка», который будет состоять из названия и заголовка:


Далее переиспользуем созданный компонент в коллекции «Специальность»:

Кодогенерация
После сохранения каждой из коллекций происходит автоматическая кодогенерация схем. По сути, код генерируется согласно принципам архитектуры Model-Routes-Controllers-Service, только вместо Model директория называется content-types. Подробнее об этой архитектуре можно почитать здесь.
Также генерируется логика получения коллекций, и все это хранится в следующих папках:
content-types: здесь хранится схема коллекции, которая приходит с API;
controllers: здесь хранится логика, с которой обрабатывается HTTP-запрос при обращении к API: происходит валидация и парсинг параметров запроса, настраивается структура ответа с сервера. Controllers используют внутри себя services, чтобы обращаться к БД. Controllers, в отличие от services, не могут быть переиспользованы;
routes: здесь хранится список эндпоинтов данной коллекции, которые доступны для обращения;
services: здесь настраивается логика обращения к БД. Также именно в services должна быть написана какая-то специфичная бизнес-логика для приложения: например, отправка письма на почту пользователю (это легко сделать с помощью плагинов). Services используются внутри controllers, а также могут переиспользовать друг друга.

В результате получаются следующие файлы:
- 
файлы speciality:
 - 
файлы student:
 
Стоит отметить, что после создания коллекции ее Singular ID и Plural ID нельзя поменять из интерфейса – их можно изменить только путем редактирования кода в файлах выше, но такое изменение часто приводит к ошибкам в миграциях. Если вам все же нужно поменять идентификаторы для коллекции, то самый надежный способ – удалить ее и создать заново.

Как работать с API
Когда код сгенерирован, можно переходить к работе с API и наполнению коллекций контентом. В этом разделе мы продолжим рассматривать функционал Strapi на примере с проектом личного кабинета студента.
Плагин для автогенерации swagger
Strapi дает возможность устанавливать плагины, которые упрощают настройку и тестирование коллекций. Один из таких плагинов – strapi documentation – позволяет получить автогенерируемый swagger. Чтобы установить его, нужно выполнить следующую команду:
yarn strapi install documentation  
После установки документация будет доступна по адресу:
http://localhost:{PORT}/documentation/v1.0.0.

Добавление данных в коллекцию
Через режим Content Manager создадим несколько экземпляров коллекции «Студент» и заполним их данными. Здесь важно оговориться, что по умолчанию экземпляр коллекции создается в состоянии Draft – это значит, что он не будет доступен с помощью API. Для того, чтобы открыть доступ к созданному экземпляру, нужно перейти в режим его редактирования и нажать Publish.

Настройка доступов
По умолчанию Strapi делает все коллекции приватными – получить их можно только с помощью отправки сгенерированного токена в заголовке. Чтобы сделать коллекцию публичной, нужно открыть раздел «Роли» в настройках. Он будет находиться по адресу:
http://localhost:1338/admin/settings/users-permissions/roles.
В этом разделе можно выбрать, к каким коллекциям будет доступ у авторизованных и неавторизованных пользователей.

Публичные запросы
Укажем, какие типы операций неавторизованный пользователь сможет осуществлять с коллекцией Student. Нам достаточно чтения коллекции и отдельного экземпляра, поэтому отметим флажки find и findOne.

Приватные запросы
Если же мы хотим, чтобы какие-то запросы с API были доступны только авторизованным пользователям, следует создать API Token, который придется отправлять его при каждом запросе. Это можно сделать по адресу:
http://localhost:1338/admin/settings/api-tokens/create.  

Значение сгенерированного токена нужно пробрасывать при каждом запросе в заголовке Authorization, в swagger это делается через нажатие на кнопку Authorize):  

Параметры запроса
Рассмотрим query-параметры для получения коллекции Student. Они делятся на две категории.
Первая категория – те параметры, которые связаны исключительно с пагинацией.

Вторая категория – параметры, которые помогают настраивать, какие из полей вы хотите запросить у экземпляров коллекций и их порядок.

С параметрами этой категории я предлагаю познакомиться поближе:
sort – значение, по которому нужно отсортировать коллекцию. Принимает на вход название поля и направление сортировки (asc/desc). Есть возможность множественной сортировки;
fields – список примитивных полей, которые нужно получить с API. По умолчанию отдаются все примитивные поля, к которым относятся text, enumeration, rich_text, email, password, date, number, boolean и JSON. Fields позволяет описать только те поля, которые нужно получить;
populate: Relation, Media, Dynamic Zone и Component не являются примитивными полями, поскольку для их запроса необходимо выполнить дополнительные запросы в БД или использовать join. Поэтому Strapi по умолчанию вместо связи отправляет id этой связи. Чтобы запросить поля вложенных сущностей, эти поля нужно перечислить в populate;
filters – правила для фильтрации коллекции. Может принимать значения
$gte,$equalи другие.
Подробнее о параметрах можно почитать в документации Strapi.
Пример запроса
Сделаем запрос на список студентов:
http://localhost:1338/api/students
И получим следующий ответ:
Скрытый текст
{
  "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
    }
  }
}
Обратите внимание: экземпляр нашей коллекции обернут в структуру вида { id, attributes }. Strapi делает так с каждым экземпляром коллекции, а также со вложенными сущностями, о которых мы говорили выше.
Также можно заметить, что в ответе на запрос мы не получили поля speciality и image. Произошло это потому, что они являются вложенными сущностями. Как я уже отметил ранее, по умолчанию Strapi не добавляет их в ответ.
Чтобы получать из API информацию о специальности студента и его фото, необходимо добавить параметр populate и указать в нем, какие именно свойства вы хотите получить. Здесь можно почитать о том, как корректно описывать populate.
Добавим в запрос информацию о полях, которые нам нужны, и получим запрос следующего вида:
http://localhost:1338/api/students?populate=speciality,photo
С параметром populate ответ изменился: теперь в него включены поля speciality и photo.
Скрытый текст
{
  "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
    }
  }
}
Как можно кастомизировать Strapi
Рассмотрим следующую ситуацию. У каждой специальности есть детальная страница на сайте университета, имеющая примерно следующий url:
https://project-example.ru/speciality/{serial_id}
serial_id – это идентификатор, который автоматически присваивается каждому экземпляру коллекции при его создании. С его помощью Strapi позволяет получать информацию об отдельном экземпляре коллекции. К примеру, получить детальную информацию о специальности можно с помощью следующей команды:
GET https://strapi-example.ru/api/speciality/<id>  
При этом для улучшения SEO нужно, чтобы каждая детальная страница специальности имела slug (читабельный url) приблизительно следующего формата:
https://project-example.ru/speciality/lingvistikahttps://project-example.ru/speciality/prikladnaya_matematika
Для этого Strapi позволяет переписать логику контроллера и роутов, которые отвечают за то, по каким правилам будет выполняться запрос к API. Чтобы сделать это, в режиме Content-Type Builder добавим в структуру коллекции «Специальность» поле slug типа UID. Теперь slug каждой специальности будет уникален:

Затем в режиме Content Manager присвоим каждому экземпляру коллекции свой slug и опубликуем всю коллекцию:

Теперь нужно переписать логику контроллера, чтобы можно было получать нужный экземпляр коллекции по его slug. Для этого придется выполнить следующие преобразования для коллекции «Специальность»:
Добавить кастомный роут
/api/specialties/get-by-slug/{slug}.Добавить в файл
servicesфункциюfindOneBySlug, чтобы на уровне Strapi корректно обрабатывался sql-запрос.Доработать логику контроллера, чтобы он корректно доставал из query-параметров
slugиpopulateи пробрасывал их в функциюfindOneBySlug.
Сначала добавим утилиту kts-strapi-project/src/utils/extendCoreRouter.js, чтобы расширять логику дефолтного роутера:
  
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 };
Затем доработаем файл kts-strapi-project/src/api/speciality/services/speciality.js с запросом к БД:  
Было
'use strict';
/**
* speciality service
*/
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::speciality.speciality');
Стало
"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,
     });
   },
 })
);
Аналогичным образом поступим и с другими файлами.
kts-strapi-project/src/api/speciality/controllers/speciality.js:  
Было
'use strict';
/**
* speciality controller
*/
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::speciality.speciality');
Стало
"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:
Было
'use strict';
/**
* speciality router
*/
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::speciality.speciality');
Стало
"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,
   },
 },
]);
Таким образом, мы добавили кастомный эндпоинт /api/specialties/get-by-slug/{slug}.  
В примерах выше используется функция sanitizeQuery, она используется чтобы очистить параметры запроса от ошибок и небезопасных значений. Подробнее об sanitize можно почитать здесь.
Теперь запросим с помощью slug какой-нибудь экземпляр из коллекции «Специальность». Сделаем следующий запрос:
http://localhost:1338/api/specialties/get-by-slug/prikladnaya-matematika-i-informatika  
И получим следующий ответ:
{
  "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": {
  }
}
Теперь мы можем легко получить экземпляры коллекции не по serial_id, а по slug.
Однако важно отметить, что Swagger не сможет подтянуть типы принимаемых параметров, несмотря на изменение логики получения. Следовательно, сделать запрос с помощью slug через Swagger будет невозможно:

При этом сделать запрос, к примеру, через postman, будет возможно:

Заключение
В рамках этой статьи мы рассмотрели Strapi в общих чертах и определили, для каких целей он нужен. Создали несколько коллекций и получили их с помощью API и добавили кастомный роут для получения экземпляров коллекции с помощью slug. Однако этим функционал системы, разумеется, не заканчивается.
Если вам не терпится углубиться в подробности, следите за нашими публикациями. В следующей статье мы изучим Strapi более детально и поговорим о том:
как обрабатывать сложные типы данных (rich text и dynamic zone);
как сортировать коллекции по популярности;
как мы типизируем и валидируем коллекции на клиенте с использованием Typescript и zod;
как подключать postgres, s3, minio;
какие данные стоит хранить в Strapi, а какие – в админке;
что ожидается в новой версии Strapi 5.
А чтобы скрасить ожидание, предлагаю вам почитать другие материалы в нашем блоге, не менее полезные для фронтенд-разработчиков:
Комментарии (8)

Swordsman
31.08.2024 10:04Куда катится Хабр. 20-30 строк кода на пыхе + правильно настроенный апач порвет всякие гламурные штуки. Работа ради работы. Ну бред же.

kava13 Автор
31.08.2024 10:04+3CMS – это все-таки более широкий функционал, чем 20-30 строк кода на пыхе
Проект на Strapi может успешно функционировать вообще без написания кода разработчиками — весь код генерируется автоматически на основе коллекций, создаваемых через UIStrapi же предоставляет возможность гибко кастомизировать функционал по мере необходимости

jeserg
31.08.2024 10:04А в чем гламурность этого инструмента, просто интересно на чем строятся такие оценки?

LexterWayne
31.08.2024 10:04Вот благодаря таким программистам (20-30 строк кода) у меня есть работа)) Потом сидишь переписываешь проекты с нуля потому что кто-то решил свой велосипед на PHP соорудить вместо использования фреймворка хотя бы)

PavelKuptsov
31.08.2024 10:04"Media – изображение или видео в json-формате, хранит ссылку на файл в хранилище S3;"
Вот это напрягает. Зачем нам еще тут S3?
Чтобы однажды сайт превратился в тыкву?
          
 
xaver
Не рассматривали directus в качестве аналога?
kava13 Автор
Мы решили использовать Strapi из-за его популярности и более развитого комьюнити
Но обязательно обратим внимание на Directus, спасибо за предложенную альтернативу!