How start own blog

Серия статей, в которой я объясню как запустить свой блог, если у вас уже есть локально хранящиеся файлы в md формате

Подразумевается, что у вас уже есть:

  • файлики со статьями в .md формате, пишите вы их в Obsidian или где-то еще - неважно, но я буду использовать плагин, специфичный для obsidian (возможно он вам не понадобится)
  • желание создать блог
  • минимальные знания js и react в частности. Минимальных знаний должно хватить, чтобы подстроить шаблон блога под себя.
  • установлены последняя версия nodejs и npm (на момент написания статьи, требуется node v18) и текстовый редактор (я предпочитаю VS Code)

Для тех, кому лень все это читать и вникать, а хочется просто взять и чтобы все само заработало, вот ссылка на мой репозиторий, вам нужно будет только прилинковать свой Obsidian vault (в ридми описано как, и в этой статье кстати тоже), ну и заменить ваши контактные данные и фото профиля. Если не лень, то вам надо читать дальше)

Введение

Я начал писать заметки в Obsidian для себя, так как считаю его для себя одним из лучших решений (не то что бы у меня богатый опыт). Он позволяет писать в cтатьи и заметки в markdown синтаксисе и хранит их в неизменном виде. Поэтому я быстро решил (спустя 2 десятка заметок), что пора сделать их (заметки) достоянием общественности, превратив их в блог. А сам процесс создания я опишу в этой статье, наверняка кто-то еще захочет провернуть то же самое 😁

В качестве платформы для блога я сначала решил использовать eleventy, но спустя 10 минут у меня не получилось нормально скопировать изображения и я решил использовать решение, которое я использовал раньше для создания сайта-визитки для своего знакомого - Gatsby

Формат статей

  1. В статьях обязательно наличие поля date в формате frontmatter

Frontmatter - это способ идентификации метаданных в файлах Markdown. Метаданные могут быть буквально тем, что вы хотите, но часто они используются для элементов данных, необходимых вашей странице, и вы не хотите отображать их напрямую.

Эти данные должны располагаться в самом начале .md файлов, дата позволит сортировать блоги по дате

---
date: MONTH/DAY/YEAR
---
  1. Ссылки на изображения должны быть в формате
[image alt]('./images/name-of-image.(jpeg|gif|png)')

☝️ Это важно, потому что у меня так и не получилось заставить работать формат

![[some-image.(jpeg|gif|png)]]
  1. Ссылки на соседние статьи в формате, который используется в Obsidian будут работать корректно
[[Some post title]]

Начало. Создаем директорию и накатываем GatsbyJs

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

Проверяем, что все работает: запускаем в терминале npm run develop - после того, как в терминале появится ссылка http://localhost:8080 - тыкаем и переходим по ней или просто открываем в браузере. Если все ок, то мы должны увидеть надпись hello world

Поздравляю, ваш блог готов! 😁 (шутка, дальше мы заставим его отображать наши посты из obsidian)

Создаем символьную ссылку на ваши статьи

Предположим, что ваши статьи находятся по такому пути: /Users/yourUsername/Documents/knowledgebase/

Теперь создадим символьную ссылку таким образом:

ln -s /Users/yourUsername/Documents/knowledgebase/ ./content

Открываем в вашем редакторе проект с блогом и видим, что в директории ./content появилаcь директория knowledgebase в которой хранятся статьи

Устанавливаем необходимые плагины

npm i gatsby-source-filesystem gatsby-transformer-remark gatsby-remark-obsidian

Редактируем конфигурацию блога

Открываем файл gatsby-config.js, вот такое содержимое получилось у меня:

/**
 * Configure your Gatsby site with this file.
 *
 * See: https://www.gatsbyjs.com/docs/reference/config-files/gatsby-config/
 */

/**
 * @type {import('gatsby').GatsbyConfig}
 */
module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        path: `${__dirname}/content/knowledgebase`,
        name: `blog`,
        ignore: [`**/\.obsidian`]
      },
    },
    {
        resolve: "gatsby-transformer-remark",
        options: {
            plugins: [
                {
                    resolve: 'gatsby-remark-obsidian',
                    options: {
                        titleToURL: (title) => `/${title}`, // optional
                        markdownFolder: `${__dirname}/content/knowledgebase`, // optional
                        highlightClassName: 'highlight', // optional
                    },
                },
            ]
        }
    },
  ],
}```

### Проверяем, как выглядят данные в /graphql

Запускаем

```sh
npm run develop

После того как все соберется (если все прошло гладко) вы увидите в терминале ссылки на зачаток блога и ссылку на Браузерный graphql playground, где можно накликать запрос и посмотреть в каком виде будут доступны данные:

