Блог “У Василича”

Как работают блоги на Gatsby

Приблизительное время чтения20 мин.
Фото от Chris Leggat

Демистификация принципов работы современных блогов. На примере этого сайта

Всегда считал создание личного блога одной из основных целей в карьере фронтенд-разработчика. Типа, посадить дерево, построить блог… Конечно кругом полно готовых генераторов, но нет, имеется в виду прям с нуля. Это нечто светлое, сияющее на горизонте, заставляющее тебя расти профессионально, чтобы когда-нибудь ты получил все необходимые для этого знания. И в моём понимании это всегда был широкий профиль знаний, уходящий в фулстак, который всё время оставался всё так же недостижим. Но… так получилось, что технологии дошли до меня быстрее, чем я до них. И говорят: “Псст, Кирюшенский, а ты в курсе, что в 2020 фронтенд-разработчику достаточно оставаться самим собой, чтобы запустить свой блог? JAMStack, может слышал?“. Вот так я в итоге и запустил этот сайт.

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

В целом процесс можно разбить на 3 основных этапа:

  1. Работа с контентом. Определяемся с форматом и подходом к работе с контентом
  2. Работа с конфигом сборки. Преобразуем контент в готовую для добавления на страницы структуру данных
  3. Работа с приложением. Добавляем контент на сайт

Пройдёмся по ним по порядку.

Сразу оговорюсь, что в статье речь пойдёт про стек: React + Gatsby + Netlify. Но все умозаключения экстраполируются и на другие фреймворки, генераторы статических сайтов, CDN и CMS, пока решение следует парадигмам JAMStack.

Выбираем формат и подход к работе с контентом

Первым делом нужно выбрать формат, в котором будут писаться статьи для блога. Понятно, что веб работает на html, так что в итоге мы к нему и придём, но сразу в html никто не пишет — это всё же неудобно. Абсолютно все рассмотренные мною блоги писались в md или mdx формате. Markdown действительно хорош для этого и покрывает все требования к текстовому контенту. MDX — это расширенная версия, отличающаяся возможностью использовать в разметке самописные компоненты, импортируя их из кодбазы. Оба варианта полностью валидны и зависят от того нужен ли вам этот расширенный функционал. В моём случае md было достаточно.

Большую часть markdown файла занимает контент в привычном формате, но помимо него наверху прописан набор полей — так называемый frontmatter, который мы потом можем распарсить, получив переменные на уровне приложения:

---
title: Как работают блоги на Gatsby
slug: how-gatsby-blogs-work
publishDate: 2020-09-30T17:29:07.133Z
thumbnail: /assets/how-gatsby-blogs-work.jpg
unsplashLink: https://unsplash.com/@chris_legs
unsplashAuthor: Chris Leggat
description: Демистификация принципов работы современных блогов. На примере этого сайта.
tags:
  - Gatsby
  - Netlify CMS
  - markdown
  - remark
---

Всегда считал создание личного блога одной из основных целей в карьере фронтенд-разработчика. Типа, посадить дерево, построить блог...

Параллельно с выбором формата идёт выбор подхода к работе с контентом. Обычно вы хотите хранить md файлы внутри репозитория. Аналогично с используемыми внутри постов ассетами — лучше их держать в репе. Речь здесь в основном про изображения, но и файлы вроде прикреплённых pdf сюда же относятся. В простейшем случае чтобы добавить новый пост, вы открываете репозиторий, создаёте в контентной директории новый md файл и размещаете рядом все используемые ассеты.

Основной вопрос здесь — нужно ли вам что-то большее? Если вы разработчик и это ваш личный блог, то, в принципе, этого достаточно: репозиторий развёрнут локально, загрузить в него файлы — обычная рутина. Если же публикациями занимаются несколько человек и/или это контент-менеджеры, не залезающие в код, то нужно что-то более презентабельное — визуальный редактор, разделение прав, система драфтов. Здесь на сцене появляется headless CMS.

Для этого блога я решил использовать Netlify CMS. Это сейчас самое популярное решение, при этом максимально простое. Зачем это мне? Спасибо, что спросили. Прежде всего технический интерес — разобраться как на сайт интегрируется CMS, ну и в целом вся эта абстракция ощущается более профессионально что ли…

Стоит отметить, что выбор формата и подхода к работе с контентом тесно связаны отчасти потому, что CMS накладывает свои требования к контенту — так например, Netlify CMS работает с md и не поддерживает mdx. Если же CMS не используется, то ничего нас не сдерживает от использования mdx, и я в ином случае на нём бы и остановился.

Разбираемся как работает CMS

