技術ブログの作り方 - 実装編
開発備忘シリーズの最後は実装編です。
ソースコードはGitHubにて公開しているので、この記事では全体の構成とポイントを何か所かピックアップして記載します。
ディレクトリ構成
今回ディレクトリ構成は以下のようになっています。
src
├── app
├── components
│ ├── elements
│ ├── features
│ ├── icons
│ ├── layouts
│ └── templates
├── const
├── libs
│ └── newt
├── models
│ └── Article
├── modules
└── styles
完全にオレオレ設計ですが、結構推しの構成です。また別途記事にしたいと思っています。
各ディレクトリについて簡単に説明します。
app
今回はNext.jsでApp Routerを使用しているため、routingやlayoutははここで定義する形になります。
また、Data fetchも全てpage.tsx
ファイル内で行っています。
SSRやStreamingする場合はもっと細かくSuspence境界を意識する必要がありますが、今回は静的エクスポートなので全部まとめちゃいました。
components
各コンポーネントは全てこのディレクトリの中で定義します。
色々意図があってこの設計にしているのですが、長くなるので詳細は別の機会に。
- element
- 見た目だけに責務を持つコンポーネント
- ロジックや状態は持たない
- features
- 機能(≒ロジック)を持つコンポーネント
- 必然的に状態を持ったりブラウザAPIを使用するためclient componentになる
- icons
- React component化したsvg要素
- layouts
- HeaderやFooter、コンテンツ幅を決めるコンテナ要素など
- 基本的に
layout.tsx
内で使用される - elemetsの中でも特殊な位置づけのものをまとめたイメージ
- templates
- 画面単位のコンポーネント
- 画面数と同じ数のコンポーネントだけがこのディレクトリ配下に入る
page.tsx
ではこのコンポーネントを呼び出し必要に応じてpropsを流す
const
共通で使われる定数。環境変数などもここでprocess.env
から取ってexportしてます。
libs
外部サービス依存のmoduleを格納する場所。大体api clientとかになる。
modles
フロントに最適化したドメインモデル的なものを型定義する。
APIから取得したデータをmodelの型に整形し、コンポーネントはそのmodelの形でpropsを受け取る。
modules
汎用的な関数の置き場。
styles
グローバルに適用するcssを定義する場所。
tailwindで必要な↓こういうやつと、html{ font-size: 62.5%; }
みたいなやつを書いてます。
@tailwind base;
@tailwind components;
@tailwind utilities;
Tailwind
tailwind.config.js
でtheme設定し、デザイン設計に沿ったスタイルに制限出来るようにしています。
デフォルトのutility pluginを上書きして使用できる色やフォントサイズ等を定義している感じです。
tailwindはこういったcssメタフレームワークとしての側面が魅力的で、この辺も別途記事に書きたいと思っています。
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.tsx"],
theme: {
screens: {
sp: { max: "767px" },
pc: { min: "768px" },
},
colors: {
main: "#117099",
accent: "#A94949",
backGround: "#272727",
font: "#FFFFFF",
sub: "#3C3C3C",
subFont: "#CBCBCB",
},
fontSize: {
first: "3.2rem",
second: "2.4rem",
third: "2rem",
fourth: "1.6rem",
fifth: "1.4rem",
sixth: "1.2rem",
},
//...
}
}
記事データの表示
html-react-parserを使用してHTML文字列をパースし、HTMLタグごとに愚直にスタイルを当てています。
( 結構大変なのでなんかもっといい感じにしたい )
export const ParserOption: HTMLReactParserOptions = {
replace: (domNode) => {
if (!(domNode instanceof Element)) return NoReturnValue
const { tagName, children, attribs, parentNode } = domNode
const props = attributesToProps(attribs)
if (tagName === "h2")
return (
<h2 className="mb-[30px] mt-[60px] text-second" {...props}>
{domToReact(children, ParserOption)}
</h2>
)
if (tagName === "h3")
return (
<p className="mb-[25px] mt-[50px] text-third" {...props}>
{domToReact(children, ParserOption)}
</p>
)
//...
},
}
Syntax Highlight
記事のコード部分はreact-syntax-highlighterを使用しています。
styleはatomOneDarkにしてみました。
上述のHTML文字列のパースの中でSyntaxHighlighter
コンポーネントに渡しています。
// ...
if (tagName === "pre") return <>{domToReact(children, ParserOption)}</>
if (tagName === "code") {
const languageClass = /language-(\w+)/.exec(attribs.class)
if (!languageClass)
return (
<code className="rounded-[3px] bg-sub px-[4px] py-[1px] text-fourth font-medium" {...props}>
{domToReact(children, ParserOption)}
</code>
)
const language = languageClass[1] ?? ""
const codeText = children
.filter((child): child is Text => child instanceof Text)
.map((child) => child.data)
.join("")
return (
<SyntaxHighlighter language={language} style={atomOneDark} PreTag={CustomPre} CodeTag={CustomCode}>
{codeText}
</SyntaxHighlighter>
)
}
まとめ
はい、主要なところはこれくらいですね。とても簡単でした。
どちらかというとCMSのWebhookをトリガーにしてGitHub Pagesにデプロイするフローを組むところの方が詰まったので、それはまた別途記事にします。