Великий Gatsby.js

На выходных захотелось поэкспериментировать с блогом. Возможности vuepress исследованы. Душа просит новизны. Под руку попался gatsby. Ну, что ж, попробуем сделать мультиязычный сайт и поиграть с graphql, который поставляется как вариант по-умолчанию для передачи данных.

# Установка

Любимый для подобных вещей npx спешит на помощь. Выбираем минимальный starter-шаблон, чтобы всё сделать по-своему с нуля.

$ npx gatsby new gatsby-blog https://github.com/gatsbyjs/gatsby-starter-hello-world
$ cd gatsby-blog
$ npm run develop
1
2
3

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

.
├── content
│   ├── en
│   │   └── frontend
│   └── ru
│       ├── frontend
│       └── backend
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
├── gatsby-ssr.js
├── src
│   ├── assets
│   │   ├── images
│   │   │   └── great.jpg
│   │   └── styles
│   │       └── base.css
│   ├── components
│   │   ├── layout.js
│   │   └── theme-switcher.js
│   ├── pages
│   │   ├── 404.js
│   │   └── index.js
│   ├── templates
│   │   └── post-template.js
│   └── translations
│       ├── en.json
│       ├── ru.json
│       └── index.js
└── static
    ├── favicon.ico
    ├── sitemap.xml
    └── robots.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

Из названий, в общем-то, всё понятно. Единственное, что стоило бы уточнить: данные будут представлять собой markdown-файлы. Это наша база данных. Типичная для генератора статики, но не являющаяся единственным вариантом в случае с gatsby.

# Graphql

После получения копии gatsby, можно сразу же запустить его в режиме разработки и поиграть с graphql. Чтобы получить что-то полезное, сначала надо это полезное написать. Хотя бы задать глобальные данные для сайта. Сделать это можно в gatsby-config.js.

// gatsby-config.js
module.exports = {
  siteMetadata: {
    title: "Great gatsby multi-language blog",
  },
}
1
2
3
4
5
6

Открыть браузер по адресу http://localhost:8000/___graphql и написать запрос:

{
  site {
    siteMetadata {
      title
    }
  }
}
1
2
3
4
5
6
7

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

Надеюсь, вы получили в ответ свой заголовок. Пора отправляться дальше.

# Контент

Главная вещь, ради которой мы тут собрались: данные. Этот раздел самый большой, поэтому приготовьте чаю.

# Данные

Перво-наперво пишем заметочку. Лучше несколько.

По slug определим путь к заметке. Category для тех, кто хочет разбивать страницы по категориям. Конечно, дата, в формате год-месяц-день. Остальное должно быть понятно.

<!--content/ru/frontend/gatsby/gatsby.mdx-->
---
h1: Заголовок поста
title: Title для браузера
description: Описание для поисковых роботов
date: 2020-04-06
category: frontend
slug: gatsby
---

Мой контент
![alt](./tree.jpg)
1
2
3
4
5
6
7
8
9
10
11
12

Структура директории в формате локаль -> категория -> запись:

.
├── content
│   ├── en
│   │   └── frontend
│   └── ru
│       ├── frontend
│       │   ├── gatsby
│       │   │   ├── gatsby.jpg
│       │   │   └── gatsby.mdx
1
2
3
4
5
6
7
8
9

# Инструменты

Установка пакетов для работы с форматом mdx (в отличие от обычного markdown эта штука позволит использовать компоненты прямо в md-файлах. Ну не круто ли?).

$ npm i gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react
1

Пакет «расшаривания» директории, где лежит контент, чтобы его содержимое было видно в graphql.

$ npm i gatsby-source-filesystem
1

И пакеты, позволяющие запросто подключать картинки и хранить их не где-то в static, а рядом с файлом-заметкой.

$ npm i gatsby-plugin-sharp gatsby-remark-images
1

Скормим их в файлу конфигурации:

