В этом материале мы посмотрим как использовать фреймворк React Next.js со Strapi для создания веб-сайта портфолио.

Что будем делать?

Не имеет значения, является ли создаваемый вами сайт личным, сайтом-портфолио или сайтом компании. Применяются те же инструменты.

CMS, которую мы собираемся использовать — это Strapi, которая является бесплатной, с отличным обслуживанием клиентов и с открытым исходным кодом.

Strapi CMS — это так называемая автономная CMS или безголовая система управления, т.е. headless. По сути, это означает, что это серверная служба вашего сайта.

Другие CMS, такие как WordPress, включают общедоступную часть сайта в то же приложение, что и серверную часть. Это часто приводит к тому, что веб-сайт становится медленным и сложным в управлении. Но как разработчик JS, который недолюбливает PHP, я могу быть здесь немного предвзятым.

WordPress теперь поддерживает использование себя в качестве автономной безголовой CMS. Однако я не буду пробовать это из-за… ну, знаете… PHP.

В любом случае, давайте начнем создание сайта с Next.js. Репозиторий проекта:

https://github.com/Devalo/create-portfolio-with-next-strapi

Планирование

Так что именно мы будем создавать? Как упоминалось ранее, простое онлайн-портфолио. Мы должны иметь возможность входить в систему и записывать, редактировать и удалять записи. Посетители будут просматривать все записи и нажимать на конкретную запись, чтобы просмотреть всю историю.

Фронтенд будет написан в Next.js версии 10. Он будет запрашивать данные у Strapi API. Мы будем использовать Bootstrap 5 для нашего макета. Bootstrap 5 только что вышел из бета-версии, так что мы должны быть готовы к работе с ним!

После некоторых зарисовок на бумаге, макет будет выглядеть примерно так:

Ничего особенного, довольно простая компоновка. Ну что ж давайте наконец-то создадим новый проект Next.js.

Настраиваем Фронтенд

Для создания нового проекта Next.js необходимо сначала установить Node.js. Если он еще не установлен, посетите сайт https://nodejs.org/ и установите Node.js перед тем, как продолжить.

Создаем новый проект Next.js

Next.js поставляется с очень хорошим генератором проекта. Мы создадим наш новый проект, набрав:

$ npx create-next-app my-portfolio

Это создаст для нас новый проект. Мы переходим внутрь него (cd) и запускаем:

$ cd my-portfolio
my-portfolio $ npm run dev

Если вы посетите http://localhost:3000/, вы должны увидеть стартовую страницу Next.js:

Установка Bootstrap 5

Как упоминалось ранее, мы собираемся использовать Bootstrap 5 для наших проектов. Мы можем начать с установки пакета Bootstrap 5 NPM:

$ npm install bootstrap@next

Внутри нашего файла /pages/_app.js мы импортируем только что установленную библиотеку:

// /pages/_app.js
import '../styles/globals.css'
import 'bootstrap/dist/css/bootstrap.min.css'; // Added
function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}
export default MyApp

Чтобы убедиться, что это работает, мы можем заменить все внутри файла index.js простым контейнером bootstrap и текстом:

// /pages/index.js
export default function Home() {
  return (
    <div className="container">
      Is this working?
    </div>
  );
};

Если вы сейчас посмотрите на http://localhost: 3000/, то вы увидите, что наша строка текста смещена к середине. Если мы удалим контейнер из нашего класса div, текст будет прижат слева нашей страницы. Это означает, что наш Bootstrap запущен и работает.

Axios

Мы будем общаться со Strapi, отправляя и получая JSON. Мы будем использовать «Axios» для выполнения тяжелой работы. Давайте установим его:

$ npm install axios

Вот и все, что нужно для создания нашего фронтэнда. Давайте начнем создавать интерфейс фронтенда.

Создание визуала

Мы начнем с создания статической верхней панели навигации. Мы не собираемся делать ничего особенного, а воспользуемся встроенными в Bootstrap компонентами. Мы создадим отдельный файл макета, который будет содержать наши основные элементы макета.

Мы создадим новую папку в корневом каталоге под названием components. В каталоге компонентов мы создадим новый файл, который назовем Layout.jsx:

// /components/Layout.jsx

const Layout = ({ children }) => (
  <div>
    {children}
  </div>
)
export default Layout;