You can now view gatsby-starter-hello-world in the browser.
⠀
  http://localhost:8000/
⠀
View GraphiQL, an in-browser IDE, to explore your site's data and schema
⠀
  http://localhost:8000/___graphql
⠀

Переходим по этой ссылке http://localhost:8000/___graphql и видим примерно следующее graphql playground

Открываем GraphiQL Explorer и там можно выбрать из любых данных, в зависимости от того, какие плагины вы наподключали, нас же интересуют наши посты в markdown и как они трансформировались в html.

Кликаем на allMarkdownRemark и там необходимо накликать необходимые поля, из тех, что точно понадобятся excerpt (чтобы показывать превью текста) и html чтобы рендерить полный текст статьи, вот какой запрос получился у меня:

query MyQuery {
  allMarkdownRemark {
    edges {
      node {
        excerpt(format: HTML)
        html
        frontmatter {
          date
          title
        }
        id
      }
    }
  }
}

И вот как все выгдядит, когда я нажимаю на кнопочку play (которая красная) graphql posts query

Круто, мы видим, что наш markdown трансформировался в html - а значит пора сделать так, чтобы посты отображались в браузере, вместо надписи hello world

Но перед этим я добавлю немного мета информации для блога в gatsby-config.js

module.exports = {
	siteMetadata: {
	    social: {
	      twitter: 'https://twitter.com/the_strange_dev',
	      siteUrl: 'https://thestrangeadventurer.com',
	      twitterUsername: '@the_strange_dev',
	      profileImage: '/profile.jpg'
	    },
	    title: 'Блог беспечного авантюриста',
	    description: 'Яндексойд, путешественник, авантюрист, беспечный и непредсказуемый. Люблю путешествовать, писать и делиться своими историями. Все мои путешествия и истории о них вы можете найти здесь (шутка, тут только про код).',
	},
	// ...остальной код

Свою аватарку я положил в директорию /static и назвал profile.jpg, все эти метаданные тоже можно достать с помощью graqhql запроса metadata query 🤬 А че с ними делать, с этими запросами? Об этому чуть позже, все по порядку)

Создаем странички и рендерим наши посты

Дальше задача такая:

  • создать на главной список всех постов с постраничной навигацией
  • создать все страницы с постами

В Gatsby можно создавать страницы двумя способами:

Так как у меня раньше был небольшой опыт с Gatsby я решил не переизобретать велосипед и сделать все уже знакомым мне способои и все делал по документации .

Ниже приведу пример файлов, которые нужно создать и опишу код, который они содержат:

gatsby-node.js в корне проекта

Во-первых, нам понадобится пакет slugify, чтобы конвертировать названия на русском в человекопонятные слаги для построения ссылок, так ссылки выглядят более аккуратно и у меня есть ничем неподкрепленное убеждение, что это положительно влияет на SEO 😁

Устанавливаем пакет: npm install slugify

Дальше привожу пример кода с максимально подробными комментариями

const path = require("path")
const { createFilePath } = require("gatsby-source-filesystem")

/*
 * npm install slugify - пакет нужен чтобы превращать заголовки в слаги на латинице
 * Например "Привет мир" => "privet-mir"
 */ 
const slugify = require("slugify");


/**
 * Создаем поля slug и title для каждого поста
 * slug - это путь к посту, например /privet-mir
 * title - это заголовок поста, например "Привет мир"
 * Функция вызывается при создании каждого поста
 */
exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions
  if (node.internal.type === `MarkdownRemark`) {
    const value = createFilePath({ node, getNode });

    createNodeField({
      name: `slug`,
      node,
      value: slugify(value, { lower: true }),
    })
    
    createNodeField({
      name: `title`,
      node,
      value: value,
    })
  }
}

/**
 * Создаем все необходимые страницы на основе файлов
 * которые лежат в папке src/templates
 */
