Как известно на одних бойлерплейтах далеко не уедешь, поэтому приходится лезть вглубь любой технологии, чтобы научиться писать что-то стоящее. В этой статье рассмотрены детали Gatsby.js, знание которых позволит вам создавать и поддерживать сложные вебсайты и блоги.
Предыдущая статья о том как создать и опубликовать личный блог используя JAM-stack
Темы рассмотренные далее:
- Структура страниц и роутинг
- Компоненты, шаблоны и их взаимодействие
- Работа с данными
- Плагины
- Стилизация приложения
- SEO-оптимизация с использованием react-helmet
- Настройка PWA
Подготовка
yarn global add gatsby-cli
npx gatsby new gatsby-tutorial https://github.com/gatsbyjs/gatsby-starter-hello-world
cd gatsby-tutorial
git init
git add .
git commit -m "init commit"
yarn start
Если в консоли нет ошибок, а в браузере по пути http://localhost:8000 виднеется "Hello world!" значит всё работает исправно. Можно попробовать изменить содержимое файла /src/pages/index.js, чтобы проверить hot-reload.
Структура страниц и роутинг
Чтобы создать страницу в Gatsby, достаточно просто поместить новый файл в папку /src/pages, и он будет скомпилирован в отдельную HTML-страницу. Важно заметить, что путь к этой странице будет соответствовать фактическому пути с названием. Например, добавим ещё несколько страниц:
src
└── pages
├── about.js
├── index.js
└── tutorial
├── part-four.js
├── part-one.js
├── part-three.js
├── part-two.js
└── part-zero.js
Контент пока не важен, поэтому можно использовать любой текст, чтобы различать страницы
import React from "react";
export default () => <div>Welcome to tutorial/part-one</div>;
Проверяем в браузере:
Вот таким образом, можно структурируя файлы, сразу решать вопросы роутинга.
Также существует специальный createPage API, с помощью которого можно более гибко управлять путями и названиями страниц, но для работы с ним нам понадобится понимание работы данных в Gatsby, поэтому рассмотрим его чуть дальше в статье.
Объединим созданные страницы с помощью ссылок, для этого воспользуемся компонентом <Link />
из пакета Gatsby, который создан специально для внутренней навигации. Для всех внешних ссылок следует использовать обычный <a>
тег.
/src/pages/index.js
import React from "react";
import { Link } from "gatsby";
export default () => (
<div>
<ul>
<li>
<Link to="/about">about</Link>
</li>
<li>
<Link to="/tutorial/part-zero">Part #0</Link>
</li>
<li>
<Link to="/tutorial/part-one">Part #1</Link>
</li>
<li>
<Link to="/tutorial/part-two">Part #2</Link>
</li>
<li>
<Link to="/tutorial/part-three">Part #3</Link>
</li>
<li>
<Link to="/tutorial/part-four">Part #4</Link>
</li>
</ul>
</div>
);
<Link>
под капотом имеет очень хитрый механизм по оптимизации загрузки страниц и поэтому используется вместо<a>
для навигации по сайту. Детальнее можно почитать здесь.
Страницы созданы, ссылки добавлены, получается что с навигацией закончили.
Компоненты, шаблоны и их взаимодействие
Как известно, в любом проекте всегда есть повторяющиеся элементы, для вебсайтов это хедер, футер, навигационная панель. Также страницы, вне зависимости от контента, строятся по определённой структуре, и так как Gatsby это компилятор для React, здесь используется тот же компонентный подход для решения этих проблем.
Создадим компоненты для хедера и навигационной панели:
/src/components/header.js
import React from "react";
import { Link } from "gatsby";
/**
* обратите внимание на то что изображение для логотипа
* импортируется также, как и в обычном React-проекте.
* Это временное и не оптимальное решение, потому что картинка
* поставляется "как есть". Немного далее мы рассмотрим
* как это делать "правильно" используя GraphQL и gatsby-плагины
*/
import logoSrc from "../images/logo.png";
export default () => (
<header>
<Link to="/">
<img src={logoSrc} alt="logo" width="60px" height="60px" />
</Link>
That is header
</header>
);
/src/components/sidebar.js
import React from "react";
import { Link } from "gatsby";
export default () => (
<div>
<ul>
<li>
<Link to="/about">about</Link>
</li>
<li>
<Link to="/tutorial/part-zero">Part #0</Link>
</li>
<li>
<Link to="/tutorial/part-one">Part #1</Link>
</li>
<li>
<Link to="/tutorial/part-two">Part #2</Link>
</li>
<li>
<Link to="/tutorial/part-three">Part #3</Link>
</li>
<li>
<Link to="/tutorial/part-four">Part #4</Link>
</li>
</ul>
</div>
);
и добавим их в /src/pages/index.js
import React from "react";
import Header from "../components/header";
import Sidebar from "../components/sidebar";
export default () => (
<div>
<Header />
<Sidebar />
<h1>Index page</h1>
</div>
);
Проверяем:
Всё работает, но нам нужно импортировать Header и Sidebar на каждую страницу отдельно, что не очень то и удобно, и чтобы решить этот вопрос достаточно создать layout-компонент, и обернуть им каждую страницу.
Gatsby layout == React container
да-да, именно нестрогое равенство, потому что это "почти" одно и тоже
/src/components/layout.js
import React from "react";
import Header from "./header";
import Sidebar from "./sidebar";
export default ({ children }) => (
<>
<Header />
<div
style={{ margin: `0 auto`, maxWidth: 650, backgroundColor: `#eeeeee` }}
>
<Sidebar />
{children}
</div>
</>
);
/src/pages/index.js (и все остальные страницы)
import React from "react";
import Layout from "../components/layout";
export default () => (
<Layout>
<h1>Index page</h1>
</Layout>
);
Готово, смотрим в браузер:
Почему в проекте все названия файлов с маленькой буквы? Для начала определимся что namespacing для React происходит из того, что "каждый файл это класс, а класс всегда называется с большой буквы". В Gatsby файлы по прежнему содержат классы, но есть одно "но" ― "каждый файл является потенциальной страницей, а его название ― URL к этой странице". Комьюнити пришло к выводу о том, что ссылки вида http://domain.com/User/Settings
это не comme-il-faut и утвердили kebab-case для названий файлов.
src
├── components
│ ├── header.js
│ ├── layout.js
│ └── sidebar.js
├── images
│ └── logo.png
└── pages
├── about.js
├── index.js
└── tutorial
├── part-eight.js
├── part-five.js
├── part-four.js
├── part-one.js
├── part-seven.js
├── part-six.js
├── part-three.js
├── part-two.js
└── part-zero.js
Работа с данными
Теперь, когда структура сайта готова, можно переходить к наполнению контентом. Классический "хардкод" подход не устраивал создателей JAM-стека, так же как и "рендерить контент из AJAX-запросов" и поэтому они предложили заполнять сайты контентом во время компиляции. В случае с Gatsby за это отвечает GraphQL, который позволяет удобно работать с потоками данных из любых источников.
Рассказать про GraphQL в двух словах невозможно, поэтому желательно изучить его самостоятельно либо подождать моей следующей статьи. Детальнее о работе с GraphQL можно почитать здесь.
Для работы с GraphQL, со второй версии, в пакете gatsby
есть компонент StaticQuery, который может использоваться как на страницах, так и в простых компонентах, и в этом его главное отличие от его предшественника ― page query. Пока что наш сайт не соединён с какими-то источниками данных, поэтому попробуем вывести метаданные страниц, для примера, а затем перейдем к более сложным вещам.
Чтобы построить query
нужно открыть http://localhost:8000/___graphql, и пользуясь боковой панелью с документацией найти доступные данные о сайте, и не забудьте про автодополнение.
/src/components/sidebar.js
import React from "react";
import { Link, StaticQuery, graphql } from "gatsby";
export default () => (
<StaticQuery
query={graphql`
{
allSitePage {
edges {
node {
id
path
}
}
}
}
`}
render={({ allSitePage: { edges } }) => (
<ul>
{edges.map(({ node: { id, path } }) => (
<li key={id}>
<Link to={path}>{id}</Link>
</li>
))}
</ul>
)}
/>
);
Теперь мы используя query
получаем данные о страницах, которые рендерим в панели навигации, и больше не нужно переживать по поводу того что ссылка не будет соответствовать названию, потому что все данные собираются автоматически.
По факту это все данные, которые могут быть на нашем сайте без использования сторонних плагинов и без старого доброго "хардкода", поэтому мы плавно переходим в следующую тему нашей статьи ― плагины.
Плагины
По своей сути Gatsby это компилятор с кучей плюшек, которыми как раз и являются плагины. С помощью них можно настраивать обработку тех или иных файлов, типов данных и различных форматов.
Создадим на корневом уровне приложения файл /gatsby-config.js. который отвечает за конфигурацию компилятора в целом, и попробуем настроить первый плагин для работы с файлами:
Установка плагина:
yarn add gatsby-source-filesystem
Конфигурация в файле /gatsby-config.js:
module.exports = {
plugins: [
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/images/`,
}
}
],
}
/**
* gatsby-config.js это файл который должен
* по умолчанию экспортировать объект JS
* с конфигурацией для компилятора
*/
module.exports = {
/**
* поле 'plugins' описывает pipeline процесса
* компиляции, и состоит из набора плагинов
*/
plugins: [
/**
* каждый плагин может быть указан в виде строки,
* или в виде объекта для настройки его опций
*/
`gatsby-example-plugin`,
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/images/`,
}
}
],
}
Помните мы говорили про "правильный" импорт картинок в Gatsby?
/src/components/header.js
import React from "react";
import { Link, StaticQuery, graphql } from "gatsby";
export default () => (
<StaticQuery
query={graphql`
{
allFile(filter: { name: { eq: "logo" } }) {
edges {
node {
publicURL
}
}
}
}
`}
render={({
allFile: {
edges: [
{
node: { publicURL }
}
]
}
}) => (
<header>
<Link to="/">
<img src={publicURL} alt="logo" width="60px" height="60px" />
</Link>
That is header
</header>
)}
/>
);
На сайте ничего не изменилось, но теперь картинка подставляется с помощью GraphQL, вместо простого webpack-импорта. С первого взгляда, может показатся что конструкции слишком сложные и это были лишние телодвижения, но давайте не спешить с выводами, потому что дело всё в тех же самых плагинах. Например если бы мы решили размещать на сайте тысячи фотографий, то нам в любом случае пришлось задумываться об оптимизации загрузки всего контента, и чтобы не строить свой lazy-load процесс с нуля, мы бы просто добавили gatsby-image плагин, который бы оптимизировал загрузку всех картинок, импортируемых с помощью query
.
Установка плагинов для стилизации:
yarn add gatsby-plugin-typography react-typography typography typography-theme-noriega node-sass gatsby-plugin-sass gatsby-plugin-styled-components styled-components babel-plugin-styled-components
gatsby-config.js
module.exports = {
plugins: [
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/images/`
}
},
// add style plugins below
`gatsby-plugin-typography`,
`gatsby-plugin-sass`,
`gatsby-plugin-styled-components`
]
};
На официальном сайте можно найти плагин на любой вкус.
Стилизация приложения
Приступим к стилизации приложения используя различные подходы. В предыдущем шаге мы уже установили плагины для работы с SASS, styled-components и библиотекой typography.js, при этом важно отметить что css.modules поддерживаются "из коробки".
Начнём работу с глобальных стилей, которые, как и остальные вещи относящиеся ко всему сайту, должны быть сконфигурированы в файле /gatsby-browser.js:
import "./src/styles/global.scss";
Детальнее про gatsby-browser.js
/src/styles/global.scss
body {
background-color: lavenderblush;
}
В силу разных причин, тенденции последних лет склоняются в сторону "CSS in JS" подхода, поэтому не стоит злоупотреблять глобальными стилями и лучше ограничиться указанием шрифта и переиспользуемых классов. В этом конкретном проекте планируется использование Typography.js для этих целей, поэтому глобальные стили останутся пустыми.
Вы уже могли заметить изменения внешнего вида сайта после добавления gatsby-plugin-typography
в конфигурацию ― это потому что был применён его пресет по умолчанию, а сейчас мы сконфигурируем его под себя.
/src/utils/typography.js
import Typography from "typography";
import theme from "typography-theme-noriega";
const typography = new Typography(theme);
export default typography;
Можно выбрать любой другой пресет из списка или создать свой собственный используя API пакета (пример конфигурации официального сайта Gatsby)
/gatsby-config.js
module.exports = {
plugins: [
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/images/`
}
},
{
resolve: `gatsby-plugin-typography`,
options: {
pathToConfigModule: `src/utils/typography`
}
},
`gatsby-plugin-sass`,
`gatsby-plugin-styled-components`
]
};
И в зависимости от выбранного пресета, глобальный стиль сайта будет изменён. Каким подходом настраивать глобальные стили решайте сами, это вопрос личных предпочтений и различий с технической точки зрения нет, а мы переходим к стилизации компонентов используя styled-components:
Добавим файл с глобальными переменными /src/utils/vars.js
export const colors = {
main: `#663399`,
second: `#fbfafc`,
main50: `rgba(102, 51, 153, 0.5)`,
second50: `rgba(251, 250, 252, 0.5)`,
textMain: `#000000`,
textSecond: `#ffffff`,
textBody: `#222222`
};
import React from "react";
import { Link, StaticQuery, graphql } from "gatsby";
import styled from "styled-components";
import { colors } from "../utils/vars";
const Header = styled.header`
width: 100%;
height: 3em;
display: flex;
justify-content: space-between;
align-items: center;
background-color: ${colors.main};
color: ${colors.textSecond};
padding: 0.5em;
`;
const Logo = styled.img`
border-radius: 50%;
height: 100%;
`;
const logoLink = `height: 100%;`;
export default () => (
<StaticQuery
query={graphql`
{
allFile(filter: { name: { eq: "logo" } }) {
edges {
node {
publicURL
}
}
}
}
`}
render={({
allFile: {
edges: [
{
node: { publicURL }
}
]
}
}) => (
<Header>
That is header
<Link to="/" css={logoLink}>
<Logo src={publicURL} alt="logo" />
</Link>
</Header>
)}
/>
);
import React from "react"
import { Link, StaticQuery, graphql } from "gatsby"
import styled from "styled-components"
import { colors } from "../utils/vars"
const Sidebar = styled.section`
position: fixed;
left: 0;
width: 20%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
background-color: ${colors.second};
color: ${colors.textMain};
`
const navItem = `
display: flex;
align-items: center;
margin: 0 1em 0 2em;
padding: 0.5em 0;
border-bottom: 0.05em solid ${colors.mainHalf};
postion: relative;
color: ${colors.textBody};
text-decoration: none;
&:before {
content: '';
transition: 0.5s;
width: 0.5em;
height: 0.5em;
position: absolute;
left: 0.8em;
border-radius: 50%;
display: block;
background-color: ${colors.main};
transform: scale(0);
}
&:last-child {
border-bottom: none;
}
&:hover {
&:before {
transform: scale(1);
}
}
`
export default () => (
<StaticQuery
query={graphql`
{
allSitePage {
edges {
node {
id,
path
}
}
}
}
`}
render={({
allSitePage: {
edges
}
}) => (
<Sidebar>
{
edges.map(({
node: {
id,
path
}
}) => (
<Link to={path} key={id} css={navItem} >{id}</Link>
))
}
</Sidebar>
)}
/>
)
Уже существующие элементы стилизованы, и пришло время связать контент с Contentful, подключить маркдаун плагин и сгенерировать страницы используя createPages API.
Детальнее о том как связать Gatsby и Contentful читайте в предыдущей статье
[
{
"id": "title",
"type": "Symbol"
},
{
"id": "content",
"type": "Text",
},
{
"id": "link",
"type": "Symbol",
},
{
"id": "orderNumber",
"type": "Integer",
}
]
Установка пакетов:
yarn add dotenv gatsby-source-contentful gatsby-transformer-remark
/gatsby-config.js
if (process.env.NODE_ENV === "development") {
require("dotenv").config();
}
module.exports = {
plugins: [
`gatsby-transformer-remark`,
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/images/`,
}
},
{
resolve: `gatsby-plugin-typography`,
options: {
pathToConfigModule: `src/utils/typography`,
},
},
{
resolve: `gatsby-source-contentful`,
options: {
spaceId: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
},
},
`gatsby-plugin-sass`,
`gatsby-plugin-styled-components`,
],
}
Удаляем папку /src/pages со всеми файлами внутри и создаем новый файл, для управления узлами в Gatsby:
/gatsby-node.js
const path = require(`path`);
/**
* экспортируемая функция, которая перезапишет существующую по умолчанию
* и будет вызвана для генерации страниц
*/
exports.createPages = ({ graphql, actions }) => {
/**
* получаем метод для создания страницы из экшенов
* чтобы избежать лишних импортов и сохранять контекст
* страницы и функции
*/
const { createPage } = actions;
return graphql(`
{
allContentfulArticle {
edges {
node {
title
link
content {
childMarkdownRemark {
html
}
}
}
}
}
}
`).then(({ data: { allContentfulArticle: { edges } } }) => {
/**
* для каждого из элементов из ответа
* вызываем createPage() функцию и передаём
* внутрь данные с помощью контекста
*/
edges.forEach(({ node }) => {
createPage({
path: node.link,
component: path.resolve(`./src/templates/index.js`),
context: {
slug: node.link
}
});
});
});
};
Детальнее про gatsby-node.js
Создаём template-файл, который будет основой для генерируемых страниц
/src/templates/index.js
import React from "react";
import { graphql } from "gatsby";
import Layout from "../components/layout";
export default ({
data: {
allContentfulArticle: {
edges: [
{
node: {
content: {
childMarkdownRemark: { html }
}
}
}
]
}
}
}) => {
return (
<Layout>
<div dangerouslySetInnerHTML={{ __html: html }} />
</Layout>
);
};
export const query = graphql`
query($slug: String!) {
allContentfulArticle(filter: { link: { eq: $slug } }) {
edges {
node {
title
link
content {
childMarkdownRemark {
html
}
}
}
}
}
}
`;
Почему здесь не используется<StaticQuery />
компонент? Всё дело в том что он не поддерживает переменные для построения запроса, а нам нужно использовать переменную$slug
из контекста страницы.
import React from "react";
import { Link, StaticQuery, graphql } from "gatsby";
import styled from "styled-components";
import { colors } from "../utils/vars";
const Sidebar = styled.section`
position: fixed;
left: 0;
width: 20%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
background-color: ${colors.second};
color: ${colors.textMain};
`;
const navItem = `
display: flex;
align-items: center;
margin: 0 1em 0 2em;
padding: 0.5em 0;
border-bottom: 0.05em solid ${colors.main50};
postion: relative;
color: ${colors.textBody};
text-decoration: none;
&:before {
content: '';
transition: 0.5s;
width: 0.5em;
height: 0.5em;
position: absolute;
left: 0.8em;
border-radius: 50%;
display: block;
background-color: ${colors.main};
transform: scale(0);
}
&:last-child {
border-bottom: none;
}
&:hover {
&:before {
transform: scale(1);
}
}
`;
export default () => (
<StaticQuery
query={graphql`
{
allContentfulArticle(sort: { order: ASC, fields: orderNumber }) {
edges {
node {
title
link
orderNumber
}
}
}
}
`}
render={({ allContentfulArticle: { edges } }) => (
<Sidebar>
{edges.map(({ node: { title, link, orderNumber } }) => (
<Link to={link} key={link} css={navItem}>
{orderNumber}. {title}
</Link>
))}
</Sidebar>
)}
/>
);
SEO-оптимизация
С технической стороны сайт можно считать готовым, поэтому давайте поработаем с его мета-данными. Для этого нам понадобятся следующие плагины:
yarn add gatsby-plugin-react-helmet react-helmet
react-helmet генерирует <head>...</head>
для HTML страниц и в связке с Gatsby рендерингом является мощным и удобным инструментом для работы с SEO.
/src/templates/index.js
import React from "react";
import { graphql } from "gatsby";
import { Helmet } from "react-helmet";
import Layout from "../components/layout";
export default ({
data: {
allContentfulArticle: {
edges: [
{
node: {
title,
content: {
childMarkdownRemark: { html }
}
}
}
]
}
}
}) => {
return (
<Layout>
<Helmet>
<meta charSet="utf-8" />
<title>{title}</title>
</Helmet>
<div dangerouslySetInnerHTML={{ __html: html }} />
</Layout>
);
};
export const query = graphql`
query($slug: String!) {
allContentfulArticle(filter: { link: { eq: $slug } }) {
edges {
node {
title
link
content {
childMarkdownRemark {
html
}
}
}
}
}
}
`;
Теперь title
сайта будет всегда соостветствовать названию статьи, что будет существенно влиять на выдачу сайта в результатах поиска конкретно по этому вопросу. Сюда же можно легко добавить <meta name="description" content="Описание статьи">
с описанием каждой статьи отдельно, этим дав возможность пользователю ещё на странице поиска понять о чём идет речь в статье, вообщем все возможности SEO теперь доступны и управляемы в одном месте.
Настройка PWA
Gatsby разработан, чтобы обеспечить первоклассную производительность "из коробки". Он берёт на себя вопросы по разделению и минимизации кода, а также оптимизации в виде предварительной загрузки в фоновом режиме, обработки изображений и др., так что создаваемый вами сайт обладает высокой производительностью без какой-либо ручной настройки. Эти функции производительности являются важной частью поддержки прогрессивного подхода к веб-приложениям.
Но кроме всего вышеперечисленного существуют три базовых критерия для сайта, которые определяют его как PWA:
- https-протокол
- наличие manifest.json
- оффлайн доступ к сайту за счёт service workers
Первый пункт не может быть решён силами Gatsby, так как домен, хостинг и протокол это вопросы деплоймента, и никак не разработки, но могу порекомендовать Netlify, который решает вопрос https по умолчанию.
Переходим к остальным пунктам, для этого установим два плагина:
yarn add gatsby-plugin-manifest gatsby-plugin-offline
и настроим их /src/gatsby-config.js
if (process.env.NODE_ENV === "development") {
require("dotenv").config();
}
module.exports = {
plugins: [
{
resolve: `gatsby-plugin-manifest`,
options: {
name: `GatsbyJS translated tutorial`,
short_name: `GatsbyJS tutorial`,
start_url: `/`,
background_color: `#f7f0eb`,
theme_color: `#a2466c`,
display: `standalone`,
icon: `public/favicon.ico`,
include_favicon: true
}
},
`gatsby-plugin-offline`,
`gatsby-transformer-remark`,
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/images/`
}
},
{
resolve: `gatsby-plugin-typography`,
options: {
pathToConfigModule: `src/utils/typography`
}
},
{
resolve: `gatsby-source-contentful`,
options: {
spaceId: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN
}
},
`gatsby-plugin-sass`,
`gatsby-plugin-styled-components`,
`gatsby-plugin-react-helmet`
]
};
Вы можете настроить свой манифест используя документацию, а также кастомизировать стратегию service-workers, перезаписав настройки плагина.
Никаких изменений в режиме разработки вы не заметите, но сайт уже соответствует последним требованиям мира web, и когда он будет размещён на https:// домене ему не будет равных.
Вывод
Пару лет назад когда я впервые столкнулся с проблемами вывода в интернет React-приложения, его поддержки и обновления контента, я и не мог представить что на рынке уже существовал JAM-stack подход, который упрощает все эти процессы, и сейчас я не перестаю удивлятся его простоте. Gatsby решает большинство вопросов влияющих на производительность сайта просто "из коробки", а если ещё немного разобравшись в тонкостях настроить его под свои нужды, то можно получить 100% показатели по всем пунктам в Lighthouse, чем существенно повлиять на выдачу сайта в поисковых системах (по крайней мере в Google).