Що таке Gridsome і як його готувати

August 24, 2021 — ☕☕🍪🍪🍪🍪 8 хвилин чтива

Пару років тому я натрапив на подкаст SynTax, в одному з епізодів якого промайнула згадка про Gridsome - генератор статичних вебсайтів, побудований на основі Vue. Ім'я нове, не настільки заїжджене, як, наприклад, Gatsby, тож я відклав його в пам'яті з наміром таки зробити на ньому свій блог (який на той момент перебував на стадії "розчарування в Jekyll").

Станом на вересень 2022 репозиторій Gridsome не відзначається суттєвою активністю, тож я не рекомендую використовувати цей генератор статичних сайтів, особливо для нових проектів

Вступ

Отже, почнімо спочатку. JAMstack - це акронім, який означає J(JavaScript), A(API), та M(Markup), і буквально означає вебсайт, бізнес-логіка якого цілком сконцентрована на клієнтській частині, і не потребує нічого додаткового від веб-сервера. Термін виник з подачі СЕО Netlify Матіаса Біілмана у 2015 році, хоча як технологія цей підхід далеко не новий (той самий Jekyll існує з 2008 року), і до цього була відома під назвою "Static site generators". Можна сперечатися щодо тотожності цих термінів, але в контексті даного допису це не важливо.

Логотип Gridsome

Gridsome як продукт з'явився у 2018 році, і фактично являється аналогом Gatsby, тільки побудований на основі Vue. Можливість роботи з будь-якими джерелами даних (локальні файли, CMS, чи публічні інтерфейси, як от: Google-таблиці, Airtable, стрічка Instagram, тощо), універсальний GraphQL-інтерфейс доступу до даних, набір статичних файлів як результат роботи генератора - ці риси притаманні як Gatsby, так і Gridsome.

Початок роботи

Отже, ми таки вирішили скористатися Gridsome. Документація дає достатньо інформації для початку.

  1. Для ініціалізації проекту (і роботи з проектом загалом) існує спеціальна утиліта командної стрічки, яку нам і треба буде встановити в першу чергу:
  • За допомогою Yarn yarn global add @gridsome/cli
  • За допомогою NPM npm install --global @gridsome/cli (далі в тексті я буду використовувати Yarn)
  1. Далі створимо проект, де і буде знаходитися наш блог:
  • Команда gridsome create __blog створить проект на основі усталеного шаблону
  • Команда gridsome develop, виконана всередині проекту - збере і запустить його. Після цього можна відкрити http://localhost:8080 у браузері, і побачити типовий "Hello world":

Наступним кроком документація пропонує створити .vue-компооненти в директорії src/pages. Я цей крок пропущу, оскільки у нас блог, і нам значно цікавіше створювати дописи, аніж статичні сторінки.

Робота зі статичним вмістом

Отже, візьмемо класичний варіант - статті в форматі .md. Для початку нам знадобиться шаблон для відображення цього вмісту. В Gridsome такі шаблони зберігаються у директорії src/templates. Створімо там файл MarkdownPost.vue із таким вмістом:

<template>
  <Layout>
    <h1 class="title">{{ $page.post.title }}</h1>
    <em>{{ $page.post.date }}</em>
    <div class="content" v-html="$page.post.content" />
  </Layout>
</template>

<script>
export default {
  metaInfo() { // тут ми встановлюємо вміст мета-тегів сторінки
    return {
      title: this.$page.post.title,
      meta: [
        {
          name: "description",
          content: this.$page.post.description,
        },
      ],
    };
  },
};
</script>

<page-query>
query MarkdownPost ($id: ID!) { // Назва GraphQL-колекції повинна співпадати з назвою шаблону
  post: markdownPost (id: $id) {
    title
    date
    description
    content
  }
}
</page-query>

Далі, нам потрібно підключити маркдаун-файли як джерело даних. У Gridsome за це відповідає плагін source-filesystem (так, вірно, як видно з назви, він працює з файлами загалом), яким ми і скористаємося. Внесімо такі зміни в файл gridsome.config.js:

plugins: [
  // ...
  {
    use: '@gridsome/source-filesystem',
    options: {
      baseDir: './content',
      path: '**/*.md',
      typeName: 'MarkdownPage', // Це назва нашої GraphQL-колекції, вона теж повинна співпадати з назвою шаблону
      remark: {
        externalLinksTarget: '_blank', // Корисні опції для посилань у тексті статті
        externalLinksRel: ['noopener', 'noreferrer'],
        plugins: ['@gridsome/remark-prismjs'], // Це для кольорової підсвітки синтаксису у вставках коду
      },
    },
  },
  // ...
]

Що ми забули?