Все содержимое нашей страницы будет передано в этот компонент в качестве аргумента. Другими словами, мы обернем содержимое нашей страницы внутри этого компонента Layout. Давайте импортируем этот компонент в наш индексный файл и поместим в него наш контент:

// /pages/index.js

import Layout from '../components/Layout';

export default function Home() {
  return (
    <Layout>
      <div className="container">
        Is this working?
      </div>
    </Layout>
  );
};

Если вы перейдете на http://localhost:3000/, все должно выглядеть одинаково.

Создаем панель навигации

Внутри нашего каталога компонентов мы создадим новый. Мы назовем его shared. Внутри этого каталога мы создадим новый файл, который мы назовем Navbar.jsx. Мы создадим нашу навигацию (Navbar) здесь.

// components/shared/Navbar.jsx
const Navbar = () => (
  <nav className="navbar navbar-expand-lg navbar-dark bg-primary fixed-top">
    <div className="container">
      <a className="navbar-brand" href="#">My Portfolio</a>
      <button
        className="navbar-toggler"
        type="button"
        data-bs-toggle="collapse"
        data-bs-target="#navbarText"
        aria-controls="navbarText"
        aria-expanded="false"
        aria-label="Toggle navigation"
      >
        <span className="navbar-toggler-icon" />
      </button>
      <div className="collapse navbar-collapse" id="navbarText">
        <ul className="navbar-nav me-auto mb-2 mb-lg-0">
          <li className="nav-item">
            <a className="nav-link active" aria-current="page" href="#">Portfolio</a>
          </li>
          <li className="nav-item">
            <a className="nav-link" href="#">About me</a>
          </li>
        </ul>
      </div>
    </div>
  </nav>
);
export default Navbar;

И мы импортируем и добавляем его в наш файл макета (Layout):

// components/Layout.jsx
import Navbar from './shared/Navbar';
const Layout = ({ children }) => (
  <div>
    <Navbar />
    <div className="main-container container">
      {children}
    </div>
  </div>
)
export default Layout;

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

Создание интерфейса (UI)

Давайте теперь обсудим, как мы собираемся разместить все элементы портфолио. Я подумываю разделить все на строки и столбцы, где в каждой строке по 2 элемента. Мы можем изменить наш index.js на:

// pages/index.js
import Layout from '../components/Layout';
export default function Home() {
  const entries = ['Project 1', 'Project 2', 'Project 3', 'Project 4'];
  return (
    <Layout>
      <div className="entries">
        <div className="row justify-content-start  ">
          {entries.map((entry) => (
            <div className="col-md-6">
              <div className="entry mb-3">
                <div className="main-image">
                  <img src="https://via.placeholder.com/600x400" />
                  <h1>{entry}</h1>
                  <p>
                    About
                    {' '}
                    {entry}
                  </p>
                </div>
              </div>
            </div>
          ))}
        </div>
      </div>
    </Layout>
  );
}

Давай посмотрим что здесь происходит. Мы смоделируем 4 записи портфолио с помощью массива. Чуть позже мы заменим массив фактическим содержимым.

Мы добавим в это немного CSS. В папке public/ или в папке styles/ вы найдете файл css с именем globals.css, содержимое которого мы удалим и заменим на:

// publi/globals.css
.main-container {
  padding-top: 70px;
}
.entries .main-image img {
  width: 100%;
  display: block;
}

У нас должно получиться что-то вроде этого:

Сейчас мы просто используем изображения-заполнители. Позже мы заменим их подходящими изображениями, а также воспользуемся компонентом Image в Next.js.

Интерфейс (UI) для одного элемента портфолио

Теперь, когда у нас есть главная страница, на которой мы можем отображать все элементы нашего портфолио, нам следует подумать о создании страницы, на которой будет отображаться один элемент портфолио.

Здесь вступает в игру система маршрутизации Next.js. Видите ли, каждый файл в каталоге pages — это маршрут. Поэтому, если вы создадите файл и компонент testpage.jsx и перейдете по адресу http://localhost:3000/testfile, вы должны увидеть содержимое компонента.

Это безумно круто, и это одна из вещей, которые делают Next.js таким хорошим. Это также работает с динамическим содержимым. Если вы заключите имя файла в квадратные скобки, NextJS будет рассматривать его как динамический файл.