// gatsby-config.js
module.exports = {
  // ...
  plugins: [
    "gatsby-plugin-sharp",
    {
      resolve: "gatsby-source-filesystem",
      options: {
        name: "content",
        // шарить директорию content
        path: `${__dirname}/content/`,
      },
    },
    {
      resolve: `gatsby-plugin-mdx`,
      options: {
        extensions: [`.mdx`, `.md`],
        gatsbyRemarkPlugins: [
          {
            resolve: `gatsby-remark-images`,
            options: {
              maxWidth: 1200,
            },
          },
        ],
      },
    },
  ],
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

Данные подготовлены, пакеты установлены. Дальше следует выяснить как работает механизм передачи данных. Для каждого markdown-файла создаётся страница (gatsby-node.js). Страница должна иметь шаблон (post-template.js). В шаблоне можно вывести что угодно.

Поехали.

# Создание страниц

// gatsby-node.js
const path = require("path")

// createPages даёт доступ к graphql и некоторым методам gatsby (actions)
exports.createPages = async ({ actions, graphql }) => {
  const { createPage } = actions

  // получить все markdown-записи
  const {
    data: {
      allMdx: { edges: posts },
    },
  } = await graphql(`
    {
      allMdx {
        edges {
          node {
            frontmatter {
              slug
              category
            }
          }
        }
      }
    }
  `)

  // для каждой записи создать страницу
  posts.forEach(({ node }) => {
    const { slug, category } = node.frontmatter
    return createPage({
      // путь к странице
      path: `${category}/${slug}`,
      // шаблон страницы
      component: require.resolve("./src/templates/post-template.js"),
      // контекст, который попадёт в шаблон
      // может быть использован для дальнейших манипуляций с данными
      context: { slug },
    })
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# Шаблон

В шаблоне запрашиваем любые данные.

// src/templates/post-template.js

import React from "react"
import { graphql } from "gatsby"
import { MDXRenderer } from "gatsby-plugin-mdx"

// динамический slug берётся из переданного на этапе создания страниц контекста
export const query = graphql`
  query getPost($slug: String!) {
    mdx(frontmatter: { slug: { eq: $slug } }) {
      body
      frontmatter {
        h1
        slug
      }
    }
  }
`
// Отобразить контент помогает MDXRenderer
export default function PostTemplate({ data }) {
  const { h1 } = data.mdx.frontmatter
  const { body } = data.mdx

  return (
    <main>
      <h1>{h1}</h1>
      <MDXRenderer>{body}</MDXRenderer>
    </main>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

Всё! Новую запись можно увидеть по адресу /frontend/gatsby/. Это был самый трудоёмкий этап. Дальше можно вывести все записи на главной и перейти к пунктам более простым и не менее полезным.

Записи на главной:

// pages/index.js
import React from "react"
import { graphql, useStaticQuery, Link } from "gatsby"

// получить все записи, отсортированные по дате
const getPosts = graphql`
  query allPosts {
    allMdx(sort: { fields: frontmatter___date, order: DESC }) {
      edges {
        node {
          frontmatter {
            h1
            slug
            category
            date(formatString: "MMMM Do, YYYY")
          }
        }
      }
    }
  }
`

export default function HomePage() {
  const response = useStaticQuery(getPosts)
  const posts = response.allMdx.edges

  return (
    <main>
      <h1>Posts</h1>
      <ul>
        {posts.map(({ node: { frontmatter: post } }, index) => (
          <li key={index}>
            <span>{post.date}</span><br/>
            <Link to={`${post.category}/${post.slug}/`}>{post.h1}</Link>
          </li>
        ))}
      </ul>
    </main>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

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

# Мультиязычность и SEO

На мой взгляд хорошим подспорьем для переводов в обычных javascript-файлах, таких как страницы (pages), может быть пакет react-intl. Однако, есть одна проблема: для gatsby его метод injectIntl работать не будет: не предусмотрен для использования в этом окружении. Но не беда, есть обёртка над react-intl, её и установим.

$ npm i gatsby-plugin-intl
1

Файл конфигурации принимает следующий вид:

// gatsby-config.js
module.exports = {
  // ...
  plugins: [
    // ...
    {
      resolve: "gatsby-plugin-intl",
      options: {
        path: `${__dirname}/src/translations`,
        languages: ["en", "ru"],
        defaultLanguage: "ru",
        // автоматически перенаправлять на `/ru` или `/en` когда человек на главной `/`
        // имейте ввиду: у Google Chrome всегда стоит `en-US`! экспериментируйте
        redirect: false,
      },
    },
  ],
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

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

src
└── translations
    ├── en.json
    ├── ru.json
    └── index.js
1
2
3
4
5

Переводы не должны иметь вложенность. Только flat. Это ограничение react-intl. Оно, конечно, обходится, но не станем заострять на этом внимание сейчас.

// src/translations/en.json
{
  "home.h1": "Hello, welcome {user}",
  "home.title": "Home title",
  "home.description": "Home description"
}
1
2
3
4
5
6

Предпочитаю брать их все из одного места. Так удобнее.

// src/translations/index.js
import locale_en from "./en.json"
import locale_ru from "./ru.json"

export default {
  en: locale_en,
  ru: locale_ru,
}
1
2
3
4
5
6
7
8

В markdown-файлы добавить информацию о языке, это очень важные данные!

<!--content/ru/frontend/gatsby/gatsby.mdx-->
---
h1: Заголовок поста
lang: ru

<!--content/en/frontend/gatsby/gatsby.mdx-->
---
h1: Post heading
lang: en
1
2
3
4
5
6
7
8
9

Поскольку контентные страницы создаём самостоятельно, передавать контекст тоже надо самостоятельно.

// gatsby-node.js
exports.createPages = async ({ actions, graphql }) => {
  // запрос тот же, только lang добавить
  // ...
  frontmatter {
    lang
    slug
    category
  }
  // ...

  // создать отдельные страницы для разных языков
  posts.forEach(({ node }) => {
    const { lang, slug, category } = node.frontmatter
    return createPage({
      path: `${lang}/${category}/${slug}`,
      component: require.resolve("./src/templates/post-template.js"),
      context: { lang, slug },
    })
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

И lang в шаблон, конечно, прокинуть. Надо же теперь распределять весь контент по языковой принадлежности! Заодно правильно отформатируем даты для текущего языка.

// src/templates/post-template.js
export const query = graphql`
  query getPost($slug: String!, $lang: String!) {
    mdx(frontmatter: { slug: { eq: $slug }, lang: { eq: $lang } }) {
      body
      frontmatter {
        h1
        date(formatString: "MMMM Do, YYYY", locale: $lang)
      }
    }
  }
`
1
2
3
4
5
6
7
8
9
10
11
12

Страницы в page после установки плагина обзавелись новой переменной контекста — language. Теперь для каждой из них можно получить язык, установленный в браузере посетителя. Фильтровать по этому признаку записи или добавлять seo-заголовки. Попробуем.

// src/pages/index.js
// ... добавить в запрос lang
frontmatter {
  lang
  h1
  slug
  category
}
// ...

export default function HomePage({ pageContext: { language } }) {
  const response = useStaticQuery(getPosts)
  // только записи с актуальной локалью
  const posts = response.allMdx.edges.filter(
    post => post.node.frontmatter.lang === language
  )

  return (
    <main>
      <ul>
        {posts.map(({ node: { frontmatter: post } }, index) => (
          <li key={index}>
            <Link to={`/${post.lang}/${post.category}/${post.slug}/`}>
              {post.h1}
            </Link>
          </li>
        ))}
      </ul>
    </main>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

Осталось разобраться с локализацией самих js-страниц. На сцену снова выходит react-intl.

Конечно, захочется правильно устанавливать html-атрибут lang и писать в head всякие красивые мета-данные для поисковых роботов. В этом поможет layout.js, куда будем складывать всё это добро. Ставим react-helmet:

$ npm i react-helmet
1

Чтобы не страдать с импортом относительных путей, докрутим webpack.

// gatsby-node.js
exports.onCreateWebpackConfig = ({ actions }) => {
  actions.setWebpackConfig({
    resolve: {
      modules: [path.resolve(__dirname, "src"), "node_modules"],
    },
  })
}
1
2
3
4
5
6
7
8

Дописываем в gatsby-config.js глобальные title и description. Или не дописываем, а передаём с конкретной страницы. Тогда и graphql-запрос не нужен. В общем, идём делать SEO.

// components/layout.js
import React from "react"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"

export default function Layout({
  lang = "ru",
  title = "",
  description = "",
  children,
}) {
  const { site } = useStaticQuery(
    graphql`
      query {
        site {
          siteMetadata {
            title
            description
          }
        }
      }
    `
  )
  const metaDescription = description || site.siteMetadata.description

  return (
    <>
      <Helmet
        htmlAttributes={{ lang }}
        title={title}
        titleTemplate={`%s | ${site.siteMetadata.title}`}
        meta={[
          // по желанию добавить Open Graph для социальных сетей
          { name: "description", content: metaDescription },
        ]}
      />
      <main>{children}</main>
    </>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

Допустим, pages/index.js пуст (если нет, создаём любой другой, к примеру about.js).

// pages/index.js
import React from "react"
import {
  useIntl,
  FormattedDate,
  FormattedMessage,
  FormattedNumber,
} from "gatsby-plugin-intl"
import Layout from "components/layout"

// получить language из контекста и вывести:
// правильно отформатированные дату, число и валюту, да просто переменную в тексте
// красота!
export default function HomePage({ pageContext: { language } }) {
  const intl = useIntl()

  return (
    <Layout
      lang={language}
      title={intl.formatMessage({ id: "home.title" })}
      description={intl.formatMessage({ id: "home.description" })}
    >
      <p>
        <FormattedDate value={new Date()} /><br />
        <FormattedNumber value={12000} style="currency" currency="USD" /><br />
        <FormattedMessage id="home.h1" values={{ user: "Jack" }} />
      </p>
    </Layout>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

На этом вопросы мультиязычности и SEO считаю закрытыми.

# gatsby-ssr

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

// gatsby-ssr.js
export const onPreRenderHTML = ({ getHeadComponents }) => {
  // для разработки inline css удобнее
  if (process.env.NODE_ENV !== "production") return

  getHeadComponents().forEach(el => {
    // а после сборки его не будет
    if (el.type === "style") {
      el.type = "link"
      el.props["href"] = el.props["data-href"]
      el.props["rel"] = "stylesheet"
      el.props["type"] = "text/css"

      delete el.props["data-href"]
      delete el.props["dangerouslySetInnerHTML"]
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# Webpack

Небольшой пример тюнинга конфигурации webpack был рассмотрен в предыдущем разделе.

// gatsby-node.js
exports.onCreateWebpackConfig = ({ stage, getConfig, rules, loaders, actions }) => {
  actions.setWebpackConfig({
    // ...
  })
}
1
2
3
4
5
6

Здесь упомяну лишь о том, как ставить свои плагины. На самом деле нет ничего проще. Ставим что хотим + обязательно babel-preset-gatsby.

$ npm i --save-dev babel-preset-gatsby @babel/plugin-proposal-optional-chaining
1

Создаём свой .babelrc в корне проекта.

{
  "plugins": [
    "@babel/plugin-proposal-optional-chaining"
  ],
  "presets": [
    [
      "babel-preset-gatsby",
      {
        "targets": {
          "browsers": [
            ">0.5%",
            "not dead"
          ]
        }
      }
    ]
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Всё. Уже можно использовать plugin-proposal-optional-chaining.

# Картинки вне static

Импорт картинок, лежащих не глобально в static, а где-нибудь в src/assets, можно произвести следующим образом:

// src/pages/index.js
import React from "react"
import image from "assets/images/great.jpg"

export default function HomePage() {
  return (
    <main>
      <img src={image} alt="Great Gatsby" width={375} height="auto" />
    </main>
  )
}
1
2
3
4
5
6
7
8
9
10
11

# Dark mode

В мобильных приложениях появился dark mode. Осмелюсь доложить, штука неплохая. Вечерами спасает глаза. Те, кто хочет завести себе PWA и быть ближе к нативным мобильным, явно захотят и эту фичу. Что ж, сделаем!

Понадобятся стили, общие для всех страниц для объявления в них css-переменных. В примере динамически меняться будут только фон и текст страницы.

/* assets/styles/base.css */
:root {
  --textColor: #3f3f3f;
  --bgColor: #fafafa;
}

[data-theme="dark"] {
  --textColor: #d9d7e0;
  --bgColor: #232129;
}

body {
  background-color: var(--bgColor);
  color: var(--textColor);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Идём на сторону клиента. Код в onClientEntry отрабатывает только один раз когда посетитель зашёл на страницу.

// gatsby-browser.js
// подключаем глобальные стили
require("./src/assets/styles/base.css")

exports.onClientEntry = () => {
  enableTheme()
}

// Если тема не установлена, менять её в зависимости от времени суток.
// Если установлена пользователем, сохранить его выбор в localStorage,
// чтобы не заставлять человека выбирать её снова и снова при переходе на другие страницы
function enableTheme() {
  const root = document.getElementsByTagName("body")[0]
  const userTheme = localStorage.getItem("theme-ui-color-mode")
  const date = new Date().getHours()
  const theme = userTheme ? userTheme : date >= 5 && date < 20 ? "light" : "dark"

  // выставить data-атрибут темы для элемента body
  root.setAttribute("data-theme", theme)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Неплохо бы сделать переключатель, чтобы пользователь сам мог менять оформление.

// src/components/theme-switcher.js
import React from "react"

const onThemeToggle = () => {
  // получить тему из data-theme
  const root = document.getElementsByTagName("body")[0]
  const theme = root.getAttribute("data-theme")
  const uiTheme = theme === "dark" ? "light" : "dark"

  // изменить атрибут
  // если была светлая тема, поставить тёмную и наоборот
  root.setAttribute("data-theme", uiTheme)
  // запомнить выбор
  localStorage.setItem("theme-ui-color-mode", uiTheme)
}

export default function ThemeSwitcher() {
  return (
    <button onClick={onThemeToggle} type="button">
      Change Theme
    </button>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

Ну, и подключить компонент-switcher куда-нибудь на страницу или в другой компонент: в шапку сайта, например.

// src/pages/index.js
import React from "react"
import Layout from "components/layout"
import ThemeSwitcher from "components/theme-switcher"

export default function HomePage({ pageContext: { language } }) {
  return (
    <Layout lang={language}>
      <ThemeSwitcher />
      <h1>Home</h1>
    </Layout>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13

Проверяем, кликаем.

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

Когда вы пишете в своих заметках тонны кода как это делаю я, подсветка жизненно необходима. Для gatsby есть два варианта: prism.js или highlight.js. Возьмём первый.

$ npm i prismjs gatsby-remark-prismjs
1

Настроек у пакета достаточно. Не вижу смысла описывать их все здесь, лучше увидеть полную картину.

Ограничимся минимумом. Надо сказать gatsby-plugin-mdx, чтобы он применил prism.js.

// gatsby-config.js
module.exports = {
  plugins: [
    // ...
    {
      resolve: `gatsby-plugin-mdx`,
      options: {
        gatsbyRemarkPlugins: [
          {
            resolve: `gatsby-remark-prismjs`,
            options: {
              classPrefix: "language-",
              inlineCodeMarker: null,
            },
          },
        ],
      },
    },
  ],
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Не забыть подключить стили подсветки. Можно глобально, а можно только на страницах записей.

// gatsby-browser.js
require("prismjs/themes/prism-solarizedlight.css")
1
2

И посмотреть-таки, что получилось.

<!--content/ru/frontend/gatsby/gatsby.mdx-->
Кусок кода css.

```css
.gatsby-highlight {
  background-color: #fdf6e3;
  border-radius: 0.3em;
}
```
1
2
3
4
5
6
7
8
9

# Комментарии

Редкий блог обходится без комментариев. Здесь у каждого свои предпочтения, начиная от выбора самой системы комментирования, и заканчивая реализацией её подключения. Мне интересно поведение, когда комментарии показываются только если посетитель сам захотел их увидеть. Следовательно, до этого момента (а он может не наступить никогда), я не хочу грузить какие-то сторонние скрипты.

Реализация именно такого поведения и представлена ниже. Из всего многообразия похожих скриптовых систем используем disqus.

Первый шаг: добавить id, который дают в disqus для идентификации сайта.

// gatsby-config.js
module.exports = {
  siteMetadata: {
    disqusID: `xxxxx.disqus.com`,
  },
}
1
2
3
4
5
6

Компонент комментариев.

// src/components/comments.js
import React, { useState, useEffect, memo } from "react"
import { graphql, useStaticQuery } from "gatsby"

// получаем идентификатор
const getDisqusId = graphql`
  {
    site {
      siteMetadata {
        disqusID
      }
    }
  }
`
// подключение стороннего скрипта на сайт
const createComments = id => {
  const page = window.document;
  const script = page.createElement("script");

  script.src = `//${id}/embed.js`;
  script.setAttribute("data-timestamp", +new Date());
  (page.head || page.body).appendChild(script);
}

const Comments = () => {
  // Забираем идентификатор.
  // Если юзер тыкнул по кнопке (isThreadOpened = true),
  // подключаем скрипт (createComments)
  const response = useStaticQuery(getDisqusId)
  const [isThreadOpened, setIsThreadOpened] = useState(false)
  const handleOpenThread = () => {
    setIsThreadOpened(true)
    createComments(response.site.siteMetadata.disqusID)
  }

  // используем memo, поэтому есть вероятность,
  // что при переходе на другую страницу, компонент Comments не обновится
  // на всякий случай удаляем все ifram'ы, которые могли остаться после disqus
  useEffect(() => {
    document
      .querySelectorAll("iframe")
      .forEach(elem => elem.parentNode.removeChild(elem))
  }, [])

  // показать кнопку, если комментарии ещё не открывали
  return (
    <div className="comments">
      {!isThreadOpened && (
        <button onClick={handleOpenThread}>
          Show comments
        </button>
      )}
      <div id="disqus_thread" />
    </div>
  )
}

export default memo(Comments)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

Подключаем компонент на страницы записей.

// src/templates/post-template.js
import React from "react"
import { graphql } from "gatsby"
import { MDXRenderer } from "gatsby-plugin-mdx"
import Comments from "components/comments"

export default function PostTemplate({ data }) {
  const { h1 } = data.mdx.frontmatter
  const { body } = data.mdx

  return (
    <main>
      <h1>{h1}</h1>
      <MDXRenderer>{body}</MDXRenderer>
      <Comments />
    </main>
  )
}
// ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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

# MDX

Plugin MDX — это в первую очередь доступ к множеству пакетов. Для gatsby они подключаются в двух вариациях: как адаптированный gatsby-пакет или как родной пакет remark.

Выглядит это так:

module.exports = {
  plugins: [
    {
      resolve: "gatsby-plugin-mdx",
      options: {
        extensions: [".mdx", ".md"],
        // родные плагины remark
        remarkPlugins: [
          // ставит внешним ссылкам атрибуты rel="nofollow, noopener, noreferrer"
          // открывает их в новой вкладке
          require("remark-external-links"),
        ],
        // адаптированные плагины
        gatsbyRemarkPlugins: [
          "gatsby-remark-images",
          "gatsby-remark-prismjs",
        ],
      },
    },
  ],
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

А самое вкусное то, что можно импортировать компоненты прямо в markdown-файлы. Это позволит сделать почти всё, что угодно. Вывод красивых табличек-предупреждений, нормальные вкладки с табами. Возможно, даже галерею изображений.

Самый простой случай — предупреждения. Делаем.

/* components/tip/tip.module.css */
/* стиль с именем с module.css это css-модуль */
.tip {
  padding: 2rem;
  color: white;
}
.heading { font-weight: bold; }
.warning { background-color: coral; }
.danger { background-color: crimson; }
1
2
3
4
5
6
7
8
9

Сам компонент:

// components/tip/index.js
import React from "react"
import styles from "./tip.module.css"

export default function Tip ({ type, heading, children }) {
  return (
    <div className={[styles.tip, styles[type]].join(' ')}>
      <p className={styles.heading}>{heading}</p>
      {children}
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12

Использование в mdx-файле:

import Tip from "components/tip"

<Tip heading="Danger!" type="danger">
My danger text
</Tip>
1
2
3
4
5

# Публикация

Вариантов много. Рассмотрим деплой на github pages.

От знакомства с vuepress у меня остался прекрасный маленький скрипт, который я оставлю здесь, потому как он универсален.

#!/usr/bin/env sh

# abort on errors
set -e

# build
npm run build

# navigate into the build output directory
cd public

# if you are deploying to a custom domain
echo 'blogname.com' > CNAME

git init
git add -A
git commit -m 'deploy'

# git push -f git@github.com:<имя_юзера>/<имя_репозитория>.git <имя_ветки>
git push -f git@github.com:jack/jack.github.io.git master:gh-pages
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# Послесловие

В статье осталось много неосвящённых моментов: как поднять PWA (точно надо?), можно ли брать gatsby когда нужно сотворить Headless CMS + статику (можно), обязательно ли использовать graphql (нет).

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

С чем сравнивать gatsby когда стоишь перед выбором? Зависит от потребностей.

На первой ступени стоят генераторы статики.

Тот же vuepress был создан в первую очередь для написания документации. И с этой задачей он справляется на 100% без ручного вмешательства. Его можно сравнить с octopress или jekyll. Вернее, из него можно сделать octopress (и даже лучше).

Вторая ступень... не знаю как обозначить эти фреймворки. Gatsby и его молодой аналог на Vue — Gridsome. Они предоставляют уже больше возможностей: обращение к серверу или напрямую к базе данных, или использование классики в виде локально хранящихся markdown-файлов.

Третья ступень: Next.js/Nuxt.js. Это если нужен server side rendering «из коробки», но одной лишь статикой не обойтись. В основном это приложения с множеством страниц, когда интерфейс подстраивается под каждого пользователя индивидуально.

Про мир Angular сказать не могу, но явно у них есть свои решения.

В любом случае смотреть надо на наличие хорошей документации, поддержки сообщества и того, сколько средств вливается в инструмент. У gatsby в этом плане на текущий момент всё очень неплохо.

У меня остались хорошие впечатления. И я, пожалуй, перееду на gatsby когда надумаю в следующий раз делать глобальный редизайн.