Разберём на примере Netlify как работают headless CMS. Конечная цель при добавлении CMS не меняется — мы должны получить md файлы и ассеты в репозитории, просто их генерацию мы делегируем системе.

Netlify CMS — это просто React приложение с админкой, которую мы можем отдавать как отдельную страницу нашего сайта. Если у нас есть права на просмотр этой страницы, то внутри нас встречает незамысловатый интерфейс, через который мы можем создавать новый контент через визуальный редактор. Headless CMS отвечает исключительно за контент и не лезет в наше приложение, в чём и заключается основное отличие от CMS традиционных.

Грузим плагин

First things first, хотелось бы увидеть админку. Для этого Netlify предоставляет npm-модуль netlify-cms-app, но в случае с Gatsby, разумеется, всё делается через плагины, а именно gatsby-plugin-netlify-cms. Этот плагин подключит npm-модуль за нас и сгенерирует страницу admin/index.html, после чего мы сможем открыть в браузере административную панель по адресу /admin/.

Главная страница админки Netlify CMS
Административная панель Netlify CMS в развёрнутом локально приложении

Относительно авторизации в моём случае это просто вход через Github: у вас есть push access к репозиторию — добро пожаловать в админку. Для больших команд предусмотрены более гибкие решения.

Прописываем конфигурацию

Какой CMS использует бэкенд и метод авторизации, где сохраняет файлы, какие типы контента позволяет создавать — всё это настраивается через файл конфигурации. Это просто yml файл, хранящийся у вас в репозитории.

Полный набор полей всегда можно найти в документации, остановлюсь только на самом интересном — коллекциях. Каждая коллекция — это тип контента, который можно создавать через админку. У неё есть название, место хранения и набор полей, которые будут отражены в интерфейсе через предусмотренные виджеты. Обязательным полем является body — это как раз-таки markdown контент, остальные поля полностью опциональны и будут записаны во frontmatter сгенерированного документа.

Для постов этого блога коллекция выглядит как-то так:

collections:
  - name: 'blog'
    label: 'Blog'
    folder: 'content/blog'
    create: true
    slug: '{{year}}-{{month}}-{{day}}-{{fields.slug}}'
    preview_path: 'blog/{{fields.slug}}'
    editor:
      preview: false
    fields:
      - { label: 'Title', name: 'title', widget: 'string' }
      - { label: 'Slug', name: 'slug', widget: 'string' }
      - { label: 'Publish Date', name: 'publishDate', widget: 'datetime', dateFormat: 'DD.MM.YYYY', timeFormat: false }
      - { label: 'Featured Image', name: 'thumbnail', widget: 'image' }
      - { label: 'Unsplash Link', name: 'unsplashLink', widget: 'string' }
      - { label: 'Unsplash Author', name: 'unsplashAuthor', widget: 'string' }
      - { label: 'Description', name: 'description', widget: 'text' }
      - { label: 'Tags', name: 'tags', widget: 'list' }
      - { label: 'Body', name: 'body', widget: 'markdown' }

Что в результате отражается в интерфейсе CMS:

Страница создания нового поста Netlify CMS
Настроенный для создания постов визуальный редактор Netlify CMS

По умолчанию Netlify CMS выводит превью результата справа от редактора, но CMS ничего не знает о вашем сайте, так что просто выводит текст на белом экране. Превью можно настроить через всё тот же Gatsby плагин, добавив шаблон со всеми стилями, но я счёл этот процесс воссоздания реплики сайта чересчур геморным и отключил превью вовсе — я всегда могу посмотреть результат на тестовых площадках перед публикацией.

Пробуем CMS в деле

После того как плагин подключен и конфиг прописан, можем опробовать CMS в деле. Открываем админку, создаём новый пост, заполняем все обязательные поля, вбиваем контент, сохраняем результат.

В простейшем случае после сохранения будет сделан коммит в master ветку репозитория, в котором будет добавлен получившийся md файл и связанные ассеты. У меня активирован editorial workflow, который вместо прямого коммита в master создаёт новую ветку и заводит pull request. Это состояние драфта — в нём я могу дорабатывать статью: сохранения изменений будут добавлять коммиты в ветку, а публикация после перевода драфта в статус “Ready” наконец будет мержить реквест в master. Мне нравится этот подход из-за возможности создавать драфты + я могу посмотреть превью внутри пул реквеста, т.к. ветка публикуется на отдельной площадке Netlify. Так или иначе коммит с новыми файлами в итоге в мастере.

Pull request с новым md файлом и изображениями
CMS добавила контентные файлы в репозиторий

Превращаем контент в структуру данных

