🔧

Shift JIS の Web サイト向けに JSX 静的サイト・ジェネレーターを作る

2021/12/11に公開

ZOZO で Web エンジニアをしてます@takewell です。
最近はZOZOTOWNのフロントエンドサーバーのリプレイスなどをやっております。 今年で17周年の EC サイトのリプレイスは、やりがいがありいろいろ熱くなって楽しいですね。

ところで、世界にはShift JISという文字コードが用いられた Web サイトが存在します。
Web技術についてサーベーをまとめているW3Techsの調査によるとなんと世界の0.1%程度存在するという結果でした。例えば、みんな大好き https://www.2ch.sc/ などがその一例となります。

歴史的経緯によりShift JISを使わざるおえないケースが稀に存在します。一般的でない規格や技術を用いると芋づる式にさまざまな問題を生じさせることは、コンピューターに関わる方ならば身に覚えがあるのではないでしょうか。文字コードに関して言えば世界はUTF-8以外のことなどは、もう考慮してくれません。外部サービス,SDK,ライブラリなどと接続、連携する際に不都合が生じることもしばしばです。

例えば次のようなライブラリを用いる際に不都合が生じます。 Jamstack と呼ばれるWebサイトの構築手法が考案されています。この手法の良さの一つにあらかじめマークアップを静的に生成するため、移植性が高く、ホストするサーバーの環境を選ばないという特徴があります。しかし、当然ながらjamstackを実現するソフトウェアライブラリはUTF-8のソースからUTF-8のファイルを生成するためShift JISにはそのまま何もせず対応はできません。

そこで本稿では文字コードがShift JISのホストティング環境においても、いわゆる静的サイト・ジェネレーター(Static Site Generator)を部分的に導入するための実装について紹介します。

JSX を用いたマークアップの静的ジェネレーション

Jamtackとは JavaScript,API,Markup の組み合わせです。JavaScript, API は自明ですが、
テンプレート記述言語はさまざま存在します。著名なものでいうと markdown, EJS, Pug, Hamlなどでしょうか。これらを実現するライブラリである静的サイトジェネレーターは300以上存在し、OSSとして公開されています
これらのライブラリを用いてもいいのですが、今回はNode.jsで実現したいと思います。理由としては
文字コード変換をあるのでできるだけデータの加工の透明性を確保したいというのと、余計なものが含まれないシンプルなマークアップ[1]にしたいという2点のためです。

また、テンプレート記法は JavaScript の構文拡張である、JSXを用います。
JSX の優れた点としては、JavaScript の拡張にすぎないという点です。JavaScriptの中にテンプレート記法を用いてマークアップを生成することができます。独自で抑えておかないといけないのは classではなく classNameだとか、リストを生成するときに要素にkey属性が必要だとか覚えることはごくわずか[2]でほぼHTMLそのままのように利用できるため、覚えることが少なく魅力的です。また、マークアップを生成するためのロジックとビューをわざわざ分離せずに、同じ箇所にコンポーネントという単位でモジュール化できる点も保守性の面で大変優れています。

JSXはReactが作り出したDSLですが、優れたアイデアのためVue.jsなどでも利用できるようになっていたりします。

余談ですが、UIを記述するDSLとしてJSXが優れていた点がReactがここまで普及した要因なのではないかと私は考えています。Reactをフレームワークと呼ぶこともこともでかもしれませんが、覚えるAPIも少ないですし、JavaScriptの気の利いた宣言的UIライブラリとして使える点がシンプルで美しいと思います[3]

JSXのついて長々と書いてしまいましたが ReactDOM の API にrenderToStaticMarkupがあります。これはいわゆるSSRなどを実現するためにあるHTMLをnodejs上でレンダリングするためのAPIです。これを使えば静的サイトジェネレーターは簡単に実装できます。

以下サンプル実装

import { promises as fs } from 'fs'
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'

const TITLE = 'タイトル'
const App = () => <h1>Hello</h1>

;(async () => {
  const body = renderToStaticMarkup(<App />)
  const html = `
    <!DOCTYPE html>
    <html lang="ja">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>${TITLE}</title>
    </head>
    <body>
      ${body}
    </body>
    </html>
  `
  await fs.writeFile('./output.html', html)
})()

生成されるHTML

 <!DOCTYPE html>
  <html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>タイトル</title>
  </head>
  <body>
    <h1>Hello</h1>
  </body>
  </html>

