はじめに

TEI (Text Encoding Initiative) に準拠したXMLデータをXSLTでHTMLに変換し、Webで公開する構成は、デジタルヒューマニティーズの分野で広く使われています。

従来、ブラウザのクライアントサイドXSLT変換(<?xml-stylesheet?>やJavaScriptによるXSLTProcessor)でXMLを表示するケースが多いですが、この方式にはいくつかの課題があります。

  • ページを開くたびにブラウザがXSLT変換を実行するため、表示が遅い
  • SEO・クローラー対応が難しい
  • ブラウザごとのXSLT実装差異

本記事では、Vercelのビルド時にXML→HTML変換を自動実行し、生成済みHTMLを静的配信する方法を紹介します。

構成

project/
├── docs/              # Vercelの出力ディレクトリ
│   ├── index.html     # トップページ
│   └── data/
│       ├── main.xsl       # XSLTスタイルシート
│       ├── main.sef.json  # コンパイル済みスタイルシート
│       ├── source.xml     # TEI/XMLソース
│       └── source.html    # 生成されるHTML(ビルド時生成)
├── build.js           # ビルドスクリプト
├── package.json
└── vercel.json

Node.jsでのXSLT処理ライブラリ比較

Vercelのビルド環境ではネイティブツール(xsltproc等)が使えないため、Node.jsのXSLTライブラリを使う必要があります。以下の3つを検証しました。

xsltproc(参考:ローカル環境)

macOSに標準搭載されているC言語実装のXSLTプロセッサです。

xsltproc docs/data/main.xsl docs/data/source.xml > docs/data/source.html

一瞬で完了しますが、Vercelのビルド環境では利用できません(apt-getが使えない)。

xslt-processor(純JavaScript実装)

npm install xslt-processor

GoogleのAJAXSLT(2005年)をベースにES2015+に更新したライブラリです。ブラウザにXSLTサポートがなかった時代のpolyfillが起源であり、1400行程度のXMLファイルの変換でも数分以上かかり、実用に堪えませんでした。

遅い理由は以下の通りです。

  • XPath式を実行時に毎回パース・評価する(キャッシュや事前コンパイルの仕組みがない)
  • 最適化戦略がない設計のため、XPath評価が累積的に重くなる
  • 純JavaScriptのDOM実装による木構造走査のオーバーヘッド

saxon-js(Saxonica社製)

npm install saxon-js xslt3

XSLT 3.0仕様の編集者であるMichael Kayが率いるSaxonica社が開発した高性能XSLTプロセッサです。最大の特徴は事前コンパイル方式にあります。

  1. XSLTスタイルシートをSEF(Stylesheet Export File)というJSON形式にコンパイル
  2. 実行時はコンパイル済みのSEFを読み込んで処理するだけ
# XSLTをSEFにコンパイル(初回のみ)
npx xslt3 -xsl:docs/data/main.xsl -export:docs/data/main.sef.json -nogo

同じファイルの変換が0.5秒で完了しました。

比較結果

ツール実装方式処理時間Vercel対応
xsltprocC/ネイティブ<0.1秒不可
saxon-js事前コンパイル+JS実行0.5秒
xslt-processor純JS実行時処理数分以上可(実用不可)

実装

build.js

const fs = require('fs');
const path = require('path');
const SaxonJS = require('saxon-js');

const dataDir = path.join(__dirname, 'docs', 'data');
const sefPath = path.join(dataDir, 'main.sef.json');
const sefText = fs.readFileSync(sefPath, 'utf-8');
const sef = JSON.parse(sefText);

const xmlFiles = fs.readdirSync(dataDir).filter(f => f.endsWith('.xml'));

for (const file of xmlFiles) {
  const xmlPath = path.join(dataDir, file);
  const htmlPath = xmlPath.replace(/\.xml$/, '.html');
  const xmlText = fs.readFileSync(xmlPath, 'utf-8');
  console.log(`Processing: ${file}...`);
  const result = SaxonJS.transform({
    stylesheetInternal: sef,
    sourceText: xmlText,
    destination: 'serialized'
  });
  fs.writeFileSync(htmlPath, result.principalResult, 'utf-8');
  console.log(`Built: ${file} -> ${file.replace(/\.xml$/, '.html')}`);
}

package.json

{
  "scripts": {
    "build": "node build.js"
  },
  "dependencies": {
    "saxon-js": "^2.7.0"
  },
  "devDependencies": {
    "xslt3": "^2.7.0"
  }
}

vercel.json

{
  "buildCommand": "node build.js",
  "outputDirectory": "docs"
}

Vercelプロジェクト設定

Vercelのダッシュボードまたはvercel project inspectで以下を確認します。

  • Root Directory: プロジェクトルート(docsではなく、リポジトリのルート)
  • Build Command: node build.js(vercel.jsonで指定済み)
  • Output Directory: docs(vercel.jsonで指定済み)

Root Directoryがサブディレクトリ(例: docs)になっている場合、build.jspackage.jsonが見つからずビルドが失敗するため、プロジェクトルートに変更する必要があります。

XSLTスタイルシート変更時のワークフロー

XSLTスタイルシート(main.xsl)を変更した場合は、SEFの再コンパイルが必要です。

# SEFを再コンパイル
npx xslt3 -xsl:docs/data/main.xsl -export:docs/data/main.sef.json -nogo

# ローカルで動作確認
node build.js

# コミット&プッシュ(Vercelが自動デプロイ)
git add -A && git commit -m "update" && git push

XMLデータのみの変更であれば、SEFの再コンパイルは不要で、プッシュするだけでVercelがビルド時にHTMLを生成します。

まとめ

  • TEI/XMLのXSLT変換をクライアントサイドからビルド時に移行することで、表示速度とSEO対応を改善できる
  • Vercelのビルド環境ではネイティブツール(xsltproc)が使えないが、saxon-jsを使えば高速に変換できる
  • xslt-processorは純JavaScript実装のため大きなXMLでは実用的な速度が出ない
  • saxon-jsの事前コンパイル(SEF)方式により、ビルド時間は1秒未満に抑えられる