Next.jsを使って静的なブログサイトを作成する(2)

はじめに

Next.jsのBlog Starter Kitを使ったサイト作成の続編です。
rehype-highlightを使ってシンタックスハイライトを実現します。
テーマごとの見た目はhttps://highlightjs.org/demoで確認できます。
他にマークダウンや目次を設定します。

シンタックスハイライトを使えるようにする

ターミナルを立ち上げてプロジェクトのトップへ移動します。

> cd C:\website\blog-starter-app

必要なパッケージをインストールします。

> npm i unified
> npm i remark-parse
> npm i remark-rehype
> npm i rehype-highlight
> npm i rehype-stringify
> npm i highlight.js

「src\lib\markdownToHtml.ts」をエディタで開くと以下のようになってます。
なお比較のため改行を追加してます。

import { remark } from "remark";
import html from "remark-html";

export default async function markdownToHtml(markdown: string) {
  const result = await remark()
    .use(html)
    .process(markdown);
  return result.toString();
}

上のコードではマークダウン->HTMLを以下のように役割分担しています。

  • remark: マークダウン -> マークダウンの木構造
  • remark-html: マークダウンの木構造 -> HTMLの木構造 -> HTML

これを以下のように変更するとシンタックスハイライトを実現できます。

  • remark: マークダウン -> マークダウンの木構造
  • remark-rehype: マークダウンの木構造 -> HTMLの木構造
  • rehype-highlight: HTMLの木構造 -> HTMLの木構造
  • rehype-stringify: HTMLの木構造 -> HTML

さらに今後の変更に備え、remarkを unifiedremark-parse で置き換えます。

  • remark-parse: マークダウン -> マークダウンの木構造
  • remark-rehype: マークダウンの木構造 -> HTMLの木構造
  • rehype-highlight: HTMLの木構造 -> HTMLの木構造
  • rehype-stringify: HTMLの木構造 -> HTML

以上を参考に「src\lib\markdownToHtml.ts」を以下のように変更します。

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeHighlight from 'rehype-highlight';
import rehypeStringify from 'rehype-stringify';

export default async function markdownToHtml(markdown: string) {
  const result = await unified()
    .use(remarkParse)
    .use(remarkRehype)
    .use(rehypeHighlight)
    .use(rehypeStringify)
    .process(markdown);
  return result.toString();
}

好みのテーマをインポートします。ここではa11y-darkを使います。「src/app/layout.tsx」 内に以下の行を追加します。

import "highlight.js/styles/a11y-dark.css";

以上でシンタックスハイライトが使えるようになってます。

記事のmdファイルで

```js
console.log("Hello");
```

のように記載すると、生成されるHTMLは以下のように表示されます。

console.log("Hello");

マークダウンにHTMLタグを埋め込めるようにする

mdファイルにHTMLタグを記載しても

<p>HTML</p>
Markdown

変換の過程でその内容は消えてしまいます。

Markdown

HTMLタグを反映させるにはremark-rehyperehype-stringifyallowDangerousHtmlオプションを設定します。

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeHighlight from 'rehype-highlight';
import rehypeStringify from 'rehype-stringify';

export default async function markdownToHtml(markdown: string) {
  const result = await unified()
    .use(remarkParse)
    .use(remarkRehype, { allowDangerousHtml: true })
    .use(rehypeHighlight)
    .use(rehypeStringify, { allowDangerousHtml: true })
    .process(markdown);
  return result.toString();
}

参考:

https://zenn.dev/yodaka/articles/cee5943b92dfed

GitHub風のマークダウン記法を使えるようにする

Blog Starter Kitで最初から使えるマークダウンは機能が弱いので、remark-gfmを導入します。あと<br>改行が簡単に使えるようにremark-breaksも導入します。

プロジェクトのトップへ移動して、インストールを実行します。

> npm i remark-gfm
> npm i remark-breaks

「src\lib\markdownToHtml.ts」を以下のように変更します。

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkBreaks from 'remark-breaks';
import remarkRehype from 'remark-rehype';
import rehypeHighlight from 'rehype-highlight';
import rehypeStringify from 'rehype-stringify';

export default async function markdownToHtml(markdown: string) {
  const result = await unified()
    .use(remarkParse)
    .use(remarkGfm)
    .use(remarkBreaks)
    .use(remarkRehype, { allowDangerousHtml: true })
    .use(rehypeHighlight)
    .use(rehypeStringify, { allowDangerousHtml: true })
    .process(markdown);
  return result.toString();
}

目次を表示させる

プロジェクトのトップへ移動して、インストールを実行します。

> npm i rehype-toc
> npm i rehype-slug

rehype-tocだけでも目次は作成できますが、リンクが働きません。
rehype-slugは見出しにIDを振って、リンクを有効にしてくれます。

「src\lib\markdownToHtml.ts」を以下のように変更します。

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkBreaks from 'remark-breaks';
import remarkRehype from 'remark-rehype';
import rehypeSlug from 'rehype-slug';
import rehypeToc from 'rehype-toc';
import rehypeHighlight from 'rehype-highlight';
import rehypeStringify from 'rehype-stringify';

