Создаем движок блога на основе Next.js

В этой статье мы будем использовать Next.js для создания статического блога — с дизайном и структурой, навеянными движком Jekyll. Меня всегда впечатляло то, как Jekyll упрощает процесс создания блога для новичков, но в то же время предоставляет огромные возможности по модификации каждой функции для продвинутых пользователей.

Появление Next.js, вместе с ростом популярности библиотеки React в последние годы, открыло новые перспективы для развития статических блогов. Фреймворк Next.js позволяет без особого труда создавать статические сайты, основанные на файловой системе, которой почти не требуется конфигурация.

Структура каталога среднестатистического блога на движке Jekyll (без каких-либо дополнений) выглядит следующим образом:

├─── _posts/          ...посты блога в разметке Markdown
├─── _layouts/        ...макеты различных страниц
├─── _includes/       ...повторно используемые компоненты
├─── index.md         ...главная страница
└─── config.yml       ...конфигурация блога

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

Для тех, кто не знаком с Jekyll — это генератор сайтов, который преобразует простой текст в статические блоги и веб-страницы. Подробности по настройке и запуску Jekyll — в руководстве пользователя.

Материал, изложенный в этой статье, также предполагает наличие базовых знаний о фреймворке React. Начать изучение React можно с руководства для новичков.

Установка

Фреймворк Next.js создан на платформе Node.js с использованием библиотеки React. Поэтому для обработки next, react и react-dom необходимо установить менеджер пакетов npm:

mkdir nextjs-blog && cd $_
npm init -y
npm install next react react-dom --save

Для запуска скриптов Next.js из командной строки, нам надо добавить команду next в раздел scripts файла package.json:

"scripts": {
  "dev": "next"
}

Теперь мы запустим команду npm run dev и посмотрим, что получится:

$ npm run dev
> nextjs-blog@1.0.0 dev /~user/nextjs-blog
> next

ready - started server on http://localhost:3000
Error: > Couldn't find a `pages` directory. Please create one under the project root

После запуска локального сервера компилятор выдает сообщение об ошибке: «Каталог страниц pages не найден. Создайте каталог в корневой папке проекта». Мы рассмотрим концепцию каталога страниц в следующем разделе.

Концепция страниц

Фреймворк Next.js основан на концепции страниц. Каждая страница представляет собой компонент React типа .js или .jsx, маршрутизация выполняется по имени файла:

Файл                            Маршрут
----                            -----
/pages/about.js                 /about
/pages/projects/work1.js        /projects/work1
/pages/index.js                 /

Создадим каталог страниц pages в корневой папке проекта и заполним первую страницу, index.js, с помощью основного компонента React:

// pages/index.js
export default function Blog() {
  return <div>Welcome to the Next.js blog</div>
}

Запустим сервер командой npm run dev и перейдем по адресу http://localhost:3000 в браузере на первую запись в блоге, которая гласит «Добро пожаловать в Next.js блог»:

