注: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で想定しているコンポーネントに変換するように設定した。
//...
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
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に変更しました)
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
実際に出来上がったもの
自分にとっては設計しやすいし、これからも変更が楽しい感じになったかなと思ったのでOK