export default async function markdownToHtml(markdown: string) {
  const result = await unified()
    .use(remarkParse)
    .use(remarkGfm)
    .use(remarkBreaks)
    .use(remarkRehype, { allowDangerousHtml: true })
    .use(rehypeSlug)
    .use(rehypeToc, { headings: ["h2"],})
    .use(rehypeHighlight)
    .use(rehypeStringify, { allowDangerousHtml: true })
    .process(markdown);
  return result.toString();
}

<h2>タグで囲まれた見出しを拾って、記事の先頭に目次が作成されます。

デフォルト以外の言語をシンタックスハイライトする

デフォルトでシンタックスハイライトできるのは、以下の表の「初期」欄に丸がついている言語になります。

言語 エイリアス 初期
1C 1c
4D 4d
ABAP sap-abap, abap
ABNF abnf
Access logs accesslog
ActionScript actionscript, as
Ada ada
Alan ln
Alan IF alan, i
AngelScript angelscript, asc
Apache apache, apacheconf
Apex apex
AppleScript applescript, osascript
Arcade arcade
Arduino (C++ w/Arduino libs) arduino, ino
ARM assembler armasm, arm
AsciiDoc asciidoc, adoc
AspectJ aspectj
AutoHotkey autohotkey
AutoIt autoit
AVR assembler avrasm
Awk awk, mawk, nawk, gawk
Ballerina ballerina, bal
Bash bash, sh, zsh
Basic basic
BBCode bbcode
Blade (Laravel) blade
BNF bnf
BQN bqn
Brainfuck brainfuck, bf
C c, h
C# csharp, cs
C/AL cal
C++ cpp, hpp, cc, hh, c++, h++, cxx, hxx
C3 c3
Cache Object Script cos, cls
Candid candid, did
Cap’n Proto capnproto, capnp
Chaos chaos, kaos
Chapel chapel, chpl
Cisco CLI cisco
Clojure clojure, clj
CMake cmake, cmake.in
COBOL cobol, standard-cobol
CODEOWNERS codeowners
CoffeeScript coffeescript, coffee, cson, iced
Coq coq
CpcdosC+ cpc
Crmsh crmsh, crm, pcmk
Crystal crystal, cr
CSP csp
CSS css
cURL curl
Cypher (Neo4j) cypher
D d
Dafny dafny
Dart dart
Delphi dpr, dfm, pas, pascal
Diff diff, patch
Django django, jinja
DNS Zone file dns, zone, bind
Dockerfile dockerfile, docker
DOS dos, bat, cmd
dsconfig dsconfig
DTS (Device Tree) dts
Dust dust, dst
Dylan dylan
EBNF ebnf
Elixir elixir
Elm elm
Erlang erlang, erl
Excel excel, xls, xlsx
Extempore extempore, xtlang, xtm
F# fsharp, fs, fsx, fsi, fsscript
FIX fix
Flix flix
Fortran fortran, f90, f95
FunC func
Gams gams, gms
GAUSS gauss, gss
G-Code gcode, nc
GDScript godot, gdscript
Gherkin gherkin
Glimmer and EmberJS hbs, glimmer, html.hbs, html.handlebars, htmlbars
GN for Ninja gn, gni
Go go, golang
Golo golo, gololang
Gradle gradle
Grammatical Framework gf
GraphQL graphql, gql
Groovy groovy
GSQL gsql
Haml haml
Handlebars handlebars, hbs, html.hbs, html.handlebars
Haskell haskell, hs
Haxe haxe, hx
High-level shader language hlsl
HTML, XML xml, html, xhtml, rss, atom, xjb, xsd, xsl, plist, svg
HTTP http, https
Hy hy, hylang
Inform7 inform7, i7
Ini, TOML ini, toml
Iptables iptables
IRPF90 irpf90
Java java, jsp
JavaScript javascript, js, jsx
Jolie jolie, iol, ol
JSON json, jsonc
JSONata jsonata
Julia julia, jl
Julia REPL julia-repl
Kotlin kotlin, kt
Lang
Lasso lasso, ls, lassoscript
LaTeX tex
LDIF ldif
Leaf leaf
Lean lean
Less less
Lisp lisp
LiveCode Server livecodeserver
LiveScript livescript, ls
LookML lookml
Lua lua
Luau luau
Macaulay2 macaulay2
Makefile makefile, mk, mak, make
Markdown markdown, md, mkdown, mkd
Mathematica mathematica, mma, wl
Matlab matlab
Maxima maxima
Maya Embedded Language mel
Mercury mercury
Mint mint
MIPS Assembler mips, mipsasm
mIRC Scripting Language mirc, mrc
Mirth mirth
Mizar mizar
MKB mkb
MLIR mlir
Mojolicious mojolicious
Monkey monkey
Moonscript moonscript, moon
Motoko motoko, mo
N1QL n1ql
Never never
Nginx nginx, nginxconf
Nim nim, nimrod
Nix nix
NSIS nsis
Oak oak
Object Constraint Language ocl
Objective C objectivec, mm, objc, obj-c, obj-c++, objective-c++
OCaml ocaml, ml
OpenGL Shading Language glsl
OpenSCAD openscad, scad
Oracle Rules Language ruleslanguage
Oxygene oxygene
Papyrus papyrus, psc
Parser3 parser3
Perl perl, pl, pm
PF pf, pf.conf
Phix phix
PHP php
Pine Script pine, pinescript
Plaintext plaintext, txt, text
Pony pony
PostgreSQL & PL/pgSQL pgsql, postgres, postgresql
PowerShell powershell, ps, ps1
Processing processing
Prolog prolog
Properties properties
Protocol Buffers proto, protobuf
Puppet puppet, pp
Python python, py, gyp
Python profiler results profile
Python REPL python-repl, pycon
Q k, kdb
Q# qsharp
QML qml
R r
Razor CSHTML cshtml, razor, razor-cshtml
ReasonML reasonml, re
Rebol & Red redbol, rebol, red, red-system
RenderMan RIB rib
RenderMan RSL rsl
ReScript rescript, res
RiScript risc, riscript
RISC-V Assembly riscv, riscvasm
Roboconf graph, instances
Robot Framework robot, rf
RPM spec files rpm-specfile, rpm, spec, rpm-spec, specfile
Ruby ruby, rb, gemspec, podspec, thor, irb
Rust rust, rs
RVT Script rvt, rvt-script
SAS SAS, sas
Scala scala
Scheme scheme
Scilab scilab, sci
SCSS scss
SFZ sfz
Shape Expressions shexc
Shell shell, console
Smali smali
Smalltalk smalltalk, st
SML sml, ml
Solidity solidity, sol
Splunk SPL spl
SQL sql
Stan stan, stanfuncs
Stata stata
STEP Part 21 p21, step, stp
Structured Text iecst, scl, stl, structured-text
Stylus stylus, styl
SubUnit subunit
Supercollider supercollider, sc
Svelte svelte
Swift swift
Tcl tcl, tk
Terraform (HCL) terraform, tf, hcl
Test Anything Protocol tap
Thrift thrift
Toit toit
TP tp
Transact-SQL tsql
Twig twig, craftcms
TypeScript typescript, ts, tsx, mts, cts
Unicorn Rails log unicorn-rails-log
Unison unison, u
Vala vala
VB.Net vbnet, vb
VBA vba
VBScript vbscript, vbs
Verilog verilog, v
VHDL vhdl
Vim Script vim
WGSL wgsl
X# xsharp, xs, prg
X++ axapta, x++
x86 Assembly x86asm
x86 Assembly (AT&T) x86asmatt
XL xl, tao
XQuery xquery, xpath, xq, xqm
YAML yml, yaml
ZenScript zenscript, zs
Zephir zephir, zep
Zig zig