Мы создадим новую папку в папке pages, которую мы называем portfolio. Внутри этой папки мы создадим файл, который будем называть [slug].jsx . Этот файл будет содержать отдельные элементы нашего портфолио. Чтобы проверить его динамические возможности, мы можем написать компонент для быстрого тестирования:

// pages/portfolio/[slug].jsx

const PortfolioItem = () => {
  return (
    <div>
      This is a test component
    </div>
  );
};
export default PortfolioItem;

Независимо от того, какой URL вы используете, Next.js будет отображать один и тот же компонент.

http://localhost:3000/portfolio/(45231
http://localhost:3000/portfolio/test
http://localhost:3000/portfolio/fsdfsdfsdfs

Компонент просто терпеливо сидит и ждет, пока его заполнят динамическими данными, которые будут показаны нашим замечательным посетителям.

// pages/portfolio/[slug].jsx

import Layout from '../../components/Layout';
const PortfolioItem = () => {

  return (
    <Layout>
      <div className="row">
        <div className="portfolio-image text-center mb-4">
          <div className="col-md-12">
            <img src="https://via.placeholder.com/1000x500" />
          </div>
        </div>
      </div>
      <div className="row">
        <div className="portfolio-content">
          <div className="col-md-12">
            <div className="portfolio-headline text-center m-2">
              <h1>Project</h1>
            </div>
            <p>
              Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium
              doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis
              et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem
              quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores
              eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem i
              psum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius
              modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut
              enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit
              laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure
              reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur,
              vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
            </p>
          </div>
        </div>
      </div>
    </Layout>
  );
};
export default PortfolioItem;

По большей части это заполнитель для наших реальных данных. Отдельная страница портфолио должна теперь выглядеть примерно так:

Никогда не говорил, что это будет выглядеть красиво.

На нашей главной странице отображаются все объекты портфолио. У нас также есть страница с одним элементом портфолио. Мы должны развернуть и запустить наш бэкэнд Strapi прежде чем мы вернемся к нашему фронтэнду приложения. Как только мы запустим бэкэнд Strapi, мы сможем наполнить его контентом, который будет использован во фронтенде.

Создание движка

Установка

Как упоминалось ранее, Strapi — это полноценная CMS без головы с включенными возможностями движка. Если вы никогда раньше не работали со Strapi, он просто отлично вам подойдет.

Strapi имеет собственный генератор проектов. Мы выйдем (cd) из нашего внешнего приложения и создадим серверную часть бок о бок:

my-portfolio $ cd ..
$ yarn create strapi-app portfolio-backend --quickstart

После установки сервер запустится автоматически. Вы попадете прямо на новую вкладку в вашем браузере:

Это будет вашей главной информацией для входа в систему. Заполните ее должным образом, чтобы не столкнуться с какими-либо проблемами во время разработки.

Потрясающе! После того, как вы создадите свой аккаунт, вы должны увидеть главную панель инструментов Strapi:

По сути, это графический интерфейс для вашей базы данных. Это означает, что мы можем создавать таблицы с содержимым, как в обычной базе данных.

Коллекции

В левом меню, под Plugins, вы увидите Content-Type Builder. Здесь вы сможете создавать различные типы данных.

Тип Single — это как раз то, на что он похож. Это единый тип данных. Это может быть фрагмент статического текста или что-то подобное.

Тип Коллекция (Collection) — это тип, у которого много элементов. В нашем случае тип Коллекция будет содержать много элементов портфолио. Это также было бы правильным выбором, если бы вы создали блог, газету или что-нибудь еще с несколькими записями, которые вы бы повторяли во фронтенде.

Давайте настроим тип коллекции — Портфолио.

Нажмите кнопку «Создать новый тип коллекции» (Create a new collection type) и введите имя коллекции.

Когда закончите, нажмите «Продолжить» (Continue). Затем нам нужно выяснить, какие поля данных мы хотим использовать. Я думаю об этих вещах:

  • Headline
  • Content
  • Image
  • slug id

Для заголовка мы создадим новое текстовое поле Text field и назовем его «Headline» (заголовок).

Для нашего контента мы создадим текстовое поле Rich text:

Для нашего основного изображения мы создадим поле Media:

Для нашего слага (url) мы создадим поле id и установим для него Headline (Заголовок):

Это должно дать нам 4 поля, и мы можем создать их в итоге нажав «Сохранить» (Save):