Теперь когда в репозитории лежит контент, нашей новой целью будет превратить markdown в html и сделать доступными его и поля из frontmatter через статичные GraphQL запросы, через которые Gatsby получает данные на уровне приложения.

В основе трансформаций контента стоит remark — он распарсит markdown, превратив его в валидный в html. Помимо базового функционала у него есть система плагинов, через которую мы можем проводить более сложные трансформации, например настроить подсветку синтаксиса в блоках кода.

Т.к. все контентные файлы доступны на диске, то для получения доступа к ним через запросы воспользуемся плагином gatsby-source-filesystem. После этого вся магия по обработке md файлов ложится на gatsby-transformer-remark — это трансформер плагин, использующий внутри remark. Как и все трансформер плагины, он преобразовывает поля, доступные в запросах, то есть вместо базовой информации по файлу он распарсит его контент, сделав доступными html и поля frontmatter.

В базовой комплектации gatsby-transformer-remark добавляет поле html, пришедшее на место контента markdown, и объект frontmatter со всеми полями. Дополнительно он добавляет ништяки вроде timeToRead — приблизительного времени чтения, рассчитанного из размера контента, excerpt — контентной вырезки, которую можно использовать в качестве текста превью, tableOfContents — содержания, построенного исходя из структуры заголовков. Далее функционал можно расширять через широкий выбор плагинов.

Интерфейс GraphiQL с запросом на получение данных по файлу поста
Через GraphiQL можем увидеть, что содержимое контентных файлов стало доступно для запросов

Накатываем плагины remark

Плагины remark задаются через соответствующие Gatsby плагины, которые в свою очередь являются плагинами для gatsby-transformer-remark. Все они так или иначе дорабатывают html, и здесь уже можно разгуляться с трансформациями. В принципе мы можем пилить и свои собственные, например если хотим отрендерить вместо списка свой компонент, но обычно этого никто не делает, и набор фич определяется набором готовых плагинов.

Плагинов под gatsby-transformer-remark довольно много. Их можно найти в документации по поиску “gatsby-remark”. На момент написания этого предложения поиск выдаёт аж 252 записи. Приведу здесь самые полезные на мой взгляд, которые уже применил на практике.

Обработка ассетов

Первое, что хочется добавить — это оптимизированные контентные изображения. По умолчанию изображения переводятся в img с alt и title атрибутами, мы же хотим получить полный джентльменский набор оптимизатора изображений: выполнить сжатие, сгенерировать вариации под различные размеры экрана, добавить webp, вставить picture вместо img, подключить ленивую загрузку и добавить tracing svg плейсхолдер. Ранее в контексте контентных изображений я о таком мог только мечтать, но так как у нас статичные билды, а изображения хранятся прямо в репозитории, то это становится возможно.

Здесь нужно разделять изображения внутри контента и за его пределами. Вторые передаются через frontmatter, например красивое и бессмысленное изображение для превью с Unsplash, и remark их не обрабатывает — для них мы используем привычную пользователям Gatsby систему обработки: делаем их доступными через gatsby-source-filesystem, трансформируем запросы через gatsby-transformer-sharp, обрабатываем через gatsby-plugin-sharp и добавляем через компонент Img из gatsby-image (если впервые слышите обо всём этом и вдруг стало страшно, то вот красивая демка с собачками). За контентные же полностью отвечает remark, а если точнее, то плагин gatsby-remark-images — у него есть свои приколы, вроде того, что мы можем дополнительно при наличии title в разметке оборачивать picture в figure, размещая title в figcaption, или оборачивать изображения в ссылки на полноразмерные версии, но в целом он очень похож на gatsby-image.

gatsby-remark-images работает только для jpeg/png. Для контентных изображений этого вполне достаточно, но ассеты могут быть и другие вроде svg или pdf — такие файлы нужно скопировать в конечную директорию билда, чтобы они стали доступны на сайте. Для этого воспользуемся другим remark плагином — gatsby-remark-copy-linked-files: он проверяет markdown на наличие изображений или ссылок до файлов всех форматов, отличных от jpeg/png и переносит их в public.

В случае с Netlify CMS также есть тонкий момент с расположением ассетов. Чтобы gatsby-remark-images работал, ему нужен относительный путь до ассета. Netlify же генерирует абсолютные пути, что делает невозможным для плагина найти на диске ассеты. Чтобы этих ребят подружить, используем плагин gatsby-remark-relative-images, который преобразует пути из абсолютных в относительные.

Подсветка кода

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

Для подключения Prism используем плагин gatsby-remark-prismjs. Сам Prism так же расширяется через плагины, но так как мы не используем его напрямую, то возможность их использовать ограничена встроенными в плагин Gatsby возможностями. А именно: у нас есть возможность добавить подсветку строк, проставить нумерацию и сделать командную строку более аутентичной. Остальные фичи вроде вывода языка приходится доделывать самому.