По-перше, власне дані. Створімо каталог content у корені проекту - тут будуть жити наші дописи. Також я хочу мати можливість додавати ілюстрації, тому кожна окрема стаття у мене буде жити в окремому каталозі. Gridsome сам підтягне назву каталогу і використає її як посилання на статтю.

Для початку я створю всередині content каталог how-to-create-a-blog-on-gridsome, а там - файл index.md. В результаті у нас повинна вийти отака структура:

$> tree ./content/
./content/
└── how-to-create-a-blog-on-gridsome
    └── index.md

1 directory, 1 file

По-друге, кожного разу після внесення змін у файли gridsome.config.js чи gridsome.server.js потрібно перезавантажувати проект командою gridsome develop. Це стосується лише файлів конфігурації, сервер слухняно перезавантажує проект всіх інших випадках, коли ми змінюємо файли застосунку, чи вміст статей.

Після перезапуску gridsome скаже, що щось пішло не так...

$> gridsome develop
Gridsome v0.7.23

Initializing plugins...
Error: No transformer for 'text/markdown' is installed.
#...

...це ми вирішимо встановленням потрібних пакунків yarn add @gridsome/source-filesystem @gridsome/transformer-remark.

І головне - треба додати сам вміст. Відкриємо нашу статтю ./content/how-to-create-a-blog-on-gridsome/index.md у текстовому редакторі, і внесемо туди такий вміст:

---
title: Що таке Gridsome і як на ньому зробити блог
date: 2020-01-10
canonical_url: false
description: "Якийсь короткий опис. Markdown працює!"
---

![Example alt image](./gridsome-logo.png)

## Вступ

Beatae doloremque esse nulla porro quidem.

```jsx
class ProfilePage extends React.Component {
  showMessage = () => {
    alert('Followed ' + this.props.user);
  };
}
```

Зверніть увагу, шапка файлу вгорі насправді несхожа на синтаксис маркдауна. Це набір метаданих нашої статті, так званий front-matter. Справжній вміст починається з зображення.

До речі, про зображення - давайте одразу додамо картинку. Я обрав для цього логотип gridsome, для перевірки більш ніж достатньо.

В результаті структура вмісту повинна мати такий вигляд:

$> tree ./content/
./content/
└── how-to-create-a-blog-on-gridsome
    ├── gridsome-logo.png
    └── index.md

1 directory, 2 files

Готово! Тепер відкриємо сторінку у браузері, використавши назву каталогу в URL: http://localhost:8080/how-to-create-a-blog-on-gridsome.

Стаття в браузері

Краса! Але не вистачає підсвітки синтаксису. Для неї майже все готово, треба тільки приєднати стилі вибраної теми. Мені сподобалась тема material-oceanic, її й застосуємо.

  • Додаємо до проекту пакунок з темами для Prism: yarn add prism-themes;
  • А потім відкриймо src/main.js, і додаймо ось такий імпорт:

    //...
    import 'prism-themes/themes/prism-material-oceanic.css'

Тепер стаття готова. Стаття, але вже з вірною підсвіткою коду

Оформимо головну

Окремі статті - це вже непогано, але для повного щастя нам потрібний повноцінний список дописів для навігації. Досі головна сторінка у нас показувала стоковий "Hello World", пора дати йому ради. Відкриємо src/pages/Index.vue, і запишемо туди такий вміст:

<template>
  <Layout>
    <h1>Дописи</h1>

    <div v-for="edge in $page.posts.edges" :key="edge.node.id">
      <g-link :to="edge.node.path" class="post-preview__title-link">
        <h2 class="post-preview__title" v-html="edge.node.title"></h2>
      </g-link>
      <small class="post-preview__date-mark">
        {{ edge.node.date }} &mdash; {{ edge.node.timeToRead }} хвилин чтива
      </small>
      <p class="post-preview__description" v-html="edge.node.description" />
      <br /><br />
    </div>
  </Layout>
</template>

<page-query>
query {
  posts: allMarkdownPost() {
    edges {
      node {
        id
        title
        date
        timeToRead
        description
        path
      }
    }
  }
}
</page-query>

// ...

Тут ми одразу робимо багато всякого:

  1. Всередині page-query ми запитуємо всі статті, скільки їх у нас є всередині каталога content/.
  2. Всередині template, одразу під заголовком, ми їх виводимо одна за одною. Звісно, ми показуємо не весь вміст, нас здебільшого цікавить лише заголовок, дата, і невеличкий опис. Фактично сюди потрапляє все те, що ми раніше закодували в заголовку маркдаун-файлу, у front-matter.
  3. Уважний погляд читача міг звернути увагу на поле timeToRead, якого насправді немає в оригінальному файлі статті. Це вбудований функціонал парсера gridsome, який додатково до полів front-matter додає іще декілька своїх. Зокрема timeToRead обраховується від числа слів у статті