Сервер перезапустится и внесет все наши изменения. После этого вы заметите, что наш тип портфолио указан в списке типов коллекций в левом меню:

Вы видели, что оно было написано во множественном числе автоматически? Это потому, что оно может содержать несколько портфолио. Этого бы не случилось с типом Single.

Разрешения

Разрешения или Permissions. Следующим шагом будет добавление разрешений. По умолчанию Strapi полностью закрыт, поэтому вам нужно вручную выбрать, какие типы запросов должны быть общедоступными. В левом меню нажмите «Настройки» (Settings), затем нажмите «Роли» Roles в разделе «Плагин пользователей и разрешений» (Users & Permissions Plugin).

Здесь мы можем установить разрешения для аутентифицированных и общедоступных API. Поскольку мы собираемся создавать элементы нашего портфолио непосредственно внутри нашей CMS, нам не нужно беспокоиться об аутентификации. Выберите здесь Public:

Мы хотим, чтобы общедоступный API был открыт только для просмотра всех или отдельных элементов портфолио:

Добавляя find и findone мы делаем роуты /portfolios/:id и /portfolios доступными. Нажмите save и переходите к нашему новому типу коллекции Portfolios (Портфолио).

Добавление элемента портфолио

У нас еще нет элементов портфолио. Давайте создадим один! Кликните на Add New Portfolio в правом верхнем углу. Нам также понадобится изображение. Я взял случайное фото, которое я буду использовать:

https://unsplash.com/photos/tZc3vjPCk-Q

Мы можем добавить новое изображение, щелкнув в правом верхнем углу поля изображения:

Загрузить ресурсы:

Мы можем выбирать между загрузкой с локального компьютера или по URL-адресу. Я добавляю URL изображения:

И выберите загрузить и завершить (upload):

Это должно добавить наше изображение прямо в наше портфолио. Заполним и остальные поля.

Обратите внимание, что слаг был заполнен автоматически. Если по какой-то причине вам это не подходит, заполните его вручную дефисами между каждым словом. Он должен напоминать ваш заголовок.

Щелкните «Сохранить» (Save). Элемент портфолио создан. Нам также нужно нажать «Опубликовать» (Publish), чтобы он стал доступен через API.

Если вы перейдете по адресу http://localhost:1337/portfolios, вы увидите нашу недавно созданную запись портфолио в формате JSON.

В следующем разделе мы вернемся к нашему интерфейсу.

Не забывайте, что ваш бэкэнд должен работать, пока вы работаете с фронтендом.

Фронтенд: чтение и отображение данных

В этой части статьи вы увидите, как великолепно работают вместе Strapi и NextJS. Вот где они демонстрируют свою силу!

Получение данных

Сначала мы напишем функцию, которая будет получать данные из нашей CMS. В корневом каталоге создайте новую папку lib и в ней файл service.js. Этот файл будет отвечать за выборку данных из CMS. Я использую для этого axios. Если вам больше нравится библиотека fetch, продолжайте и используйте ее.

// lib/service.js
import axios from 'axios';
const fetchFromCMS = async (path) => {
  const url = `http://localhost:1337/${path}`;
  const res = await axios.get(url);
  return res.data;
};
export default fetchFromCMS;

Таким образом, мы в основном просто выполняем запросы GET к нашей CMS, извлекая элементы. Мы должны иметь возможность получать все из нашего файла index.js. Посмотрим, как это сделать.

Отображение данных

Что делает NextJS таким хорошим, так это его способность отображать материалы на стороне сервера. Таким образом мы будем отображать элементы нашего портфолио.

getStaticProps() — предпочтительный способ получения статических данных из CMS. В нашем файле index.js мы импортируем нашу новую функцию выборки и реализуем getStaticProps():

// pages/index.js
// .....
import fetchFromCMS from '../lib/service';
export default function Home({ portfolioItems }) {
  console.log(portfolioItems);
  // ....
}
export async function getStaticProps() {
  const portfolioItems = await fetchFromCMS('portfolios');
  return {
    props: { portfolioItems },
    revalidate: 1,
  };
}