Для своего блога я настроил подсветку строк, командную строку, взял тему a11y Dark из официального пакета prism-themes и добавил язык через псевдоэлемент, благодаря крутой фиче по получению контента из html атрибута по типу content: '""attr(data-language)""'.

Настройка ссылок

Ссылки разделяются на внутренние и внешние. Внутренние должны обрабатываться через роутер без перезагрузки страницы. Обычно в приложении мы используем для этого компонент Link из роутера — в случае с Gatsby это компонент Link, являющийся обёрткой вокруг компонента из reach роутера. Но для генерируемого контента вставка через компонент роутера не происходит, так что мы должны использовать pushState из History API, чтобы получить привычное поведение. Именно это и делает gatsby-plugin-catch-links.

Для внешних ссылок хочется добавить открытие в новой вкладке через target="_blank", и rel="nofollow noopener noreferrer", чтобы сделать ссылки безопасными, а Google довольным. Эти атрибуты как раз и являются дефолтными в gatsby-remark-external-links.

Вставка фреймов

В контенте могут использоваться фреймы, к примеру видосы с YouTube. Плагин gatsby-remark-responsive-iframe добавляет обёртки вокруг фреймов, делая их адаптивными с сохранением соотношения сторон, исходя из указанных ширины и высоты.

Обработка заголовков

Всегда казалась крутой фича в блогах с якорями на заголовках. Чтобы её добавить, нужно проставлять якори в h1-h6 через id и добавлять рядом с заголовками иконки, обёрнутые в ссылки на эти самые заголовки. Конечно же, у Gatsby уже есть плагин для этого: gatsby-remark-autolink-headers. Здесь можно загрузить свою svg, изменить её расположение и донастроить css по вкусу.

Улучшение пунктуации

Последний штрих — сделаем пунктуацию более “умной”: такие вещи, как вставка тире вместо двойных минусов и вставка типографических кавычек вместо тех, что привычны нам в коде. Это делает gatsby-remark-smartypants, использующий retext-smartypants изнутри.

Добавляем контент на сайт

Теперь когда мы уверены в том, что GraphQL запросы вернут нам всё что надо, можем приступить к работе над компонентами. Разберём типовой кейс: мы хотим вывести список постов на разводящей странице и сгенерировать свою страницу для каждого поста.

Выводим список постов

В случае со списком у нас есть статичная страница — пропишем для неё page query, в котором получим список статей:

export const pageQuery = graphql`
  query articlesList {
    allMarkdownRemark(sort: { fields: frontmatter___publishDate, order: DESC }) {
      edges {
        node {
          timeToRead
          id
          fields {
            slug
          }
          frontmatter {
            title
            publishDate(formatString: "DD MMMM YYYY", locale: "ru")
            publishDateStrict: publishDate(formatString: "YYYY-MM-DD")
            description
            thumbnail {
              childImageSharp {
                fluid(maxWidth: 1440, traceSVG: { background: "#fff", color: "#2b2b2b", threshold: 24 }) {
                  ...GatsbyImageSharpFluid_withWebp_tracedSVG
                }
              }
            }
          }
        }
      }
    }
  }
`

Этот запрос на этапе сборки сгенерирует json файл со списком постов, который станет доступен на уровне компонента страницы через проп data. Из интересного:

  1. Сортировку постов и форматирование дат можно провести прямо через запрос
  2. Изображение для превью использует фрагмент из gatsby-transformer-sharp
  3. fields — это программно добавленные нами поля. О них подробнее чуть далее

Ну и далее уже идёт типичная работа фронтенд-разработчика по превращению массива данных в React компоненты.

Генерируем страницы для постов

Со страницами для постов ситуация интереснее, т.к. это динамические страницы и их нельзя создать привычным образом, положив в директорию pages — каждая страница имеет единый шаблон, но разные имена. Для создания таких страниц в Gatsby используется Node API — мы вручную используем тот же API, что Gatsby использует у себя для автоматического создания роутов из pages. Для нашего кейса процесс можно описать так:

  1. Создать компонент страницы, который будет использоваться в качестве шаблона
  2. Создать slug для каждой страницы — уникальное имя, по которому её можно опознать
  3. Выполнить GraphQL запрос за списком постов и программно создать для них страницы, используя slug для построения адреса и шаблон в качестве компонента

Создаём шаблон для страницы поста

Шаблон для динамической страницы — обычный компонент страницы, только расположенный не в pages, а в templates директории. Этот компонент работает аналогично разводящей странице, делая запрос через page query, только используя запрос не к списку а получая конкретный пост по id:

