目次
はじめに
このブログが完成したので、今回はざっくりと技術的なお話をしていきたいなと思います。
ソースコード
github のリポジトリはこちらということで、中身を見てもらえたらすむ話ではあるのですが、この記事ではなぜこのような技術を選定したのかということや、実装を進めていく中なかで詰まったところ、参考にしたサイトなどを書いていきたいなと思います。
あと、一応このブログは実装だけでなくデザインも自分でやっているのですが、その辺はまた別の記事で書こうかなと思います。(全然アマチュアなので、そんなに参考にはならないと思いますが。。)
技術構成
- フレームワーク: Next.js
- 言語: typescript
- スタイリング: CSS Modules
- 投稿: markdown
- デザインツール: figma
- インフラ: vercel
実装の前に考えたこと
ブログを作るぞ。と決めてから考えていたことは、前提として今ハマっている Next.js を使うということ。
となると、cms は wordpress を使うわけにはいきませんから、日本製の microcms を使うか、使い慣れている contentful を使うかと迷っていたのですが、今後どの cms が主流になっていくのかなどの流行り廃りを考えるのも面倒だし、今回はお客さん相手じゃなくて自分自身が更新するものだから、わざわざ管理画面なんてなくてもいい。
それに、シンプルな技術ブログなら markdown で書くのにちょうどいいのではないかということで更新方法は markdown を採用してみました。(事前に調査したところ、ある程度情報量もあったので)
あと、迷ったのはスタイルについてで、Next.js で css を当てるとすると、CSS Modules か Tailwind CSS が多いようで、これまでに一応どちらの手法にも触れたことがあったのですが、今人気である Tailwind CSS は web サイトを作る分には細かいスタイルの調整が難しかったり、レスポンシブを考えるのも面倒だった経験があったので、今回は使い慣れている CSS Modules を採用することにしました。 CSS Modules であれば、ふだん通りに css を書く感じでできるし、コンポーネントごとに簡単な命名をすればいいので、css 設計的なことにもそこまで頭を抱えずにすみました。
しかしながら、未だに毎回スタイルは何を選定しようかなと悩む時間があるので、そろそろ Next.js でのスタイルのベストプラクティスが決定してほしいものですね。(今のところ Tailwind CSS になりそうな雰囲気)
実装の手順
1.環境構築
まず、Next.js の最新版を typescript でインストールして環境構築を行います。
実装時の Next.js のバージョン 12 でした。
npx create-next-app@latest --typescript
Next.js での環境構築の詳しい方法は公式のドキュメントに書いてあるので読んでみてください。
2.マークアップ
まず、インストール時に構成されているディレクトリ構成から特に変更することなく、ページは pages ディレクトリに作成。
about など、更新が不要なページは何も考えずデザイン通りにマークアップをするだけですが、トップページなどの投稿を持っているページではssgで投稿情報の一覧の取得をして表示します。
今回のような web サイトではビルド時にデータが取得できているので、ssr ではなく ssg を用いるのが良いでしょう。
3.マークダウンでの投稿
こ結論から言うと、ほとんどアールエフェクト さんのブログ記事を参考にさせていただきました。
私が多少工夫したところがあるとすると、typescript に対応したところくらいでしょうか。
この記事に限らずですが、アールエフェクトさんのブログは毎回詳細に書かれていて、痒いところに手が届く的な丁寧な記事で、よく助けてもらっています。
まあ、そのブログを見ていただければ済む話ではあるのですが、以下のコードで簡単に流れを説明してみます。
まずは投稿一覧ページから
・github
import fs from 'fs'
import matter from 'gray-matter'
export const getStaticProps: GetStaticProps = async () => {
// 私の場合postsディレクトリ直下にmarkdownファイルを置いているのでreaddirSyncでファイルを取得
const files = fs.readdirSync('posts')
// ここで投稿の表示に必要な情報を作成
let posts = files.map((fileName) => {
// markdownファイルから.mdの拡張子を取り除いてurlを作成
const slug = fileName.replace(/\.md$/, '')
// readFileSyncでpostsディレクトリ下のファイルを指定してファイル内部の情報を取得
const fileContent = fs.readFileSync(`posts/${fileName}`, 'utf-8')
// matterでmarkdownファイル内に設定しておいたtitleやdescriptionといった情報を取得
const { data } = matter(fileContent)
return {
frontMatter: data,
slug,
}
})
// modifiedDateもしくはpublishedDateが新しい順に並び替え
posts = posts.sort((a, b) =>
new Date(
a.frontMatter.modifiedDate
? a.frontMatter.modifiedDate
: a.frontMatter.publishedDate
) >
new Date(
b.frontMatter.modifiedDate
? b.frontMatter.modifiedDate
: b.frontMatter.publishedDate
)
? -1
: 1
)
// publicというフラグで公開・下書きの状態を管理して、公開のもだけに絞る
posts = posts.filter((post) => post.frontMatter.public)
return {
props: {
posts,
},
}
}
あとは、return した内容をページに props として渡しておけば、無事に投稿一覧が表示できるはずです。
投稿の一覧表示はこんなものでしょう。
それでは投稿の詳細ページに移ります。
・github
import fs from 'fs'
import matter from 'gray-matter'
import { GetStaticPaths, GetStaticProps, NextPage } from 'next'
import { ParsedUrlQuery } from 'querystring'
import { unified } from 'unified'
import remarkToc from 'remark-toc'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeStringify from 'rehype-stringify'
import rehypeSlug from 'rehype-slug'
import remarkPrism from 'remark-prism'
import { PostFrontMatter } from 'type'
type ContextProps = {
frontMatter: { [key: string]: PostFrontMatter }
content: string
}
interface Params extends ParsedUrlQuery {
slug: string
}
export const getStaticProps: GetStaticProps<ContextProps, Params> = async (
params
) => {
const context = params.params!
// postsディレクトリ下のmarkdownファイルの内容を取得
const file = fs.readFileSync(`posts/${context.slug}.md`, 'utf-8')
// matterでmarkdownファイル内に設定しておいた情報(data: titleやdescriptionなどの情報, content: 投稿内容)
const { data, content } = matter(file)
// unifiedでmarkdownファイルをhtmlとして出力してもらう
const result = await unified()
.use(remarkParse)
.use(remarkPrism, {
plugins: ['line-numbers'],
})
// 目次の設定したのでここでパラメーターを設定
.use(remarkToc, {
heading: '目次',
tight: true,
ordered: true,
})
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStringify, { allowDangerousHtml: true })
.use(rehypeSlug)
.process(content)
const allContent = result.value
return {
props: { frontMatter: data, content, allContent },
}
}
export const getStaticPaths: GetStaticPaths = async () => {
const files = fs.readdirSync('posts')
// 投稿詳細ページのurlを生成
const paths = files.map((fileName) => ({
params: {
slug: fileName.replace(/\.md/, ''),
},
}))
return {
paths,
fallback: 'blocking',
}
}
export default Posts
細かく解説していったらキリがなさそうだったので、ざっくりとさせていただきました。
github でソースコードを公開しておきましたので、詳しくはそちらを参考にしてみてください。
このままダラダラと書き連ねていってもよかったのかもしれませんが、あまりに長くなっていってもしょうがないので、この記事では投稿一覧と詳細のみとさせてください。
詳しいことは、これからちょこまかと記事にまとめていこうと思います。
実装を終えて
投稿部分の実装に関しては markdown ファイルを読んできてゴニョゴニョしていくだけだったので、そこまで難しいことはなかった気がします。
さいごに
はじめてガッツリと技術記事のようなものを書いてみようとしましたが、やはり難しいものですね。
これまで当たり前のように読んできた技術記事も、これからはきっと感謝して読むようになると思います。
こんなつたない文章を最後まで読んでいただきありがとうございました。
これからも Next.js などのフロントエンド関連の記事を中心に書いていこうと思いますので、よろしくお願いします。