Великолепный Gatsby.js

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

Установка

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

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

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

.
├── 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

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

Graphql

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

// gatsby-config.js
module.exports = {
  siteMetadata: {
    title: "Great gatsby multi-language blog",
  },
}

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

{
  site {
    siteMetadata {
      title
    }
  }
}

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

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

Контент

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

Данные

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

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

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

Мой контент
![alt](./tree.jpg)

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

.
├── content
│   ├── en
│   │   └── frontend
│   └── ru
│       ├── frontend
│       │   ├── gatsby
│       │   │   ├── gatsby.jpg
│       │   │   └── gatsby.mdx

Инструменты

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

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

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

$ npm i gatsby-source-filesystem

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

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

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

// 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,
            },
          },
        ],
      },
    },
  ],
}

Данные подготовлены, пакеты установлены. Дальше следует выяснить как работает механизм передачи данных. Для каждого 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 },
    })
  })
}

Шаблон

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

// 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>
  )
}

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

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

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

// получить все записи, отсортированные по дате
const getPosts = graphql`
  query allPosts {
    allMdx(sort: { frontmatter: {date: 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>
  )
}

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

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

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

$ npm i gatsby-plugin-intl

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

// 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,
      },
    },
  ],
}

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

src
└── translations
    ├── en.json
    ├── ru.json
    └── index.js

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

// src/translations/en.json
{
  "home.h1": "Hello, welcome {user}",
  "home.title": "Home title",
  "home.description": "Home description"
}

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

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

export default {
  en: locale_en,
  ru: locale_ru,
}

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

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

<!--content/en/frontend/gatsby/gatsby.mdx-->
---
h1: Post heading
lang: en

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

// 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 },
    })
  })
}

И 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)
      }
    }
  }
`

Страницы в 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>
  )
}

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

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

$ npm i react-helmet gatsby-plugin-react-helmet

В gatsby.config.js добавляем последний. Так мета-теги будут учитываться при генерации статики.

// gatsby.config.js
plugins: [
  'gatsby-plugin-react-helmet',
]

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

// gatsby-node.js
exports.onCreateWebpackConfig = ({ actions }) => {
  actions.setWebpackConfig({
    resolve: {
      modules: [path.resolve(__dirname, "src"), "node_modules"],
    },
  })
}

Дописываем в 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>
    </>
  )
}

Допустим, 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>
  )
}

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

gatsby-ssr

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

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

  const headComponents = getHeadComponents()

  headComponents.forEach(el => {
    // для итоговой сборки сделать отдельный css-файл
    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"]
    }
  })

  replaceHeadComponents(headComponents)
}

Webpack

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

// gatsby-node.js
exports.onCreateWebpackConfig = ({ getConfig, actions, plugins }) => {
  actions.setWebpackConfig({
    // отключить  source-map в итоговой сборке
    devtool: getConfig().mode === "production" ? false : "source-map",
    resolve: {
      modules: [path.resolve(__dirname, "src"), "node_modules"],
    },
    // по желанию вырубить react-dev-tools
    plugins: [
      plugins.define({
        '__REACT_DEVTOOLS_GLOBAL_HOOK__': `({ isDisabled: true })`
      })
    ],
  })
}

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

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

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

{
  "plugins": [
    "@babel/plugin-proposal-optional-chaining"
  ],
  "presets": [
    [
      "babel-preset-gatsby",
      {
        "targets": {
          "browsers": [
            ">0.5%",
            "not dead"
          ]
        }
      }
    ]
  ]
}

Всё. Уже можно использовать 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>
  )
}

Dark mode

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

Я опишу сложный вариант, чтобы продемонстрировать работу с gatsby-browser.js. Можно проще: менять класс у html или body (рабоать это будет, конечно, до перезагрузки страницы).

Понадобятся стили, общие для всех страниц для объявления в них 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);
}

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

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

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

// Если тема не установлена, применить тему по-умолчанию.
// Если установлена пользователем, сохранить его выбор в localStorage,
// чтобы не заставлять человека выбирать её снова и снова при переходе на другие страницы
function enableTheme() {
  const root = document.getElementsByTagName("body")[0]
  try {
    const uiTheme = localStorage.getItem("theme-ui-color-mode")
    const theme = uiTheme ? uiTheme : "light"
    // выставить data-атрибут темы для элемента body
    root.setAttribute("data-theme", theme)
  } catch (error) {
    console.error('localStorage error', error);
  }
}

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

// 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"

  try {
    root.setAttribute("data-theme", uiTheme)
    // запомнить выбор
    localStorage.setItem("theme-ui-color-mode", uiTheme)
  } catch (error) {
    return false
  }
}

export default function ThemeSwitcher() {
  return (
    <button onClick={onThemeToggle} type="button">
      Change Theme
    </button>
  )
}

Ну, и подключить компонент-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>
  )
}

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

Если хочется менять тему в зависимости от времени суток, лучший вариант prefers-color-scheme. Тогда приведённый выше код даже писать не придётся.

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

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

$ npm i prismjs gatsby-remark-prismjs

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

Ограничимся минимумом. Надо сказать 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,
            },
          },
        ],
      },
    },
  ],
}

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

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

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

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

```css
.gatsby-highlight {
  background-color: #fdf6e3;
  border-radius: 0.3em;
}
```

Комментарии

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

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

Код disqus.

// src/components/disqus.js
const DISQUS_ID = "xxxxx.disqus.com" // ваш идентификатор

export const runDisqus = () => (function () {
  const page = window.document;
  const dscript = page.createElement("script");
  dscript.src = `//${DISQUS_ID}/embed.js`;
  dscript.setAttribute("data-timestamp", +new Date());
  (page.head || page.body).appendChild(dscript);
})();

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

// src/components/comments.js
import React, { useState, useEffect } from "react"
import { runDisqus } from "./disqus"

const Comments = () => {
  // Если юзер тыкнул по кнопке (isThreadOpened = true),
  // динамически подключаем скрипт disqus
  const [isThreadOpened, setIsThreadOpened] = useState(false)

  const handleOpenThread = () => {
    setIsThreadOpened(true)
    if (typeof window !== "undefined") { runDisqus() }
  }

  // переход с Link не перезагружает страницу полностью
  // поэтому выполняем DISQUS.reset, чтобы корректно обновлять
  // комментарии на странице
  useEffect(() => {
    if (window.DISQUS) {
      window.DISQUS.reset()
    }
  }, [])

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

export default Comments

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

// 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>
  )
}
// ...

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

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",
        ],
      },
    },
  ],
}

А самое вкусное то, что можно импортировать компоненты прямо в 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; }

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

// components/tip/index.js
import React from "react"
import * as 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>
  )
}

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

import Tip from "components/tip"

<Tip heading="Danger!" type="danger">
My danger text
</Tip>

Публикация

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

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

#!/usr/bin/env sh

# abort on errors
set -e

# build and navigate into the build output directory
npm run build && 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

Послесловие

В статье осталось много неосвящённых моментов: как поднять 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 когда надумаю в следующий раз делать глобальный редизайн.