exports.createPages = async function ({ actions, graphql }) {
  /**
   * Получаем слаги, которые необходимы
   * чтобы построить ссылки на все посты
   * и понять сколько будет всего страниц
   */
  const { data } = await graphql(`
    query {
      allMarkdownRemark {
        edges {
          node {
            fields {
              slug
            }
          }
        }
      }
    }
  `)

  const posts = data.allMarkdownRemark.edges
  const POSTS_PER_PAGE = 5 // Определяем сколько постов будет на одной странице
  const numPages = Math.ceil(posts.length / POSTS_PER_PAGE) // Считаем сколько всего страниц

  /**
   * Создаем страницу для каждого поста
   * в качестве компонента, который будет отвечать за рендеринг
   * указываем src/templates/BlogPost.js
   */
  data.allMarkdownRemark.edges.forEach((edge) => {
    const { slug, title } = edge.node.fields

    actions.createPage({
      path: slug,
      component: path.resolve(process.cwd(), `src/templates/BlogPost.js`),
      context: { slug, title },
    })
  })

  /**
   * Создаем страницы для пагинации
   * если страница 1, то путь будет /
   * если страница 2, то путь будет /page/2 и т.д.
   * в качестве компонента, который будет отвечать за рендеринг
   * указываем src/templates/BlogList.js
   */
  Array.from({ length: numPages }).forEach((_, i) => {
    actions.createPage({
      path: i === 0 ? `/` : `/page/${i + 1}`,
      component: path.resolve(process.cwd(), `src/templates/BlogList.js`),
      /**
       * Передаем в контекст данные для построения пагинации
       */
      context: {
        limit: POSTS_PER_PAGE,
        skip: i * POSTS_PER_PAGE,
        numPages,
        currentPage: i + 1,
      },
    })
  })

}

И так, мы описали, какие страницы мы создаем, а теперь нужно создать директорию /src/templates и положить туда два файлика:

  • Bloglist.js
  • BlogPost.js

Там будет код компонентов и graphql запросы для получения данных, код и комментариии ниже:

src/templates/BlogList.js

// src/templates/BlogList.js
import React from "react"
import { graphql, Link } from "gatsby"

/**
 * Запрос для получения списка постов, отсортированных по дате
 * 
 * Так как Gatsby это фреймворк - он предоставляет нам определенный API
 * Если мы экспортируем из файла graphql запрос - то Gatsby автоматически
 * будет его выполнять во время сборки и передавать результат
 * в компонент в props.data
 */
export const blogListQuery = graphql`
  query BlogListQuery($skip: Int!, $limit: Int!) {
    allMarkdownRemark(
      sort: { frontmatter: { date: DESC }}
      limit: $limit
      skip: $skip
    ) {
      edges {
        node {
          fields {
            slug
            title
          }
          frontmatter {
            title
            date
          }
        }
      }
    }
  }
`

/**
 * Рендерим список постов со ссылками на них
 */
export default function BlogList(props) {
    const { allMarkdownRemark } = props.data;
    const { edges } = allMarkdownRemark;

    return (
        <div>
            <h1>Blog List</h1>
            {edges.map(({ node }) => {
                const { slug, title } = node.fields;
                const { date } = node.frontmatter;
                return (
                    <div key={slug}>
                        <h2>
                            <Link to={`/${slug}/`}>{title}</Link>
                        </h2>
                        <p>{date}</p>
                    </div>
                )
            })}
        </div>
    )
}

src/templates/BlogPost.js

import React from 'react';
import { graphql } from 'gatsby';

/**
 * Запрос для получения одного поста по его slug
 * slug - это часть URL, которая идет после домена
 * slug передается в компонент через контекст, который мы задали в 
 * gatsby-node.js (module.exports = { createPages })
 */
export const query = graphql`
    query BlogPageQuery($slug: String!) {
        markdownRemark(fields: {slug: {eq: $slug}}) {
            html
        }
        site {
            siteMetadata {
                description
                social {
                    siteUrl
                    twitter
                    twitterUsername
                }
                title
            }
        }
    }
`

/**
 * Рендерим пост, используя данные gql запроса
 */
export default function BlogPage(props) {
    const { markdownRemark } = props.data;
    const { html } = markdownRemark;
    return <div dangerouslySetInnerHTML={{ __html: html }}></div>
}

src/pages/404.js

Ну и добавим сразу страничку для тех случаев, когда пользователь переходит на неизвестную для нас страницу - это будет статичная страница, просто кладем файлик src/pages/404.js с таким содержимым (пока таким)

import React from "react"

export default function NotFoundPage() {
    return (
        <div>
            <h1>404</h1>
            <p>Page not found</p>
        </div>
    )
}

На данный момент должна получиться следующая структура проекта: ![структура проекта](images/project.png project)

Проверяем отображение страниц

Запускаем npm run develop и если все хорошо, то мы должны увидеть на главной список из 5 постов, а если перейдем на /page/2/ то там должен быть список из другиз 5-и постов, а каждая ссылка должна открывать отдельный пост.

render BlogList

render BlogPost page

Вместо заключения

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

Блог на Gatsby + Obsidian from scratch ч.2 Блог на Gatsby + Obsidian from scratch ч.3