Мы получаем данные из нашей функции fetchFromCMS() и передаем их в качестве аргумента в главную функцию. Это происходит автоматически. Свойство revalidate в основном сообщает Next.js, как часто он должен регенерировать эту страницу и данные. Если вы установите его в 30, то он будет перестраиваться каждые 30 секунд. Это означает, что новые записи не будут видны до тех пор, пока приложение не перестроится. Это довольно круто. Это означает, что мы подаем клиенту чистый HTML, js и CSS. Без извлечения базы данных. Это сделает сайт быстрым.

Кроме того, функция getStaticProps() работает на стороне сервера, в то время как главная функция работает на стороне клиента. Симпатично.

При обновлении страницы содержимое нашей базы данных попадает в консоль браузера:

Безумно круто! Давайте визуализируем это в нашем компоненте React:

// pages/index.js
// ...
export default function Home({ portfolioItems }) {
  return (
    <Layout>
      <div className="entries">
        <div className="row justify-content-start ">
          {portfolioItems.map((portfolio) => (
            <div className="col-md-6">
              <div className="entry mb-3">
                <div className="main-image">
                  <Image
                    src={portfolio.image.name}
                    width={600}
                    height={400}
                    alt={portfolio.Headline}
                  />
                  <h1>{portfolio.Headline}</h1>
                </div>
              </div>
            </div>
          ))}
        </div>
      </div>
    </Layout>
  );
}
// ....

Мы перебираем элементы портфолио, как мы делали это с элементами-заполнителями. Обратите внимание, что мы больше не используем тег <img />.

NextJS предлагает лучший вариант! Их недавно выпущенный компонент React сгенерирует файл в формате .webp из вашего изображения, что приведет к его более быстрой загрузке. Он также поставляется с включенной отложенной загрузкой, и будет адаптивным! Если я правильно понял, обязательные поля width и height принимают значения max-width и max-height.

Однако компонент изображения немного строгий. Вы не можете неявно добавлять изображения извне приложения без внесения URL-адреса в белый список. Итак, что нам нужно сделать дальше — это создать файл конфигурации в нашем корневом каталоге next.config.js и поместить в него следующее:

// config.next.js

module.exports = {
  images: {
    domains: ['images.unsplash.com'],
  },
};

Next.js будет жаловаться и даст вам точный URL, который вы должны ввести. В этом случае, я занесу в белый список URL Unsplash. Потрясающе. Вам нужно перезапустить приложение.

Мы получили все данные, и разместили все как надо. Вы удивлены? Я пошел дальше и создал еще несколько элементов портфолио:

Просмотр отдельных элементов портфолио

Готовы к еще одному волшебству? Мы должны сделать наши элементы портфолио кликабельными. Когда вы нажимаете на элемент, вы попадаете на страницу этого элемента. На нашей странице index.js мы используем встроенный в Next.js компонент Link. Он работает так же, как и react-router. Мы завернем наши элементы в компонент:

// pages/index.js
import Link from 'next/link';
// ....
export default function Home({ portfolioItems }) {
  return (
    <Layout>
      <div className="entries">
        <div className="row justify-content-start  ">
          {portfolioItems.map((portfolio) => (
            <div className="col-md-6">
              <div className="entry mb-3">
                <Link as={`/portfolio/${portfolio.slug}`} href="/portfolio/[id]"> // this gets added 
                  <div className="main-image">
                    <Image
                      src={portfolio.image.name}
                      width={600}
                      height={400}
                      alt={portfolio.Headline}
                    />
                    <h1>{portfolio.Headline}</h1>
                  </div>
                </Link> // this gets added
              </div>
            </div>
          ))}
        </div>
      </div>
    </Layout>
  );
}
// ....

Если вы попытаетесь нажать на какой-либо из элементов, вы попадете на страницу элемента, которую мы создали ранее. Давайте посмотрим, как мы можем заполнить её правильными данными.

В нашем [slug] .js мы собираемся использовать две специальные серверные функции. getStaticProps(), с которой мы уже знакомы, и getStaticPaths), которая будет строить динамические маршруты во время сборки.

// pages/portfolio/[slug].jsx
// ....
export async function getStaticProps({ params }) {
  const portfolio = await fetchFromCMS(`portfolios?slug=${params.slug}`);
return {
    props: { portfolio: portfolio[0] },
    revalidate: 1,
  };
}

getStaticProps() очень похожа на ту, которая находится в индексном файле, за исключением того, что мы выбираем один элемент по имени ярлыка портфолио, который мы создали в Strapi. Это массив, поэтому мы передадим данные с индексом 0 в клиентскую функцию.

