はじめに
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プロセッサです。最大の特徴は事前コンパイル方式にあります。
- XSLTスタイルシートをSEF(Stylesheet Export File)というJSON形式にコンパイル
- 実行時はコンパイル済みのSEFを読み込んで処理するだけ
# XSLTをSEFにコンパイル(初回のみ)
npx xslt3 -xsl:docs/data/main.xsl -export:docs/data/main.sef.json -nogo
同じファイルの変換が0.5秒で完了しました。
比較結果
| ツール | 実装方式 | 処理時間 | Vercel対応 |
|---|---|---|---|
| xsltproc | C/ネイティブ | <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.jsやpackage.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秒未満に抑えられる