YASD-TECH
YASD TECH
# Next.js
# TypeScript

Next.js14系で作成している個人ブログにシンタックスハイライトを導入した

投稿日:2024/5/3

更新日:2024/5/19

ttitleImage

Summary

Next.js14.4で作成している個人ブログにシンタックスハイライト導入した際につまったのでその備忘録です。

Microcms×Next.jsとかでググるとhighlight.jsを使ってる記事が多くヒットするかと思うので

とりあえずそのライブラリを使っていきます。

highlight.jsでシンタックスハイライト

app.tsx
// ライブラリ関連
import Image from "next/image";
import { Box, Chip, Container, Grid, Stack, Typography } from "@mui/material";
import AccessTimeRoundedIcon from "@mui/icons-material/AccessTimeRounded";
import SyncIcon from "@mui/icons-material/Sync";

// ####################### 追加 ###################################
import * as cheerio from "cheerio";
import hljs, { HighlightResult } from "highlight.js";
import "highlight.js/styles/github-dark.css";
// ####################### 追加 ###################################

// 型定義とかその辺
import { getAllBlogs, getBlog } from "@/libs/client";
import { Blog } from "@/types/blog";
import { renderToc } from "../../../../libs/render-toc";

// コンポーネント
import TableOfContents from "@/components/TableOfContents";

/**
 * ビルド時に詳細ページを作成させる
 * @returns
 */
export async function generateStaticParams() {
  const { contents } = await getAllBlogs();
  const paths = contents.map((blog: any) => {
    return { id: blog.id };
  });
  return [...paths];
}

export default async function Page({ params }: { params: { id: string } }) {
  const blog: Blog = await getBlog(params);
  const toc = renderToc(blog.content);

  // ####################### 追加 ###################################
  const $ = cheerio.load(blog.content);

  $("div[data-filename]").each((_, elm) => {
    $(elm).prepend(`<span>${$(elm).attr("data-filename")}</span>`);
  });

  $("pre code").each((_, elm) => {
    const language = $(elm).attr("class") || "";
    let result;
    if (language == "") {
      result = hljs.highlightAuto($(elm).text());
    } else {
      result = hljs.highlight($(elm).text(), {
        language: language.replace("language-", ""),
      });
    }
    $(elm).html(result.value);
    $(elm).addClass("hljs");
  });
  // ####################### 追加 ###################################

  return (
    <Container>
      <Box style={{ display: "flex", justifyContent: "center" }}>
        <Stack
          direction="row"
          spacing={1}
          alignItems="center"
          sx={{ paddingTop: "5px" }}>
          {blog.category.map((category: any, index: number) => (
            <Chip
              key={index}
              label={category.name}
              variant="outlined"
              sx={{ color: "white" }}
            />
          ))}
        </Stack>
      </Box>
      <h1 className="title">{blog.title}</h1>
      <Box style={{ display: "flex", justifyContent: "center" }}>
        <Stack
          direction="row"
          spacing={1}
          alignItems="center"
          sx={{ padding: "5px" }}>
          <AccessTimeRoundedIcon />
          <Typography suppressHydrationWarning={true}>
            投稿日:
            {new Date(blog.publishedAt).toLocaleDateString("ja-JP")}
          </Typography>
        </Stack>
        <Stack
          direction="row"
          spacing={1}
          alignItems="center"
          sx={{ padding: "5px" }}>
          <SyncIcon />
          <Typography suppressHydrationWarning={true}>
            更新日:
            {new Date(blog.updatedAt).toLocaleDateString("ja-JP")}
          </Typography>
        </Stack>
      </Box>
      <Image
        style={{
          marginBottom: "10px",
          marginTop: "10px",
          width: "100%",
          height: "100%",
          backgroundColor: "#fff",
        }}
        src={blog.eyecatch.url}
        width={100}
        height={200}
        alt="Slider Image"
        sizes="(min-width: 1024px) 100vw, 60vw"
        className="slideImage"
      />
      <Grid container spacing={2}>
        <Grid item xs={12} md={9}>
          {/* ####################### 追加 ################################### */}
          <Box
            className="blog"
            dangerouslySetInnerHTML={{ __html: $.html() }}
          />
          {/* ####################### 追加 ################################### */}
        </Grid>
        <Grid item xs={12} md={3}>
          <TableOfContents toc={toc} />
        </Grid>
      </Grid>
    </Container>
  );
}

