投稿日:2024/5/3
更新日:2024/5/19
Next.js14.4で作成している個人ブログにシンタックスハイライト導入した際につまったのでその備忘録です。
Microcms×Next.jsとかでググるとhighlight.jsを使ってる記事が多くヒットするかと思うので
とりあえずそのライブラリを使っていきます。
// ライブラリ関連
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>
);
}
こんな感じで追加するとローカルではシンタックスハイライトが適応されているのが確認できると思います。
ではビルド通してみましょう
% 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というライブラリを利用してみることにしました!
// ライブラリ関連
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>
);
}
ローカルだとこれでシンタックスハイライトが当たってることが確認できるはずです。
では、ビルド通してみましょう!
% 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の方がデザイン的に好きなので、結果的に変えて良かったなと思いました!