Next.jsの習作として自分のブログを作成するログ
Next.jsの習作として自分のブログを作成する。行った作業をなるべく細かく書き記していきたい(希望的観測)。
デプロイ先は vercel か GitHub Pages (マークダウンで書いて静的配信するだけなので可能のはず) のどちらかにする。
サーバー費用はかけたくないので vercel の無料枠が何をどこまでできるか要調査。GitHub Pages は容量制限があるので画像多めだとすぐ超えそう。
テンプレートからプロジェクト作成
プロジェクトは create-next-app
で作成。テンプレートは公式チュートリアルのTypeScriptが終わった状態のものを拝借。
npx create-next-app stin-blog --use-npm --example "https://github.com/vercel/next-learn-starter/tree/master/typescript-final"
チュートリアルの TypeScript 化が終わったあとの状態をテンプレートにしているのでこれでブログ完成。(終わり)(嘘)
ライブラリのバージョンを最新にしたい
package.json
が TypeScript や React などの古いバージョンを参照しているため、アップデートします。
npm i react@latest react-dom@latest date-fns@latest gray-matter@latest next@latest remark@latest remark-html@latest
npm i typescript@latest @types/react@latest -D
テンプレートの package.json
が含むライブラリのすべての最新版を引っ張ってみた。
とりあえずこの状態で動くのか確認。(remark-html
がメジャーバージョンアップしてた)
npm run dev
ビルド成功して全ページが正常に表示されることを確認。
プロジェクトのソースコードトップディレクトリは src
にしたい派なので移動する。
ディレクトリのルートに src
ディレクトリを作成して、components
, lib
, pages
, styles
を src
に移動する。
npm run dev
問題なく動作することを確認。(簡単過ぎる)
prettier を導入
npm install -D prettier
.prettierrc
ファイルを追加(秘伝のタレ的設定ファイルをコピペ)。
.prettierignore
ファイルを追加(.gitignore
の内容をコピペ)。
package.json
の scripts
にコマンド追加。
どこまで拡張子使うかわからないからとりあえず React で使えるやつ全部列挙。
"format": "prettier --write '**/*.{js,jsx,ts,tsx,json,css,scss}'"
pre-commit hook で勝手にフォーマットしてくれるようにもする。
npx mrm lint-staged
ここまでで一旦コミットします。
リセットCSSを入れる。
業務で ress.css
を使ってるので引き続きこれ。
src/stlyes/global.css
の上部に ress.css
の内容をコピペする(ライセンスのコメント部分もちゃんと含めよう)。
src/stlyes/global.css
の既存の記述を修正。
html, body {
padding: 0;
margin: 0;
line-height: 1.6;
font-size: 18px;
}
a {
color: #0070f3;
}
ここを削除して
html {
font-size: 62.5%;
}
これを追加。
やっぱり ress.css
だけでひとつのファイルにする。責務は分けていこう。(普通に見にくかった)
src/pages/_app.tsx
で import "../styles/ress.css";
を先頭に追加。
配色考えないといけない。ダークモードも導入したい。
調査したところ、React のステートに応じて css 変数を書き換えることが可能。
つまり Next.js が押してるらしい CSS Modules でスタイリングを書きつつ、テーマ変更機能も実装できる。(ダークモードはメディアクエリではなくReactのステート管理にしたかった)
document.documentElement.style.setProperty('--your-variable', '#YOURCOLOR');
ブログ作りをさぼってスプラトゥーンをしていました(反省)。
配色を決めました。adobe のこちらのツールを使用。
adobe さんUIツールを何個も生み出してるだけあって、配色決めるツールくらい無料で使わせたるわって感じですかね。ありがとうございます。決めた配色は CSS 変数で参照するようにします。テーマの差し替えを実行するために。
こちらの記事を参考にテーマ切り替えを実装します(手前味噌)。この記事のようにテーマ切り替えを実装するとリロードしたときに一瞬画面がちらつく。なぜなら useEffect
は画面が描画されたあとに実行されるため。こういう場合は useLayoutEffect
を使用すべきだった。
……気が向いたらブログのほうは加筆修正しておきます。
useEffect
は useLayoutEffect
に直して使用する。
テーマの初期値を永続化しておかないと画面リロードのたびに戻ってしまうので、ローカルストレージで管理することにした。
type Theme = "light";
const THEME_STORAGE_KEY = "__initial_theme_state";
const getTheme = (): Theme => (localStorage.getItem(THEME_STORAGE_KEY) ?? "light") as Theme;
const saveTheme = (theme: Theme) => localStorage.setItem(THEME_STORAGE_KEY, theme);
const useTheme = () => {
const [theme, setTheme] = React.useState<Theme>(getTheme);
React.useLayoutEffect(() => {
saveTheme(theme);
// 以下略
}
THEME_STORAGE_KEY
はアンダースコア2つから始めてみる。特に意味はないけどなんか内部管理用プロパティみたい(?)
あ、コンポーネントのレンダリングはサーバーサイドで行われるから localStorage
存在しないやん。
もう一個気づいた。 useLayoutEffect
も SSR orSSG では読み込めない…。
ということで useIsomorphicLayoutEffect
を作っておく。
import { useEffect, useLayoutEffect } from "react";
export const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? useLayoutEffect : useEffect;
このへん毎回考慮するの面倒なのでライブラリ作るか…。
localStorage
は SSR or SSG には存在しないので useState
の初期値セットに使用するとエラーになる。
ブラウザJavaScriptのAPIは useEffect
内に制限することでエラーを回避できる。
よって対処法はこの通り。
-
useState
の初期値はハードコーディングする - マウント時のみ実行される
useIsomorphicLayoutEffect
で本当の初期値をlocalStorage
から取得してセットする - テーマが変更されたら
useIsomorphicLayoutEffect
でlocalStorage
に上書きする
最終形はこんな感じ。
type Theme = "light" | "dark";
const THEME_STORAGE_KEY = "__initial_theme_state";
const getTheme = (): Theme => (localStorage.getItem(THEME_STORAGE_KEY) ?? "light") as Theme;
const saveTheme = (theme: Theme) => localStorage.setItem(THEME_STORAGE_KEY, theme);
const useTheme = () => {
const [theme, setTheme] = React.useState<Theme>("light");
useIsomorphicLayoutEffect(() => {
setTheme(getTheme());
}, []);
useIsomorphicLayoutEffect(() => {
saveTheme(theme);
// 以下略
テーマを変更して画面リロードしてもテーマカラーが維持されることを確認。
ブログのデザインを作るために昨日 Adobe XD をインストールした。ツール使用経験はない。デザイン経験はない。ノンデザイナーズ・デザインブックを読んだことがあるだけ。
がんばります。
あと人生で初めてドメインというものを買いました。
stin .com がほしかったけど流石になかった。stin .io も stin .dev も stin .app もない。
stin.ink を買った。大切に使う
これはTwitterか?
Next.js が生成する tsconfig.json
が strict: false
になってるの忘れていた。
忘れずに strict: true
に直しましょう。 strict: false
で開発するのは負け組(過激派)
CSS弱者ことワイ、 transform-origin
の存在を知らずに2時間以上溶かした。
.hoge {
transfom: rotate(45deg);
transform-origin: 50px 50px;
}
によって rotate
の回転する中心の座標を指定することが可能になる。(rotate
以外にも用途はある。はず。)
Markdown から TOC(Table of Contents) を生成する方法を模索していた。
remark
プラグインの remark-toc
を導入すると、記事中に # table of contens
という空セクションを用意することでそこに自動で TOC を挿し込んでくれる。
しかし、今回は記事の一部にするのではなくサイドメニューに表示したかった。 remark-toc
ではその仕様は満たせなかった。
模索した結果、 markdown-toc を導入することにした。
markdown-toc
に markdown 文字列を渡すと、tocだけを表現する markdown 文字列を生成してくれるので、それをサイドメニュー上で react-markdown
を使ってレンダリングする。
-
markdown-toc
に markdown 文字列を渡す - TOC を表現する markdown 文字列が生成される
-
react-markdown
で TOC markdown をレンダリングするコンポーネント作成 - そのコンポーネントをサイドメニューで表示
Node.js v12 に fs
の Promise 版が Stable で搭載されている。そちらを使います。
react-markdown
はデフォルトでは heading に id を付与しない。
remark-slug
を plugins
に挿し込んでみたけどダメだった。(おそらく markdown から HTML 文字列に直接変換するとき用だろう)
id を付与するには renderers
でカスタマイズする。
import remarkGfm from "remark-gfm";
import ReactMarkdown from "react-markdown";
<ReactMarkdown
plugins={[remarkGfm]}
renderers={{
heading: Heading,
}}>
{children}
</ReactMarkdown>
// const Heading = props => { ??? }
ここから自作の Heading を用意するのだが、 id をどうやって生成するかめっちゃ悩んだ。
## [Hoge](https://example.com)
といった書き方があり得るため、単純に中身を id にコピーするだけではだめ。
renderers
に渡されたコンポーネントが受け取る props
は何があるんだろうと思って console.log(props)
して探ってみたら、 props.node.data.id
にまさにほしい値があるではないか。
ということで使いたい値だけ console.log から読み取って適当に型定義をしておく。(てか props
で色々受け取れるなら型定義しておいてほしい)
カスタム Heading
はこんな感じになりました。
type HeadingProps = {
level: number;
node: {
data: { id: string };
};
};
const Heading: React.FC<HeadingProps> = props => {
return React.createElement(
`h${props.level}`,
{ id: props.node.data.id },
props.children,
);
};
remark-slug
使えないのかぁ〜と思って plugins にわたすのをやめたら id が取れなくなったw
props.node.data.id
が取得できていたのは remark-slug
のおかげだったようです…。
props
型定義しておけないのは挿入するプラグインで得られるデータが変わるためかぁ。(プラグイン式つらくね?)
Google Analytics を入れるために公式の examples を参考にした
このスクラップにメモを残しながら作成した個人ブログをリニューアルしたのでお知らせします!!!!!!!!!!!!!!!!!!!!!