RemixでGithubPageにMDX対応の個人ブログを作る
使用技術
- Remix SPAモード
- Vite
- TypeScript
- MDX
環境設定
RemixのQuickStartに沿ってインストールする
npx create-remix@latest
mkdir blog
cd blog
npm init -y
# install runtime dependencies
npm i @remix-run/node @remix-run/react @remix-run/serve isbot@4 react react-dom
# install dev dependencies
npm i -D @remix-run/dev vite
MDXRollupプラグインを導入する
ついでにremark-frontmatter,remark-mdx-frontmatterもインストールする
npm install @mdx-js/rollup remark-frontmatter remark-mdx-frontmatter
viteの設定
- MDXRollupを追加
- RemixをSPAモードに変更
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import mdx from '@mdx-js/rollup'
import remarkFrontmatter from "remark-frontmatter";
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
import { copyFileSync } from 'node:fs'
import { join } from 'node:path'
declare module "@remix-run/node" {
interface Future {
v3_singleFetch: true;
}
}
export default defineConfig({
plugins: [
tsconfigPaths(),
+ mdx({
+ remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter],
+ }),
remix({
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
v3_singleFetch: true,
v3_lazyRouteDiscovery: true,
},
+ ssr: false,
}),
],
resolve: {
alias: {
'~': '/app',
}
},
});
MDXの作成~表示
app/routes配下にmdxファイルを入れることで表示できます。
MDX=Markdown+JSXのためimportが可能で、Remixの中にあった画像をインポートしています。デフォルトのViteの設定だとpublicディレクトリ(/public)はルートパス(/)に配置されるため静的ファイルはインポートしたほうが良いと思います。imgタグのsrc属性に直接パスを記載するとmdから見た相対パス(../../public/logo-dark.png)になります。
mdxの中身はこんな感じ。
---
meta:
- title: My Post
headers:
Cache-Control: no-cache
---
import logo from '/logo-dark.png'
# Hello Content
<img src={logo} />
Remixのapp/routes配下に入れたファイルですが、routes直下に入れたページのみルーティングされるようです。
app/routes のすぐ下に直接あるフォルダのみがルートとして登録されます。深くネストされたフォルダは無視されます。app/routes/about/header/route.tsx のファイルは、ルートを作成しません。(https://remix-docs-ja.techtalk.jp/discussion/routes)
routes直下にmdxとtsxが混ざるのは避けたいです。remix-flat-routesを使用すればネストを深くできるとのことでした。(https://github.com/kiliman/remix-flat-routes)
今回は**public/**にmdxファイルと画像等ブログで使用する静的ファイルを記事ごとにまとめたいと思います。
記事の一覧表示
記事を一覧表示するposts.tsxと記事の内容を表示する$slug.tsxを作成しました。
import.meta.globでmdxを取得時にFrontmatterやMDX表示用のFunctionが渡されます。(便利ですね)
$slug.tsxはRemixの機能で動的なルーティングを実現します。そのためファイル名に一致する記事がなければ404を返すようにします。
import { Link, useLoaderData } from "@remix-run/react";
// SPAモードはLoader()は使用できないため、clientLoader()を使用
export async function clientLoader() {
// viteがglobで一致したファイルを取得
const modules = import.meta.glob('/public/posts/**/*{.md,.mdx}', {eager: true})
const posts = Object.entries(modules).map(([filePath, module])=> {
return {
slug: filePath.replace(/^.*[\\/]/, '').replace('.mdx', '').replace('.md', ''),
title: module.frontmatter.meta[0].title,
}
})
return posts
}
export default function Posts() {
const posts = useLoaderData<typeof clientLoader>()
return (
<>
<h1>投稿一覧</h1>
<ul>
{posts.map((post, index) =>
<li key={index}>
<Link to={`/${post.slug}`}>{post.title}</Link>
</li>
)}
</ul>
</>
)
}
import { LoaderFunctionArgs } from "@remix-run/node"
import { useLoaderData } from "@remix-run/react"
export async function clientLoader({params}:LoaderFunctionArgs) {
const modules = import.meta.glob('/public/posts/**/*{.md,.mdx}', {eager: true})
let post = null
Object.entries(modules).forEach(([filePath, module])=> {
const slug = filePath.replace(/^.*[\\/]/, '').replace('.mdx', '').replace('.md', '')
params.slug = params.slug.replace('/posts','')
if(params.slug === slug) {
post = {
title: module.frontmatter.meta[0].title,
module: module.default,
}
}
})
// 記事がなかった場合、404を返す
if (!post) {
throw new Response("Not Found", { status: 404 });
}
return post
}
export default function Post() {
const post = useLoaderData<typeof clientLoader>()
return (
<>
{post.module()}
</>
)
}
GitHub Pagesの設定
以下のサイトを参考にしながらvite.config.ts、GithubActionを作成しGithubにpushすると完成です。(大変参考になりました。ありがとうございます。)
Discussion