Next+ChakraUI+GitHub Pagesで静的サイトを作った & Markdownで内容を更新できるようにした

注：Web系の技術をまんべんなく初心者レベルな人間が書いています

# 書いた目的

ぱっとQiitaを見た感じ2023年時点でPagesを使ったデプロイがなかったのとNextを触ってみたかったのでその開発時の困ったところとかの備忘録

## ディレクトリ構成

```
.
├── components
│   ├── Footer.tsx
│   ├── MarkdownContent.tsx # マークダウンを解析してChakra UIに変換
│   └── NavBar.tsx
├── markdown # マークダウン置き場
│   ├── blogs
│   │   └── test.md
│   ├── contact.md
│   ├── history.md
│   ├── hobby.md
│   ├── profile.md
│   └── skills.md
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
│   ├── _app.tsx
│   ├── blogs
│   │   ├── [id].tsx # どんなファイル名かはわからないので動的にページを生成するところ
│   │   └── index.tsx # 一覧を表示する
│   ├── contact.tsx
│   ├── history.tsx
│   ├── hobby.tsx
│   ├── index.tsx
│   └── skills.tsx
├── ppm-lock.yaml
└── tsconfig.json
```

### 開発環境

- node v19.9.0
- pnpm v8.3.1
- React v18.2.0
- next v13.3.2
- @chakura-ui/react v2.6.1

エディタ(？)はCodespacesでやりましただからVSCです
## Markdownで更新を楽に

フロントも最近いろんな技術が生えまくっていて、ポートフォリオ書く時にもいちいち内容と一緒に装飾を考えるのが手間だったので、デザインと内容で別個に編集できるようにコンポーネント部分とマークダウン部分に分けた。

### ChakraUIのコンポーネントに変換する

マークダウンの解析・レンダリングに`ReactMarkdown`を使ったところ、すべての要素が同じスタイルになった。どうやらChakraUIの性質のようなので、レンダリングされたタグに基づいてそれぞれChakraUIで想定しているコンポーネントに変換するように設定した。

```tsx:MarkdownContent.tsx
//...
 const components: Components = {
     h1: ({ node, ...props }) => <Heading as="h1" size="xl" {...props} />,
     h2: ({ node, ...props }) => <Heading as="h2" size="lg" {...props} />,
     h3: ({ node, ...props }) => <Heading as="h3" size="md" {...props} />,
     blockquote: ({ node, ...props }) => <Box as="blockquote" p="1rem" borderLeft="4px solid #ddd" {...props} />,
     ul: ({ node, ...props }) => <List spacing={3} styleType="disc" paddingLeft={5} {...props} />,
     ol: ({ node, ...props }) => <List as="ol" spacing={3} styleType="decimal" paddingLeft={5} {...props} />,
     li: ({ node, ...props }) => <ListItem {...props} />,
     pre: ({ node, ...props }) => <Box as="pre" p="1rem" {...props} />,
     code: ({ node, ...props }) => <Code {...props} />,
     a: ({ node, ...props }) => <><Link color="teal.500" {...props} /><GoLinkExternal/></>,
     img: ({ node, ...props }) => <Image {...props} />,
     table: ({ node, ...props }) => <Table {...props} />,
     thead: ({ node, ...props }) => <Thead {...props} />,
     tbody: ({ node, ...props }) => <Tbody {...props} />,
     tr: ({ node, ...props }) => <Tr {...props} />,
     th: ({ node, ...props }) => <Th {...props} />,
     td: ({ node, ...props }) => <Td {...props} />,
     hr: ({ node, ...props }) => <Divider {...props} />,
     p: ({ node, ...props }) => <Text {...props} />,
 };
//...

export default function MarkdownContent ({ markdown }: MarkdownContentProps) {
    return (
        <Container ml={4}><ReactMarkdown components={components}>{ markdown }</ReactMarkdown></Container>
    );
};

```

### ブログ機能を作ってみた

