💡

RemixでGithubPageにMDX対応の個人ブログを作る

2024/11/07に公開

使用技術

  • Remix SPAモード
  • Vite
  • TypeScript
  • MDX

環境設定

RemixのQuickStartに沿ってインストールする
https://remix-docs-ja.techtalk.jp/start/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もインストールする
https://remix-docs-ja.techtalk.jp/guides/mdx
https://mdxjs.com/packages/rollup/

ターミナル
npm install @mdx-js/rollup remark-frontmatter remark-mdx-frontmatter

viteの設定

  • MDXRollupを追加
  • RemixをSPAモードに変更
vite.config.ts
 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を返すようにします。

posts.tsx
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>
    </>
  )
}
$slug.tsx
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すると完成です。(大変参考になりました。ありがとうございます。)
https://zenn.dev/cybozu_frontend/articles/remix-spa-mode-gh-page
https://ja.vite.dev/guide/static-deploy

Discussion