Стандартная установка «из коробки» обеспечивает:

  • горячую перезагрузку — не надо обновлять страницу после каждой правки кода;
  • статическую генерацию всех страниц в директории /pages/**;
  • обслуживание статических файлов для активов, размещенных в папке /public/**;
  • страницу ошибки 404.

Добавьте любой несуществующий путь к адресу на localhost, чтобы оценить страницу 404 по умолчанию. Если требуется персонализация 404 страницы, обратитесь к документации по Next.js.

Скриншот стандартной страницы ошибки 404 показывает, что страница не найдена

Динамические страницы

Статические маршруты используются при создании главной страницы сайта, раздела «О компании» и так далее. Однако для динамической генерации всех наших постов мы воспользуемся возможностями динамических маршрутов Next.js. К примеру:

Файл                        Маршрут
----                        -----
/pages/posts/[slug].js      /posts/1
                            /posts/abc
                            /posts/hello-world

В этом случае любой маршрут, например /posts/1, /posts/abc и так далее, сопоставляется с /posts/[slug].js, а слаг отправляется на страницу в качестве параметра запроса. Это необходимо для выведения всех постов блога, поскольку мы не хотим создавать отдельный файл для каждого поста. Вместо этого мы динамически передаем слаг для обработки соответствующего поста.

Структура блога

Мы разобрались с конструкционным принципом Next.js, и теперь готовы перейти к структуре нашего блога.

.
├─ api
│  └─ index.js             # загрузка постов и настроек
├─ _includes
│  ├─ footer.js            # футер сайта
│  └─ header.js            # шапка сайта
├─ _layouts
│  ├─ default.js           # основной макет статических страниц
│  └─ post.js              # макет поста, унаследованный от страницы
├─ pages
│  ├─ index.js             # главная страница сайта
|  └─ posts                # посты на динамической странице
|     └─ [slug].js       # динамическая страница блога
└─ _posts
   ├─ welcome-to-nextjs.md
   └─ style-guide-101.md

API блога

Базовая структура блога требует двух API функций:

  • для возвращения метаданных всех постов в директории _posts;
  • для извлечения отдельного поста для заданного slug с полным HTML кодом и метаданными.

В качестве дополнительной опции, мы также определим все настройки сайта в файле конфигурации config.yml — для доступа всех компонентов. Поэтому нам потребуется функция анализа YAML конфигурации для нативных объектов.

Поскольку мы будем работать с множеством форматов, не имеющих отношения к JavaScript — Markdown (.md), YAML (.yml) и другими, воспользуемся библиотекой raw-loader для загрузки таких файлов в виде строк — для упрощения обработки:

npm install raw-loader —save-dev

Затем укажем Next.js использовать raw-loader при импорте файлов форматов .md и .yml путем создания файла next.config.js в корневой папке проекта (подробнее об этой процедуре — здесь):

module.exports = {
  target: 'serverless',
  webpack: function (config) {
    config.module.rules.push({test:  /\.md$/, use: 'raw-loader'})
    config.module.rules.push({test: /\.yml$/, use: 'raw-loader'})
    return config
  }
}

В Next.js версии 9.4 появились псевдонимы для импорта по относительным путям — это позволяет избежать чрезмерного удлинения объявлений оператора. Для использования псевдонимов, создайте файл jsconfig.json в корне проекта, определяя основной путь и псевдонимы всех модулей проекта:

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@includes/*": ["_includes/*"],
      "@layouts/*": ["_layouts/*"],
      "@posts/*": ["_posts/*"],
      "@api": ["api/index"],
    }
  }
}

Это позволит нам, к примеру, импортировать макеты простой строкой:

import DefaultLayout from '@layouts/default'

Получение всех постов блога

Эта функция читает все Markdown файлы в директории _posts, анализирует основной материал постов, определенный функцией gray-matter, и возвращает массив метаданных всех записей блога:

// api/index.js
import matter from 'gray-matter'


export async function getAllPosts() {
  const context = require.context('../_posts', false, /\.md$/)
  const posts = []
  for(const key of context.keys()){
    const post = key.slice(2);
    const content = await import(`../_posts/${post}`);
    const meta = matter(content.default)
    posts.push({
      slug: post.replace('.md',''),
      title: meta.data.title
    })
  }
  return posts;
}

Среднестатистический пост в формате Markdown выглядит так:

---
title:  "Welcome to Next.js blog!"
---
**Hello world**, this is my first Next.js blog post and it is written in Markdown.
I hope you like it!

Фрагмент, отделенный от остального текста линиями —, называется основным материалом поста, и содержит все метаданные записи: заголовок, постоянную ссылку, теги и так далее. Результат обработки выглядит так:

[
  { slug: 'style-guide-101', title: 'Style Guide 101' },
  { slug: 'welcome-to-nextjs', title: 'Welcome to Next.js blog!' }
]

Перед использованием gray-matter убедитесь, что вы установили соответствующую библиотеку из пакета npm:

npm install gray-matter –save-dev.

Получение отдельного поста

Функция найдет файл в каталоге _post по заданному слагу, проанализирует Markdown с помощью библиотеки marked, вернет выходной HTML и метаданные:

// api/index.js
import matter from 'gray-matter'
import marked from 'marked'


export async function getPostBySlug(slug) {
  const fileContent = await import(`../_posts/${slug}.md`)
  const meta = matter(fileContent.default)
  const content = marked(meta.content)    
  return {
    title: meta.data.title, 
    content: content
  }
}

В результате получим:

{
  title: 'Style Guide 101',
  content: '<p>Incididunt cupidatat eiusmod ...</p>'
}

Перед использованием функции не забудьте установить библиотеку marked из пакета npm с помощью команды:

npm install marked —save-dev

Конфигурация

Чтобы использовать для нашего блога, созданного на основе Next.js, настройки фреймворка Jekyll, мы проанализируем YAML файл с помощью библиотеки js-yaml, и экспортируем конфигурацию для использования всеми компонентами:

// config.yml
title: "Next.js blog"
description: "This blog is powered by Next.js"


// api/index.js
import yaml from 'js-yaml'
export async function getConfig() {
  const config = await import(`../config.yml`)
  return yaml.safeLoad(config.default)
}

Для работы функции необходима библиотека js-yaml из пакета npm, которую устанавливают следующей командой:

npm install js-yaml --save-dev

Включения

Директория _includes содержит два основных компонента React — <Header> и <Footer>. Они будут использоваться в различных компонентах макета, определенных в каталоге _layouts:

// _includes/header.js
export default function Header() {
  return <header><p>Blog | Powered by Next.js</p></header>
}

// _includes/footer.js
export default function Footer() {
  return <footer><p>©2020 | Footer</p></footer>
}

Макеты

В нашем каталоге _layouts находятся 2 компонента макета. Базовым макетом является <DefaultLayout> — на его основе будут создаваться все остальные компоненты:

// _layouts/default.js
import Head from 'next/head'
import Header from '@includes/header'
import Footer from '@includes/footer'


export default function DefaultLayout(props) {
  return (
    <main>
      <Head>
        <title>{props.title}</title>
        <meta name='description' content={props.description}/>
      </Head>
      <Header/>
      {props.children}
      <Footer/>
    </main>
  )
}

Второй макет — <PostLayout> — он переопределяет заголовок, заданный в заголовке поста <DefaultLayout>, и отображает HTML-код записи. Макет также содержит ссылку на главную страницу блога:

// _layouts/post.js
import DefaultLayout from '@layouts/default'
import Head from 'next/head'
import Link from 'next/link'


export default function PostLayout(props) {
  return (
    <DefaultLayout>
      <Head>
        <title>{props.title}</title>
      </Head>
      <article>
        <h1>{props.title}</h1>
        <div dangerouslySetInnerHTML={{__html:props.content}}/>
        <div><Link href='/'><a>Home</a></Link></div> 
      </article>
    </DefaultLayout>
  )
}

Встроенный компонент next/head добавляет элементы в тег <head> страницы. Другой встроенный компонент next/link обрабатывает на стороне клиента переходы между маршрутами, которые определены в каталоге страниц.

Главная страница

Для заполнения части главной страницы мы выведем список всех постов блога из директории _posts. Список будет содержать название поста и постоянную ссылку на страницу записи. Страница будет использовать макет по умолчанию <DefaultLayout>. Для добавления в макет title и description мы импортируем конфигурацию в главную страницу:

// pages/index.js
import DefaultLayout from '@layouts/default'
import Link from 'next/link'
import { getConfig, getAllPosts } from '@api'


export default function Blog(props) {
  return (
    <DefaultLayout title={props.title} description={props.description}>
      <p>List of posts:</p>
      <ul>
        {props.posts.map(function(post, idx) {
          return (
            <li key={idx}>
              <Link href={'/posts/'+post.slug}>
                <a>{post.title}</a>
              </Link>
            </li>
          )
        })}
      </ul>
    </DefaultLayout>
  )
} 


export async function getStaticProps() {
  const config = await getConfig()
  const allPosts = await getAllPosts()
  return {
    props: {
      posts: allPosts,
      title: config.title,
      description: config.description
    }
  }
}

Функция getStaticProps вызывается во время сборки и предварительного рендеринга страниц с помощью передачи компоненту props страницы по умолчанию. Функция формирует список-архив всех постов на главной странице:

Страница поста

Эта страница будет отображать заголовок и содержание поста для слага, входящего в состав параметра context. Страница поста использует компонент <PostLayout>:

// pages/posts/[slug].js
import PostLayout from '@layouts/post'
import { getPostBySlug, getAllPosts } from "@api"


export default function Post(props) {
  return <PostLayout title={props.title} content={props.content}/>
}


export async function getStaticProps(context) {
  return {
    props: await getPostBySlug(context.params.slug)
  }
}


export async function getStaticPaths() {
  let paths = await getAllPosts()
  paths = paths.map(post => ({
    params: { slug:post.slug }
  }));
  return {
    paths: paths,
    fallback: false
  }
}

Если страница использует динамический маршрут, все возможные пути должны быть предоставлены Next.js во время сборки. Функция getStaticPaths передает список всех маршрутов, которые должны быть преобразованы в HTML во время сборки. Свойство fallback обеспечивает показ страницы 404, если пользователь перейдет по пути, не входящему в список маршрутов.

Производственная подготовка

Чтобы собрать статический блог и запустить сервер производства, добавьте приведенные ниже команды для build и start в файл package.json, под разделом scripts; затем последовательно запустите npm run build и npm run start:

// package.json
"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
}

Весь исходный код, приведенный в статье, доступен в репозитории на GitHub. Код можно клонировать и модифицировать по своему усмотрению. В репозитории также есть основные заполнители — для применения CSS к созданному блогу.

Доработки

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

  • пагинации;
  • подсветки синтаксиса;
  • добавления тегов и категорий постов;
  • стилей оформления.

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

Дайте знать, что вы думаете по этой теме в комментариях. За комментарии, лайки, дизлайки, подписки, отклики огромное вам спасибо!

Данная публикация является переводом статьи «Building a Blog with Next.js» , подготовленная редакцией проекта.

Меню
Posting....