Closed26

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, stylessrc に移動する。

npm run dev

問題なく動作することを確認。(簡単過ぎる)

すてぃんすてぃん

prettier を導入

npm install -D prettier

.prettierrc ファイルを追加(秘伝のタレ的設定ファイルをコピペ)。
.prettierignore ファイルを追加(.gitignore の内容をコピペ)。

package.jsonscripts にコマンド追加。
どこまで拡張子使うかわからないからとりあえず React で使えるやつ全部列挙。

"format": "prettier --write '**/*.{js,jsx,ts,tsx,json,css,scss}'"

pre-commit hook で勝手にフォーマットしてくれるようにもする。
https://prettier.io/docs/en/precommit.html#option-1-lint-stagedhttpsgithubcomokonetlint-staged

npx mrm lint-staged

ここまでで一旦コミットします。

すてぃんすてぃん

リセットCSSを入れる。
業務で ress.css を使ってるので引き続きこれ。
https://github.com/filipelinhares/ress

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.tsximport "../styles/ress.css"; を先頭に追加。

配色考えないといけない。ダークモードも導入したい。

調査したところ、React のステートに応じて css 変数を書き換えることが可能。
つまり Next.js が押してるらしい CSS Modules でスタイリングを書きつつ、テーマ変更機能も実装できる。(ダークモードはメディアクエリではなくReactのステート管理にしたかった)

document.documentElement.style.setProperty('--your-variable', '#YOURCOLOR');
すてぃんすてぃん

ブログ作りをさぼってスプラトゥーンをしていました(反省)。

配色を決めました。adobe のこちらのツールを使用。
https://color.adobe.com/ja/create/color-wheel
adobe さんUIツールを何個も生み出してるだけあって、配色決めるツールくらい無料で使わせたるわって感じですかね。ありがとうございます。

決めた配色は CSS 変数で参照するようにします。テーマの差し替えを実行するために。
https://zenn.dev/stin/articles/how-to-change-theme-with-css-modues
こちらの記事を参考にテーマ切り替えを実装します(手前味噌)。

すてぃんすてぃん

この記事のようにテーマ切り替えを実装するとリロードしたときに一瞬画面がちらつく。なぜなら useEffect は画面が描画されたあとに実行されるため。こういう場合は useLayoutEffect を使用すべきだった。

……気が向いたらブログのほうは加筆修正しておきます。

すてぃんすてぃん

useEffectuseLayoutEffect に直して使用する。
テーマの初期値を永続化しておかないと画面リロードのたびに戻ってしまうので、ローカルストレージで管理することにした。

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 から取得してセットする
  • テーマが変更されたら useIsomorphicLayoutEffectlocalStorage に上書きする

最終形はこんな感じ。

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.jsonstrict: 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-slugplugins に挿し込んでみたけどダメだった。(おそらく 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 型定義しておけないのは挿入するプラグインで得られるデータが変わるためかぁ。(プラグイン式つらくね?)

このスクラップは2021/03/25にクローズされました