Отже, перейдімо назад на головну http://localhost:8080/, і ми побачимо щось таке:

Головна сторінка

Власне, все. Основний функціонал блогу готовий. Пара штрихів - і це можна буде випускати в люди.

Декілька деталей

Правильний URL

Припустімо, я планую розміщувати на одному домені декілька різних проектів, а не лише блог. Щоб потім не думати, чи треба мені налаштовувати переспрямування, чи почати індексацію з нуля, краще одразу організуємо навігацію як треба. Нехай всі статті лежать за адресою /blog/.... Для цього додамо отакий параметр у gridsome.config.js:

// ...
templates: {
  MarkdownPost: '/blog/:path', // знову ж таки, MarkdownPost має співпадати з назвою нашої graphQL-колекції
},
// нижче - plugins

Перезавантажуємо - і все. Тепер наша стаття доступна за адресою http://localhost:8080/blog/how-to-create-a-blog-on-gridsome.

Форматування дати

Дата в ISO-форматі

Як ми пам'ятаємо, дата у нас виглядає не дуже. Не всі люди знайомі з форматами ISO 8601, тож давайте подивимося, як ми можемо спростити цей запис. Gridsome одразу пропонує робити форматування всередині GraphQL, для цього треба лише додати отакий вираз до запиту:

<page-query>
query MarkdownPost ($id: ID!) {
  post: markdownPost (id: $id) {
    title
    path
    date (format: "MMMM D, YYYY") // оцей
    description
    content
  }
}
</page-query>

Дата, що придатна для читання

Вже краще. Час нам не потрібний, оскільки в метаданих самої статті ми його не вказували.

Що мене в цьому не влаштовує - це локалізація. Точніше, її відсутність. Gridsome не дає можливості контролювати локалізацію в такому форматі запису (і було б дуже дивно, якби він таку можливість давав), тож нам потрібно щось інше. На щастя, далеко ходити не треба - у нас уже є Intl API. Тож GrapQL-запит залишимо як був, а натомість допишемо необхідні перетворення просто в інтерполяцію:

// ...текст шаблону
{{
  new Date(edge.node.date).toLocaleDateString('uk-UA', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  })
}}
&mdash;
// ...текст шаблону

Це можна потім винести в якусь загальну функцію для форматування, за потреби.

Локалізований варіант зображення дати

Але результат виглядає цілком довершено. Окрім оцінки тривалості читання...

Оцінимо тривалість читання в папугах печивах

Мені свого часу дуже сподобалося, як це оформлено у блозі Дена Абрамова.

Приклад оцінки часу через емоджі

Дешево й сердито. Що ж... Ні слова більше.

Почнімо з набору емоджі. Я обрав такий. Для спрощення роботи я одразу використав їх еквіваленти в хвилинах як ключі:

const timings = {
  0.5: '🍪',
  3: '☕',
  12: '🫖',
};

В компоненті нам знадобиться два обчислюваних поля. Перше - для власне стрічки з емоджі:

// ...computed properties
emojis() {
  let { time } = this;
  let str = '';

  [12, 3, 0.5].forEach((num) => {
    while (time >= num) {
      time -= num;
      str += timings[num];
    }
  });

  return str;
},
// ...computed properties

Друге - для стрічки з текстом. По-перше - щоб мати можливість в шаблоні вставити цей текст двічі, а по-друге - щоб виправити закінчення в "хвилинах"

// ...computed properties
text() {
  const { time } = this;
  const lastNumeral = time.toString()[time.toString().length - 1];
  let ending = '';
  if (['1'].includes(lastNumeral)) {
    ending = 'а';
  } else if (['2', '3', '4'].includes(lastNumeral)) {
    ending = 'и';
  }

  return `${time} хвилин${ending} чтива`;
},
// ...computed properties

Все, зберемо це в простенький шаблон:

<template>
  <span>
    <span :title="text">{{ emojis }}</span>
    {{ text }}
  </span>
</template>

І маємо прекрасне прев'ю для наших дописів.

Наша оцінка часу з емоджі

Висновки

Vue завжди фокусувався на простоті використання і зручності розробника. І Gridsome повністю слідує цим принципам. Цей досвід повинен особливо сподобатися Vue-розробникам. Що ж до новачків - це іще простіше, ніж починати проект на Vue з нуля, оскільки в цьому випадку весь каркас уже зібраний докупи.

Посилання

  • Весь код, наведений у даній статті, можна побачити у репозиторії на гітхабі
  • Для Gridsome напрацьована чималенька база стартерів, з якими я також рекомендую ознайомитися тут