以上でサイトジェネレーターの実装は終わります。

ファイルが増えてきた場合にソースファイルの場所と生成ファイルパスをハンドリングするルーター的な実装はどうするのか?などのサイトジェネレーターとして必要な他のテーマもありますが、今回は割愛します。個人的にはライブラリを使うよりも普通にnode.jsで扱う方が小回りが効くのと、自社で使っているテンプレート記法に対応しているサイトジェネレーターライブラリがない場合などは有効なアプローチかと思うのでJSXでサーバーサイドのテンプレートも扱いたいな〜なんてニーズには適合しやすいケースはそれなりにあるのではないかと思います。

UTF-8 => ShiftJIS

前節のサイトジェネレーターが生成するHTMLファイルは当然 UTF-8 です。
また、API などの外部サービスのレスポンスなども ShiftJIS で提供されていることはほぼないでしょう。例えば HeadlessCMS のようなものから API が提供されているとして、そのユーザーの入力も出力もUTF-8 の可能性が高いです。したがって、UTF-8 => ShiftJIS の文字コードの変換が必要です。

文字コードの変換はエンコーディングコンバーターのiconv-lite が利用できます。これを使ってbuffer形式にしてデコードとエンコードをしてあげます。

import { encode, decode } from 'iconv-lite'

const utf8Buffer = Buffer.from(html)
const utf8Html = decode(utf8Buffer, 'utf8')
const shiftJISHtml = encode(utf8Html, 'Shift_JIS')
await fs.writeFile('./output.html', shiftJISHtml)

しかし、場合によってはこれでは変換しきれないものがありあります。UTF-8の文字で ShiftJIS には対応してないものが存在するからです。iconvを用いた場合はそのような場合は文字?として出力されます。これを利用して以下のように文字参照に変換してあげることでカバーすることが可能です。

import { promises as fs } from 'fs'
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { encode, decode } from 'iconv-lite'

// 変換できない文字のサンプル
const App = () => <h1>⼿に⼊る</h1>

const TITLE = 'タイトル'

;(async () => {
  const body = renderToStaticMarkup(<App />)
  const html = `
    <!DOCTYPE html>
    <html lang="ja">
    <head>
      <meta charset="ShiftJIS">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>${TITLE}</title>
    </head>
    <body>
      ${body}
    </body>
    </html>
  `
  const utf8Buffer = Buffer.from(html)
  const utf8Html = decode(utf8Buffer, 'utf8')
  const shiftJISBuffer = encode(utf8Html, 'Shift_JIS')
  const shiftJISHtml = decode(shiftJISBuffer, 'Shift_JIS')

  const utf8Array = utf8Html.split('')
  const convertedHtml = shiftJISHtml
    .split('')
    .map((char, index) => {
      const utf8Char = utf8Array[index]
      if (utf8Char === '?') {
        return char
      } else if (char === '?') {
        const characterReference = `&#x${utf8Char
          .codePointAt(0)
          ?.toString(16)};`
        return characterReference
      }
      return char
    })
    .join('')

  await fs.writeFile('./output.html', encode(convertedHtml, 'Shift_JIS'))
})()

まとめ

  • renderToStaticMarkupでJSXのSSGは簡単に実現できる
  • 文字コードの変換はエンコーディングコンバーターを使う node.js の場合はiconv-lite が利用できる
  • 変換に失敗した場合も文字参照に変換すればなんとかカバーは可能

jamstack を実現するためにShiftJIS環境でのサイトジェネレーターについて解説してみました。
しかし、このような努力は穴の空いたバケツに水を汲むようなものなので、 脱ShiftJIS に心を燃やした方が長期的には生産性が高いと考えます。一般的でない規格や技術を用いるとどの道芋づる式にさまざまな問題を生じるためですね。万が一 ShiftJIS の環境で jamstack な静的サイトジェネレーターを実装したい場合に本稿がお役に立てば幸いです。

参考

脚注
  1. next.js などを用いるとどうしてもid="__next"などのライブラリ独自のマークアップが含まれてしまうため ↩︎

  2. ほかのEJS,Pug,Hamlなどのテンプレートエンジンと比べると ↩︎

  3. ちなみに https://w3techs.com/technologies/overview/javascript_libraryhttps://w3techs.com/technologies/overview/javascript_library によると React が用いられたサイトは世界の3%というデータがあります ↩︎

Discussion