export const pageQuery = graphql`
  query articleById($id: String!) {
    markdownRemark(id: { eq: $id }) {
      html
      timeToRead
      fields {
        slug
      }
      frontmatter {
        title
        description
        publishDate(formatString: "DD MMMM YYYY", locale: "ru")
        publishDateStrict: publishDate(formatString: "YYYY-MM-DD")
        tags
        thumbnail {
          childImageSharp {
            fluid(quality: 100, traceSVG: { background: "#fff", color: "#2b2b2b", threshold: 24 }) {
              ...GatsbyImageSharpFluid_withWebp_tracedSVG
            }
          }
        }
        unsplashLink
        unsplashAuthor
      }
    }
  }
`

Переменную id мы сделаем доступной через Node API позже, в остальном же всё аналогично предыдущему запросу, только добавился html и изменился набор полей. После этого опять получаем результат через data проп, из frontmatter полей рендерим компоненты вокруг контента, добавляем стили для html тегов по вкусу, а html вставляем через dangerouslySetInnerHTML.

Генерируем slug

slug — в широком смысле это уникальный идентификатор, по которому можно опознать пост. В контексте CMS slug — это имя файла на диске, в контексте Gatsby — URL страницы. К его генерации есть множество подходов. Для англоязычных сайтов обычно CMS генерирует slug автоматически из заголовка статьи, после чего Gatsby создаёт поле slug из имени файла, то есть получается максимальная автоматизация. В случае с русскоязычным контентом такой подход приведёт к кириллице в URL и именах файлов, что имхо не очень здорово, так что я решил сделать максимально в лоб, добавив поле slug в CMS, которое заполняется руками. Схема такая:

  1. Заполняю поле в CMS — оно заносится во frontmatter
  2. Для имён файлов добавляю даты для удобства
  3. Для урлов использую поле из frontmatter с префиксом /blog/

В принципе, т.к. slug без даты уже лежит внутри frontmatter, то можно пользоваться им напрямую, но тогда придётся префикс дописывать по всей кодбазе, поэтому вместо frontmatter.slug использую fields.slug, которое добавляется через onCreateNode в gatsby-node.js:

exports.onCreateNode = ({ node, actions }) => {
  if (node.internal.type === 'MarkdownRemark') {
    actions.createNodeField({
      name: 'slug',
      node,
      value: `/blog/${node.frontmatter.slug}/`,
    })
  }
}

Создаём страницы под посты

Теперь когда у нас есть и шаблон и slug, то мы можем воспользоваться createPages во всё том же gatsby-node.js, тем самым вклинившись в процесс генерации страниц. Здесь логика такая:

  1. Делаем запрос ко списку постов, получая автоматически сгенерированные id и добавленные самолично slug
  2. Для каждого элемента массива создаём новую страницу через createPage, используя построенный шаблон в качестве компонента, сгенерированный slug в качестве адреса и передавая id в контекст — это как раз и делает его доступным на уровне запросов, позволяя получать данные именно по запрашиваемой странице
exports.createPages = async ({ actions, graphql, reporter }) => {
  const result = await graphql(`
    {
      allMarkdownRemark {
        edges {
          node {
            id
            fields {
              slug
            }
          }
        }
      }
    }
  `)

  if (result.errors) {
    reporter.panicOnBuild('Error while running GraphQL query.')
    return
  }

  result.data.allMarkdownRemark.edges.forEach(({ node }) => {
    actions.createPage({
      path: node.fields.slug,
      component: require.resolve('./src/templates/BlogPost.tsx'),
      context: { id: node.id },
    })
  })
}

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


Вот как-то так подобные сайты и работают. Как видите, ни один бэкендер в процессе не пострадал — личный сайт сейчас себе может позволить практически любой фронтенд-разработчик.

При подобном сетапе автор теперь может зайти в админку на площадке через /admin/ и написать новый пост. CMS добавит новый коммит в репу с md файлом и ассетами, который в свою очередь увидит Netlify и запустит автодеплой. Пойдёт пересборка, в ходе которой md превратится в html, все данные перенесутся в json файлы, сбилдятся оптимизированные версии изображений и сгенерируются html файлы для поддержки SEO. По окончанию пересборки новый контент станет доступен на сайте — в списке статей новый элемент, а по ссылке новый пост.

Хоть мы и генерируем страницы динамически, сам по себе сайт остаётся полностью статичным, что гарантирует его высокую скорость работы — это очень мощный паттерн, который как раз идеально работает на сайтах вроде блогов.

p1t1ch.com