ジャンルごとにnote,notion,qiita,みたいなジャンル分けをしたほうがいいのだろうけど自分にはうまく管理できずどれもなぁなぁで放置されやすいので、マークダウンだけでページ作れるようにしたしついでに実装しようという気分になった。

ただやみくもにページを増やすとやっぱり毎回TSでコーディングしないといけなくなるので、Next.jsの動的ルーティングを用いて`/blog/`にマークダウンのファイルの数だけページを生成してくれるような実装にした。
- ref: [Dynamic Routes](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes)


```tsx:/blogs/[id].tsx
import { GetStaticPaths, GetStaticProps } from 'next';
import Head from 'next/head';
import NavBar from '../../components/NavBar';
import Footer from '../../components/Footer';
import path from 'path';
import fs from 'fs';
import MarkdownContent from '../../components/MarkdownContent';
import { Link } from '@chakra-ui/react';

export default function Blog({markdown}: {markdown: string}) {
    return (
      <>
          <Head>
              <title>My Blog</title>
          </Head>
          <NavBar />
          <MarkdownContent markdown={markdown}/>
          <Link href="/blogs" pos="fixed">Back to Blogs</Link>
          <Footer />
      </>
    );
  }

export const getStaticPaths: GetStaticPaths = async () => {
  const blogsDirectory = path.join(process.cwd(), 'markdown', 'blogs');
  const filenames = fs.readdirSync(blogsDirectory);
  const paths = filenames.map((filename) => ({
    params: { id: filename.replace(/\.md$/, '') },
  }));

  return { paths, fallback: false };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const blogsDirectory = path.join(process.cwd(), 'markdown', 'blogs');
  const filePath = path.join(blogsDirectory, `${params?.id}.md`);
  const markdown = fs.readFileSync(filePath, 'utf8');

  return { props: { markdown } };
};
```

このコンポーネントが行うのは

- `/markdown/blogs/`のマークダウンファイルを読み込んで、その名前で静的ページのパスを生成する
- `/blogs/[id]`に生成するための内容をファイルから読み込んで`MarkdownContent`に渡す

なので、それぞれNextで用意されている`getStaticPaths`,`getStaticProps`を定義して行う。

## Nextのちょっと困ったところ

これはおそらくNextへの理解度が足りてないから起こったのだろうなとおもっているけど、`next/link`の`Link`コンポーネントで指定したhref属性は基本的に前に`basePath`が挿入される。なので軽い気持ちで何も考えず`Link`コンポーネントを使うと予期しないリンクを作ってしまうかもしれないのでちゃんと確認はしようと思った。ちょっと煩わしかったので、ひとまずここではChakuraUIのほうのLinkでごまかした。

## デプロイを自動化しよう

金もサーバーもないので、GitHub Pagesを使いました。お初のデプロイのときにQiitaとZennをめぐってみたところ2023年版のブログないなーと思っていたので一応現状のActionsをここに残しておく
多分これはGitHub Pagesのドキュメントみたら作れると思う……？

(2023年6月19日追記: バージョンが古かったのでv3に変更しました)
```yml:gh-page.yml
name: github pages

on:
  push:
    branches:
      - profile

jobs:
    build-deploy:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v3
            - name: Setup node
              uses: actions/setup-node@v3
              with:
                node-version: 'latest'
            - name: Install pnpm
              run: npm install -g pnpm
            - name: Cache node modules
              uses: actions/cache@v3
              with:
                path: ~/.pnpm-store
                key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
                restore-keys: |
                  ${{ runner.os }}-pnpm-

            - name: Install dependencies
              run: pnpm install --frozen-lockfile
            
            - name: Next build
              run: pnpm run build
            
            - name: Export
              run: pnpm run export && ls ./
            
            - name: Add nojekyll
              run: touch out/.nojekyll

            - name: Deploy
              uses: peaceiris/actions-gh-pages@v3
              with:
                github_token: ${{ secrets.GITHUB_TOKEN }}
                publish_dir: ./out

```



## 実際に出来上がったもの

https://raiga0310.github.io/

自分にとっては設計しやすいし、これからも変更が楽しい感じになったかなと思ったのでOK