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 を参考にした
このスクラップにメモを残しながら作成した個人ブログをリニューアルしたのでお知らせします!!!!!!!!!!!!!!!!!!!!!