Home
Blog
Products
Profile
Study
Collatz
© 2024 Oizumi Yuta

【React】ブログ記事の TEX 対応

2024-12-14

概要

ブログ記事の TEX 対応に苦戦していたが、CSR(クライアントサイドレンダリング)により解決した。

対応方法

フロントエンドにライブラリ追加

  • remark-math
  • rehype-katex
npm install remark-math rehype-katex
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import 'katex/dist/katex.min.css';

Next.js に CSR コンポーネント追加

'use client';

// マークダウンをレンダリング
import ReactMarkdown from 'react-markdown';

// TEX レンダリング
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import 'katex/dist/katex.min.css';

import remarkGfm from 'remark-gfm'; // リンクを文字列ではなくリンク形式でレンダリングするために使用

// 独自 CSS
import styles from './markdown.module.css';

const Markdown = (props: { content: string }) => {
  // マークダウンファイルの中身
  const { content } = props;

  return (
    <div className={styles.content}>
      <ReactMarkdown
        remarkPlugins={[remarkMath, remarkGfm]}
        rehypePlugins={[rehypeKatex]}
        components={{
          // リンクを別タブで開く
          a: ({ node, ...props }) => (
            <a {...props} target="_blank" rel="noopener noreferrer">
              {props.children}
            </a>
          ),
        }}
      >
        {content}
      </ReactMarkdown>
    </div>
  );
};

export default Markdown;

SSR からマークダウンファイルを CSR に渡す

import Markdown from '@/components/markdown/page';
import styles from './post.module.css';

export default async function Page({ params }: { params: { id: string } }) {
  const res = await fetch(`${process.env.BACKEND_URL}/api/posts/${params.id}`, {
    cache: 'no-store',
  });
  const post = await res.json();

  if (!post) {
    return <div>ページが見つかりません</div>;
  }

  return (
    <div className={styles.main}>
      <h1 className={styles.title}>{post.title}</h1>
      <p className={styles.date}>{post.date}</p>

      {/* 改修前は以下のように SSR でマークダウンファイルを HTML 化したものを dangerouslySetInnerHTML に渡していた */}
      {/* <div
        className={styles.content}
        dangerouslySetInnerHTML={{ __html: post.content }} // HTMLをレンダリング
      /> */}

      {/* 今回の改修では以下のように CSR でマークダウンファイルの中身を受け取り ReactMarkdown でレンダリングするようにした */}
      <Markdown content={post.content} />
    </div>
  );
}

バックエンド改修

// 以下は SSR から CSR に変更するため削除した
import { marked } from 'marked';

app.get('/api/posts/:id', (req, res) => {
  const postId = req.params.id;
  const filePath = path.join(postsDirectory, `${postId}.md`);

  if (fs.existsSync(filePath)) {
    const fileContents = fs.readFileSync(filePath, 'utf8');
    const { data, content } = matter(fileContents);

    // 以下も削除した
    const htmlContent = marked(content, { renderer });

    // 上記の htmlContent ではなくそのまま content を返すように修正した
    res.json({ id: postId, ...data, content: content });
  } else {
    res.status(404).send({ error: 'Post not found' });
  }
});

動作確認

インライン表示: x2+1=0x^2 + 1 = 0x2+1=0

ブロック表示:

ζ(s)=∑n=1∞1ns\displaystyle \zeta(s) = \sum_{n = 1}^\infty \frac{1}{n^s}ζ(s)=n=1∑∞​ns1​

参考

  • https://zenn.dev/hayato94087/articles/649e8d817165d8
  • https://mk-record.com/article/919c69c8-775e-4055-bcdd-f943357b2856