profile image

面倒くさがりがNext.jsでブログを構築してみた

Next.jsReact

自分の技術力向上と学んだことのアウトプットの場としてブログをはじめてみた。
自分は面倒くさがりすぎるので、ここ2〜3年くらい、半年に1回くらいの頻度でブログを構築したいと思っては完成できなかったり、記事が書けなかったりで挫折してきた。
同じ轍を踏まないためにある程度記事書けるような環境が構築できたら、記事を書きながらカスタムして完成を目指していくという方針にした。

要件

挫折を繰り返さないために、記事を書くコストは限りなく小さくしたい。
具体的には下記を満たしたい。

  • Markdownで書きたい
  • ある程度自分でブログをカスタマイズできるようにReactで書ける
  • 画像の挿入もショートカットコマンド一発でやりたい
  • gitで管理してmainブランチにpushされたら本番に反映
  • 記事を作るときにyarnコマンドでテンプレートを作成できる

Next.jsを使うと大体満たせそうだったので、Next.jsで構築することにしました。

構築

Next.jsでブログ構築する際には、公式のexamplesにblog-starter-typescriptがあるのでそのまま使いました。
tailwindcssで構築されていたのもありがたく、カスタマイズしやすいです。
https://github.com/vercel/next.js/tree/canary/examples/blog-starter-typescripthttps://github.com/vercel/next.js/tree/canary/examples/blog-starter-typescript

構築時のバージョンは下記の通りです。

Library Version
next 12.1.1
zenn-markdown-html 0.1.108
zenn-embed-elements 0.1.108
zenn-content-css 0.1.108

記事ページのスタイリング

記事のスタイリングは整えるのが正直すごい面倒で、過去に完璧を目指して挫折したことがある鬼門。
いつも仕事でお世話になっているzennさんが公開しているmarkdownパーサーライブラリをそのまま使わせていただきました。
リンクをブログカード的な表示にしてくれたりめちゃめちゃ嬉しい...
(ただ、リンク先のOGP情報を取得するエンドポイントはzennさんが用意されているものにタダ乗りしてしまっていて、自分の管理下にないのが少し気になる)
適用はこちらの記事をそのまま行いました。

https://zenn.dev/waddy/articles/intro-zenn-markdownhttps://zenn.dev/waddy/articles/intro-zenn-markdown

front-matter項目修正

Next.jsの blog-starter-typescript で構築した場合、記事のデフォルトのfront-matterには個人ブログでは不要なものや設定が面倒な項目があったので自分が書きやすいようにカスタマイズし、最終的には下記のようになりました。

---
title: "面倒くさがりがNext.jsでブログを構築してみた"
coverImage: ''
date: '2022-04-02T07:04:11.054Z'
tags:
  - 'Next.js'
  - 'React'
---

一覧画面に表示される記事の説明文章を入れる項目であるexcerptは、本文のinnerTextを先頭500文字分切り出すようにしました。


タイトル下の文字がexcerpt

記事本文のmarkdownからHTMLへの変換処理は、デフォルトでは記事のコンポーネント内で処理していましたが、記事側でmarkdownのまま文字列を操作したくなることは今後ないと思われるため、lib/api.ts内に移動させました。そして変換されたHTML文字列をnode-html-parserでparseし、本文のinnerTextを先頭から500文字切り出したものをexcerptとして表示するようにしています。

記事への画像の挿入

ショートカットコマンドを使った画像の挿入はvscodeのPasteImageプラグインを使えば簡単に実現できました。
blog-starter-typescript での記事の保存場所は public/assets/blog/{記事slug}/{画像名.jpg}で保存されているので、option + command + V で画像をそちらに保存するように下記のように設定しました。

.vscode/settings.json
{
  "pasteImage.path": "${projectRoot}/public/assets/blog/${currentFileNameWithoutExt}",
  "pasteImage.basePath": "${projectRoot}/public",
  "pasteImage.forceUnixStyleSeparator": true,
  "pasteImage.prefix": "/"
}

新規記事追加のコマンド処理実装

yarn new:post {slug} で記事のファイルを作成したいので、実装してみた。

新規記事作成コマンドの内容の処理を commands/new-post.js に実装した。

commands/new-post.js
const fs = require('fs-extra')
const arg = require('arg')
const path = require('path')

function getWorkingPath(pathFromWorkingDir) {
  if (/\.\./.test(pathFromWorkingDir)) {
    console.error(
      '取得するファイル/ディレクトリの名前に不正な文字列が含まれているため処理を終了します'
    );
    process.exit(1);
  }
  return path.join(process.cwd(), pathFromWorkingDir.replace(/^\//, ''));
}

function generateFileIfNotExist(fullpath, content) {
  fs.outputFileSync(
    fullpath,
    content,
    { flag: 'wx' }
  );
}

const args = arg(
  {},
  {
    permissive: true,
  }
);

const slug = args._[0]
const fileName = `${slug}.md`
const relativeFilePath = `_posts/${fileName}`
const fullFilepath = getWorkingPath(relativeFilePath)
const today = new Date()

const fileBody =
  [
    '---',
    `title: ""`,
    `coverImage: ''`,
    `date: '${today.toISOString()}'`,
    `ogImage: ''`,
    'tags:',
    `  - 'example'`,
    '---',
  ].join('\n') + '\n'

try {
  generateFileIfNotExist(fullFilepath, fileBody);
  console.log(relativeFilePath);
} catch (err) {
  console.error('記事のファイル作成時にエラーが発生しました');
  console.error(err);
}

実装した処理を yarn 側で呼び出せるように scripts に追加した。

package.json
{
  "private": true,
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start",
    "typecheck": "tsc",
+    "new:post": "node commands/new-post.js"
  },
  "dependencies": {
  ...
  },
  ...
}

今後欲しい機能

現在は記事数が少ないので問題ないのですが、将来的には下記のような記事を検索する機能を作るとより便利になりそう。

  • ページネーション機能
  • タグ検索機能
  • 全文検索機能