技術ブログの作り方 - 実装編

2023/11/10

開発備忘シリーズの最後は実装編です。
ソースコードは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にデプロイするフローを組むところの方が詰まったので、それはまた別途記事にします。

shin-taroのプロフィール画像

shin-taro

フロントエンドをやっています。 このブログは気ままに書いているので更新頻度は疎らです。