こんな感じで追加するとローカルではシンタックスハイライトが適応されているのが確認できると思います。

ではビルド通してみましょう

Terminal
% docker compose run --rm app yarn build
yarn run v1.22.19
$ next build
Next.js 14.1.4
   - Environments: .env.local

   Creating an optimized production build ...
Compiled successfully
Linting and checking validity of types    
Collecting page data    
   Generating static pages (10/12) [   =] 
SyntaxError: Invalid regular expression: /(?!-)([!#\$%&*+.\\/<=>?@\\\\^~-]|(?!([(),;\\[\\]\`|{}]|[_:"']))(\\p{S}|\\p{P}))--+|--+(?!-)([!#\$%&*+.\\/<=>?@\\\\^~-]|(?!([(),;\\[\\]\`|{}]|[_:"']))(\\p{S}|\\p{P}))/mu: Invalid escape
    at RegExp (<anonymous>)
    at t (/app/.next/server/app/(main)/blog/[id]/page.js:5:211373)
    at a (/app/.next/server/app/(main)/blog/[id]/page.js:5:213849)
    at /app/.next/server/app/(main)/blog/[id]/page.js:5:214495
    at Array.forEach (<anonymous>)
    at a (/app/.next/server/app/(main)/blog/[id]/page.js:5:214475)
    at /app/.next/server/app/(main)/blog/[id]/page.js:5:214747
    at C (/app/.next/server/app/(main)/blog/[id]/page.js:5:214751)
    at /app/.next/server/app/(main)/blog/[id]/page.js:5:215782
    at Array.map (<anonymous>)

Error occurred prerendering page "/blog/1iyv927vu". Read more: https://nextjs.org/docs/messages/prerender-error

SyntaxError: Invalid regular expression: /(?!-)([!#\$%&*+.\\/<=>?@\\\\^~-]|(?!([(),;\\[\\]\`|{}]|[_:"']))(\\p{S}|\\p{P}))--+|--+(?!-)([!#\$%&*+.\\/<=>?@\\\\^~-]|(?!([(),;\\[\\]\`|{}]|[_:"']))(\\p{S}|\\p{P}))/mu: Invalid escape
    at RegExp (<anonymous>)
    at t (/app/.next/server/app/(main)/blog/[id]/page.js:5:211373)
    at a (/app/.next/server/app/(main)/blog/[id]/page.js:5:213849)
    at /app/.next/server/app/(main)/blog/[id]/page.js:5:214495
    at Array.forEach (<anonymous>)
    at a (/app/.next/server/app/(main)/blog/[id]/page.js:5:214475)
    at /app/.next/server/app/(main)/blog/[id]/page.js:5:214747
    at C (/app/.next/server/app/(main)/blog/[id]/page.js:5:214751)
    at /app/.next/server/app/(main)/blog/[id]/page.js:5:215782
Generating static pages (12/12) 

> Export encountered errors on following paths:
        /(main)/blog/[id]/page: /blog/1iyv927vu
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

ありゃ、、、なんかシンタックスエラーがでておる、

issue立ってたので確認したところ、Next.js14.3のバージョンのビルドプロセスに問題がありそうです。

対応策ないとのこと。

というわけで、記事少ないけどおしゃんなshikiというライブラリを利用してみることにしました!

shikiでシンタックスハイライト

app.tsx
// ライブラリ関連
import Image from "next/image";
import { Box, Chip, Container, Grid, Stack, Typography } from "@mui/material";
import AccessTimeRoundedIcon from "@mui/icons-material/AccessTimeRounded";
import SyncIcon from "@mui/icons-material/Sync";

// ####################### 追加 ###################################
import * as cheerio from "cheerio";
import { getHighlighter } from "shiki";
// ####################### 追加 ###################################

// 型定義とかその辺
import { getAllBlogs, getBlog } from "@/libs/client";
import { Blog } from "@/types/blog";
import { renderToc } from "../../../../libs/render-toc";

// コンポーネント
import TableOfContents from "@/components/TableOfContents";

/**
 * ビルド時に詳細ページを作成させる
 * @returns
 */
export async function generateStaticParams() {
  const { contents } = await getAllBlogs();
  const paths = contents.map((blog: any) => {
    return { id: blog.id };
  });
  return [...paths];
}

export default async function Page({ params }: { params: { id: string } }) {
  const blog: Blog = await getBlog(params);
  const toc = renderToc(blog.content);

  // ####################### 追加 ###################################
  const highlighter = await getHighlighter({
    themes: ["slack-dark"],
    langs: ["tsx", "shell", "typescript"],
  });
  const $ = cheerio.load(blog.content);

  $("div[data-filename]").each((_, elm) => {
    $(elm).prepend(`<span>${$(elm).attr("data-filename")}</span>`);
  });

  $("pre code").each((_, elm) => {
    let language = $(elm).attr("class")?.split("language-")[1] || "";
    const codeText = $(elm).text();
    const html = highlighter.codeToHtml(codeText, {
      lang: language,
      theme: "slack-dark",
    });
    $(elm).parent().replaceWith(html);
  });
  // ####################### 追加 ###################################

  return (
    <Container>
      <Box style={{ display: "flex", justifyContent: "center" }}>
        <Stack
          direction="row"
          spacing={1}
          alignItems="center"
          sx={{ paddingTop: "5px" }}>
          {blog.category.map((category: any, index: number) => (
            <Chip
              key={index}
              label={category.name}
              variant="outlined"
              sx={{ color: "white" }}
            />
          ))}
        </Stack>
      </Box>
      <h1 className="title">{blog.title}</h1>
      <Box style={{ display: "flex", justifyContent: "center" }}>
        <Stack
          direction="row"
          spacing={1}
          alignItems="center"
          sx={{ padding: "5px" }}>
          <AccessTimeRoundedIcon />
          <Typography suppressHydrationWarning={true}>
            投稿日:
            {new Date(blog.publishedAt).toLocaleDateString("ja-JP")}
          </Typography>
        </Stack>
        <Stack
          direction="row"
          spacing={1}
          alignItems="center"
          sx={{ padding: "5px" }}>
          <SyncIcon />
          <Typography suppressHydrationWarning={true}>
            更新日:
            {new Date(blog.updatedAt).toLocaleDateString("ja-JP")}
          </Typography>
        </Stack>
      </Box>
      <Image
        style={{
          marginBottom: "10px",
          marginTop: "10px",
          width: "100%",
          height: "100%",
          backgroundColor: "#fff",
        }}
        src={blog.eyecatch.url}
        width={100}
        height={200}
        alt="Slider Image"
        sizes="(min-width: 1024px) 100vw, 60vw"
        className="slideImage"
      />
      <Grid container spacing={2}>
        <Grid item xs={12} md={9}>
          {/* ####################### 追加 ################################### */}
          <Box
            className="blog"
            dangerouslySetInnerHTML={{ __html: $.html() }}
          />
          {/* ####################### 追加 ################################### */}
        </Grid>
        <Grid item xs={12} md={3}>
          <TableOfContents toc={toc} />
        </Grid>
      </Grid>
    </Container>
  );
}

ローカルだとこれでシンタックスハイライトが当たってることが確認できるはずです。

では、ビルド通してみましょう!

Terminal
% docker compose run --rm app yarn build
yarn run v1.22.19
$ next build
Next.js 14.1.4
   - Environments: .env.local

   Creating an optimized production build ...
Compiled successfully
Linting and checking validity of types    
Collecting page data    
Generating static pages (12/12) 
Collecting build traces    
Finalizing page optimization    

Route (app)                              Size     First Load JS
┌ ○ /                                    127 kB          218 kB
├ ○ /_not-found                          882 B          85.2 kB
├ ○ /blog                                11.1 kB         140 kB
├ ● /blog/[id]                           10.9 kB         133 kB
├   ├ /blog/cqyd1jctmz
├   └ /blog/1iyv927vu
├ ○ /contact                             2.15 kB         109 kB
├ ○ /privacy_poricy                      405 B           110 kB
├ ○ /profile                             3.33 kB         115 kB
└ ○ /sitemap.xml                         0 B                0 B
+ First Load JS shared by all            84.4 kB
chunks/69-1b12c227bd5625d7.js        29 kB
chunks/fd9d1056-ba9beeffd9164619.js  53.4 kB
other shared chunks (total)          1.96 kB


○  (Static)  prerendered as static content
●  (SSG)     prerendered as static HTML (uses getStaticProps)

Done in 106.34s.

通りました!!!これでデプロイできます!!!

まとめ

後から思ったんですが、多分shikiの方がデザイン的に好きなので、結果的に変えて良かったなと思いました!

Index

  • Summary
  • highlight.jsでシンタックスハイライト
  • shikiでシンタックスハイライト
  • まとめ