デフォルト以外の言語をシンタックスハイライトしたい場合は(ここではVBScriptを追加したいとします)、rehype-highlightのlanguagesオプションを使います。これによりデフォルトの言語はシンタックスハイライトされなくなりますので、必要な言語はすべて指定してください。ここでは以下の言語を指定します。

  • Bash
  • C++
  • CSS
  • HTML(XML)
  • JavaScript
  • Markdown
  • Plaintext
  • Shell
  • TypeScript
  • VBScript

「src\lib\markdownToHtml.ts」を以下のように変更します。必要な言語をimportしてlanguagesオプションに渡します。

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkBreaks from 'remark-breaks';
import remarkRehype from 'remark-rehype';
import rehypeSlug from 'rehype-slug';
import rehypeToc from 'rehype-toc';
import rehypeHighlight from 'rehype-highlight';
import rehypeStringify from 'rehype-stringify';

import bash from 'highlight.js/lib/languages/bash';
import cpp from 'highlight.js/lib/languages/cpp';
import css from 'highlight.js/lib/languages/css';
import html from 'highlight.js/lib/languages/xml';
import js from 'highlight.js/lib/languages/javascript';
import md from 'highlight.js/lib/languages/markdown';
import text from 'highlight.js/lib/languages/plaintext';
import shell from 'highlight.js/lib/languages/shell';
import ts from 'highlight.js/lib/languages/typescript';
import vbs from 'highlight.js/lib/languages/vbscript';

export default async function markdownToHtml(markdown: string) {
  const result = await unified()
    .use(remarkParse)
    .use(remarkGfm)
    .use(remarkBreaks)
    .use(remarkRehype, { allowDangerousHtml: true })
    .use(rehypeSlug)
    .use(rehypeToc, { headings: ["h2"],})
    .use(rehypeHighlight, {languages: {bash, cpp, css, html, js, md, text, shell, ts, vbs}})
    .use(rehypeStringify, { allowDangerousHtml: true })
    .process(markdown);
  return result.toString();
}

もしかすると、デフォルトの言語をもっと簡単に指定する方法があるのかも知れませんが、やり方がわかりませんでした。

参考

https://github.com/rehypejs/rehype-highlight
https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md