// pages/portfolio/[slug].jsx
// ....
export async function getStaticPaths() {
  const portfolios = await fetchFromCMS('portfolios');
return {
    paths: portfolios.map((portfolio) => ({
      params: {
        slug: portfolio.slug,
      },
    })),
    fallback: false,
  };
}
// ....

Это будет использовано во время сборки. Она извлекает все портфолио и создает URL из имени slug. Мы сможем получать статические страницы вместо того, чтобы каждый раз запрашивать данные у базы данных. Обратите внимание, что мы использовали revalidate в функции getStaticProps(). Скорее всего, вы захотите установить его на что-то большее, чем 1 в производстве, в противном случае это вроде как сбивает всю цель.

Теперь мы можем использовать переданный аргумент в нашей клиентской функции. Весь [slug].jsx теперь выглядит так:

// pages/portfolio/[slug].js
import Image from 'next/image';
import Layout from '../../components/Layout';
import fetchFromCMS from '../../lib/service';
const PortfolioItem = ({ portfolio }) => {
  return (
    <Layout>
      <div className="row">
        <div className="portfolio-image text-center mb-4">
          <div className="col-md-12">
            <Image
              src={portfolio.image.name}
              width={1000}
              height={500}
            />
          </div>
        </div>
      </div>
      <div className="row">
        <div className="portfolio-content">
          <div className="col-md-12">
            <div className="portfolio-headline text-center m-2">
              <h1>{portfolio.Headline}</h1>
            </div>
            <div dangerouslySetInnerHTML={{ __html: portfolio.content }}/>
          </div>
        </div>
      </div>
    </Layout>
  );
};
export async function getStaticPaths() {
  const portfolios = await fetchFromCMS('portfolios');
return {
    paths: portfolios.map((portfolio) => ({
      params: {
        slug: portfolio.slug,
      },
    })),
    fallback: false,
  };
}
export async function getStaticProps({ params }) {
  const portfolio = await fetchFromCMS(`portfolios?slug=${params.slug}`);
return {
    props: { portfolio: portfolio[0] },
    revalidate: 1,
  };
}
export default PortfolioItem;

И если вы попробуете:

Данные отображаются правильно!

Данные представлены в формате Markdown. Чтобы их правильно отформатировать, вам нужно использовать какой-то парсер Markdown. Что вы можете сделать, так это добавить новый файл в вашу папку lib с функцией, которая обрабатывает текстовое содержимое, прежде чем вы передадите его клиентской функции. Например:

// lib/processMarkdown.js
import remark from 'remark';
import html from 'remark-html';

// process markdown to html and return
const processMarkdown = async (markdown) => {
  const process = await remark()
    .use(html, {sanitize: true})
    .process(markdown.content);

  return process.toString();
};

export default processMarkdown;

Заключительные мысли

Это было весело. Я надеюсь, что это было так же весело, как и писать это руководство. Next.js и Strapi — потрясающая комбинация и моя технология для создания быстрых и надежных динамических веб-сайтов. А что лучше всего?

Все отображается на стороне сервера! Поисковым роботам легко узнать ваш сайт. Посетителям легко найти вашу информацию!
Использовать Strapi, вероятно, проще, чем писать новую панель инструментов для добавления статей, элементов или всего, что вам нужно добавить в базу данных.

И что самое лучшее? Всё легко настраивается. Плюс ко всему — это открытый исходный код. Вы можете сделать так, чтобы он выглядел именно так, как вы хотите. Он также пользуется большой поддержкой сообщества и самих Strapi.

То же самое можно сказать и о Next.js. Это невероятно удобная штука для создания высокоэффективных веб-сайтов, которые выводят динамический контент (который ведет себя как статический). И его очень легко развернуть. Я рекомендую разместить ваше приложение NextJS на Vercel. Поддержка со стороны ребят из NextJS тоже отличная.

Мы использовали Bootstrap 5, но лишь поверхностно. Если хотите, я всегда могу написать об этом в блоге. Однако самое замечательное в Bootstrap 5 — это то, что он больше не поставляется с jQuery, что, на мой взгляд, является очень хорошим решением команды Bootstrap.

Репозиторий проекта:

https://github.com/Devalo/create-portfolio-with-next-strapi

Автор — Стефан Баккелунд Валуа.

Статьи # # # #