《朝中措》
唇邊鳳語,挹來便作,銀磬微鳴。
字底分明芒刺,唇間猶護餘聲。
-
+
朝中措の調べ
羽の金を梳いて、ほのかな香りの花にする。香りは鏡の中に届いて止まる。
欄干の前、影を伴侶に、並びたいと願う。一筋の朱い髪が先に絡まる。
diff --git a/ja/poetry/fengrusong-20181102/index.html b/ja/poetry/fengrusong-20181102/index.html
index 3df2d83..e111299 100644
--- a/ja/poetry/fengrusong-20181102/index.html
+++ b/ja/poetry/fengrusong-20181102/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -127,7 +127,7 @@
《風入松》
舊山橫影亂爐光,燼處餘香。
筆枯化紙容人念,料經年、應易遷忘。
芍藥更添紅雨,玉筋暗觸龍章。
-
+
風入松の調べ
丘の亭では雪が降ろうとし、霜はすでに結ぶ。それでも頑なな陽は残る。
斜陽は煉瓦に落ちてなお去りがたく、金の炉を焚き、長い夜、君を待つ。
diff --git a/ja/poetry/jia-yi/index.html b/ja/poetry/jia-yi/index.html
index 67c334b..334e39b 100644
--- a/ja/poetry/jia-yi/index.html
+++ b/ja/poetry/jia-yi/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -146,7 +146,7 @@
-不語各含無限思,而今惟道相期。 夜闌暗忘漏聲移。 慵來還指日,明日又今時。
+
A1 · 臨江仙の調べ · 学友と灯の下で夜を過ごし、明け方になって別れる
幾度、夜更けまで二人で黙って座ったことだろう。
ひとつきりの灯、その赤い涙がゆっくり垂れる。
@@ -182,7 +182,7 @@
-乙一《無題 · 臨江仙 客散非因誰獨在,他人皆自相依。 席旁偶坐共相棲。 漏殘晨露上,風過落花時。
+
B1 · 無題 · 臨江仙の調べ
ひっそりした廊下に月が明るく、人の気配がようやく静まった。
二人で花の小径を、ゆっくり歩く。
@@ -218,7 +218,7 @@
+A2 · 臨江仙の調べ · 酒のあと、頬に手を添えて階上へ上がり、夜更けに寄り添い、人に問われても避けなかった
深く酔い、支え合って並んで寄りかかった。
階上へ上がり、人には知られたくなかった。
@@ -254,7 +254,7 @@
-乙二《無題 · 踏莎行 欲喚還休,歡猶未省。 分明一水無舟艇。 殘灰散盡各歸時,餘釭獨照空庭靜。
+
B2 · 無題 · 踏莎行の調べ
香りの温かな一室。並ぶ二つの影の跡。
笑い声は簾のあいだの闇へ沈む。
@@ -288,7 +288,7 @@
-乙三《無題 · 臨江仙 不是東風先折取,恐花為我辭根。 新痕舊鑰莫輕論。 相逢原是客,客受主家恩。
+
B3 · 無題 · 臨江仙の調べ
蝋燭は暗く、こおろぎは冷え、簾の影は細い。
酔いの名残にはまだわずかな温もり。
@@ -327,7 +327,7 @@
-乙四《無題 · 渡江雲 謄復塗、深言怕誤,淺字偏安。 明朝銅色經他手,便認作、一諾窮年。 燈滅後、丁寧只是加餐。
+
B4 · 無題 · 渡江雲の調べ
明るい銅、新しく刻まれた歯。
封じようとして、手を止める。
@@ -369,7 +369,7 @@ 乙四《無題 · 渡江雲
乙四《無題 · 渡江雲
乙五《無題 · 十六字令》
銅。鑰齒新裁付掌中。天寒了,加飯且從容。
-
+
B5 · 無題 · 十六字令の調べ
銅。
歯を新しく刻んだ鍵を、あなたの掌へ。
@@ -391,7 +391,7 @@
+
A3 · 鷓鴣天の調べ · あなたから鍵を贈られ、数語の書きつけが添えられていた。灯の下で広げて読む
新しく刻まれた鍵の歯。私はそれを大切にする。
筆を取り上げる。まだ一字も書かぬうちから、もうためらう。
@@ -424,7 +424,7 @@
+
A4 · 臨江仙の調べ · 甲一を回想して。かつて学友と幾晩か黙って座った。別れてから便りはしだいに薄れ、長い時を経てようやく心を定めた
かつて、ひっそりした廊下で二人は黙って座った。
あのときは、ただありふれたことだと思っていた。
@@ -460,7 +460,7 @@
+
A5 · 臨江仙の調べ · 甲二を回想して。酒のあと、頬に手を添えて階上へ上がり、夜更けに人に問われ、初めて隠し立てをしなかった
あの夜、誰が客と主人を分けただろう。
肩を並べ、夜更けに寄りかかっていた。
@@ -495,7 +495,7 @@
+
A6 · 鷓鴣天の調べ · 甲三を回想して。箱の中の古い鍵は緑青に覆われていた。往復した手紙を思い返し、もの寂しく書く
小さな鍵。年を経て、錆は暗く広がった。
古い手紙を読み終えても、目はまだ冴えている。
@@ -528,7 +528,7 @@
+
A7 · 臨江仙の調べ · 蘭を株分けしてある人へ送る。かつて香草を贈られた時を思い、もの寂しく書く
手で古い蘭を分け、新しく植える。
あの春の温もりを、いまも覚えているだろうか。
@@ -577,7 +577,7 @@ 乙六《得某所寄》
-
+
B6 · 送られてきたものを受け取って
古い鉢に、移し植えた蘭。
春の痕が幾度かずれた。
@@ -653,7 +653,7 @@ 甲八《夜聞舊曲》
-
+
A8 · 夜、昔の曲を聞く
あのときは、窓の下で別れた。
いまは、軒先の雨になった。
@@ -696,7 +696,7 @@
+
B7 · 忘川、忘却の川 · 箱を探して古い草稿を見つけた。これ以上持ち歩かず、すべてある人に送り、この詩を添える
灯の下で、昔の詞を読む。
夜も深くなって、ようやく眠れた。
@@ -731,7 +731,7 @@
+
A9 · 鷓鴣天の調べ · 送られてきた古い草稿を受け取り、灯の下で読み終え、箱に残していたものと合わせて甲・乙として編んだ。綴じ上げてからは客が来るたび見せ、この末尾に記す
古びた紙束を二つ、机上にひろげる。
昔の灯影のもと、それぞれが一篇をなしていた。
@@ -766,7 +766,7 @@ 甲十《中
遲,是誤了;
歸,也是誤了。
-
+
A10 · 中呂 · 山坡羊の調べ · 旧居の前をまた通る
風が顔に当たる。ここで立ち止まる。
鼻先が先に動く。魂のほうが、私より先に着く。
diff --git a/ja/poetry/linjiangxian-20180108/index.html b/ja/poetry/linjiangxian-20180108/index.html
index 7732ec3..15fca1c 100644
--- a/ja/poetry/linjiangxian-20180108/index.html
+++ b/ja/poetry/linjiangxian-20180108/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -127,7 +127,7 @@ 《臨江仙》
閬苑蓬萊身已遠,青驄且繫書樓。
別期未至不題愁。
炭冰摧過後,吟捻憶君眸。
-
+
臨江仙の調べ
麹と麦芽、塩と梅に、古い調合法はない。塵が来ても、芳しい流れにはまだ波が立たない。
簾の前、去ろうとすれば、影が先に留まる。
diff --git a/ja/poetry/linjiangxian-20260909/index.html b/ja/poetry/linjiangxian-20260909/index.html
index 8812ddf..205015f 100644
--- a/ja/poetry/linjiangxian-20260909/index.html
+++ b/ja/poetry/linjiangxian-20260909/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -127,7 +127,7 @@ 《臨江仙》
他日重簾垂到地,垂來一似從前。
手臨燈畔數迴旋。
明朝風或緊,窗且掩周全。
-
+
臨江仙の調べ
簾の影、幾晩も続いた長い話。
降りはじめた露が欄干を濡らす。
diff --git a/ja/poetry/linjiangxianyin-20191028/index.html b/ja/poetry/linjiangxianyin-20191028/index.html
index f7d56e7..57f1bab 100644
--- a/ja/poetry/linjiangxianyin-20191028/index.html
+++ b/ja/poetry/linjiangxianyin-20191028/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -128,7 +128,7 @@ 《臨江仙引》
+
臨江仙引の調べ
囲いは裂け、宴は散る。鐘は夕べに垂れる。しばし、安らぎを盗みとる。
星の街に、あのよい歳月はもうない。
diff --git a/ja/poetry/manjianghong-2022/index.html b/ja/poetry/manjianghong-2022/index.html
index 1f006c6..bb124c7 100644
--- a/ja/poetry/manjianghong-2022/index.html
+++ b/ja/poetry/manjianghong-2022/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -134,7 +134,7 @@ 《滿江紅 ·
更何須揀擇,過門皆璧。
初景寒柯違業識,豐城元禮宜同席。
看收場、針腳又周全,無痕跡。
-
+
満江紅の調べ · 引き止めたくとも、こうなるとわかっている
古い裂け目を縫ったばかりなのに、春風はまた、しきりに客を引き止める。
扉は閉じようとし、かえって簾の影が翻り、春を忍び込ませる。
@@ -172,7 +172,7 @@ 《
縱衣冠屢換,總稱圭璧。
初景寒柯違業識,豐城元禮宜同席。
曉山塗,晝影祭寒華,猶無息。
-
+
満江紅の調べ · 引き止めたくとも、こうなるとわかっている · 別稿
水時計は尽き、鐘は返る。夜明けを促すところ、余韻はなお塞がれている。
扉は閉じようとし、粗末な襟は去ろうとする。それでもまた、よい客を迎える。
diff --git a/ja/poetry/manjianghong-rereading-202610/index.html b/ja/poetry/manjianghong-rereading-202610/index.html
index e288de9..d55cf8d 100644
--- a/ja/poetry/manjianghong-rereading-202610/index.html
+++ b/ja/poetry/manjianghong-rereading-202610/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -129,7 +129,7 @@
+
満江紅の調べ · 旧作を読み返し、年月を経て再び消息を交わしたことを思う
古い詞をまた開けば、まず、あの頃注いだ力がわかる。
かつて細かく解きほぐし、わずかな言葉を千たび転じた。息はかすか、声は渋い。
diff --git a/ja/poetry/mantingfang-20191219/index.html b/ja/poetry/mantingfang-20191219/index.html
index 504e473..7707cd4 100644
--- a/ja/poetry/mantingfang-20191219/index.html
+++ b/ja/poetry/mantingfang-20191219/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -129,7 +129,7 @@ 《滿庭芳 · 遙思
把千里關山,盡斂襟前。
幾月光陰忽薄,回眸際、指過眉山。
從頭認,詩還半闋,雙影壓箋邊。
-
+
満庭芳の調べ · 遥かに楚潭を思う
風の佩玉がいま静まり、雨が朝の木槿を梳く。文は先に眉の弧を知っていた。
ひと声、低く呼ぶ。夢の弦が驚いて散る。
diff --git a/ja/poetry/queqiaoxian-20181222/index.html b/ja/poetry/queqiaoxian-20181222/index.html
index cb41f1d..7520695 100644
--- a/ja/poetry/queqiaoxian-20181222/index.html
+++ b/ja/poetry/queqiaoxian-20181222/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -125,7 +125,7 @@ 《鵲橋仙》
聞人道好,隨人道好,瓦釜雷鳴何故。
逢君仍問近來安,說罷了、回身讓路。
-
+
鵲橋仙の調べ
手紙は要らない。古い品は大切にしている。紙にはありふれた言葉ばかり。
柔らかな手の傷を、思いがけず唇が濡らした。幾年を経て、なおどれほど残るのか。
diff --git a/ja/poetry/september-2026/index.html b/ja/poetry/september-2026/index.html
index 9b8408a..1e846cc 100644
--- a/ja/poetry/september-2026/index.html
+++ b/ja/poetry/september-2026/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -129,7 +129,7 @@ 《鷓鴣
言漸杳,露初零。獨餘一語未分明。
頻頻蘸得毫端黑,墨至濃時偏不成。
-
+
鷓鴣天の調べ · 幾晩か灯の下で書こうとした、未完
幾晩か、簾の前で一緒に灯芯を切った。
一生ぶんの話をして、また三更まで来た。
@@ -161,7 +161,7 @@ 《臨江仙》
他日重簾垂到地,垂來一似從前。
手臨燈畔數迴旋。
明朝風或緊,窗且掩周全。
-
+
臨江仙の調べ
簾の影、幾晩も続いた長い話。
降りはじめた露が欄干を濡らす。
diff --git a/ja/poetry/yingtianchang-20261008/index.html b/ja/poetry/yingtianchang-20261008/index.html
index c5a6bde..aa4f1ad 100644
--- a/ja/poetry/yingtianchang-20261008/index.html
+++ b/ja/poetry/yingtianchang-20261008/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -129,7 +129,7 @@ 《應天長·喚回》
-
+
應天長の調べ · 呼び戻す
呼び戻してなお、誰も着ていない衣を整える。
袖口の余温は指に残る。
diff --git a/ja/poetry/zhegutian-20260909/index.html b/ja/poetry/zhegutian-20260909/index.html
index 5a475ad..05a7acb 100644
--- a/ja/poetry/zhegutian-20260909/index.html
+++ b/ja/poetry/zhegutian-20260909/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -125,7 +125,7 @@ 《鷓鴣
言漸杳,露初零。獨餘一語未分明。
頻頻蘸得毫端黑,墨至濃時偏不成。
-
+
鷓鴣天の調べ · 幾晩か灯の下で書こうとした、未完
幾晩か、簾の前で一緒に灯芯を切った。
一生ぶんの話をして、また三更まで来た。
diff --git a/package.json b/package.json
index a5e2d67..00808ad 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
"qa:first-love": "python3 tools/firstlovecheck.py",
"qa:voucher": "node tools/vouchertypecheck.mjs",
"qa:404": "node tools/notfoundcheck.mjs",
- "qa": "npm run qa:poetry-library && npm run qa:poetry-kunten-manuscript && npm run qa:poetry-kunten-reader && npm run qa:poetry-kunten-lines && npm run qa:poetry-layout && npm run qa:poetry-kunten && npm run qa:poetry-kunten-multiline && npm run qa:poetry-kunten-zoom && npm run qa:summer-translations && npm run qa:html && npm run qa:first-love && npm run qa:voucher && npm run qa:order-core && npm run qa:shop && npm run qa:pv-configurator && npm run qa:pv-edition && npm run qa:shop-locales && npm run qa:poetry-migration && npm run qa:author-shop && npm run qa:h10-print && npm run qa:tuner && npm run qa:browser && npm run qa:a11y-functional && npm run qa:editorial && npm run qa:404 && npm run qa:about && npm run qa:fonts && npm run qa:wrap",
+ "qa": "npm run qa:poetry-library && npm run qa:poetry-kunten-manuscript && npm run qa:poetry-kunten-reader && npm run qa:poetry-kunten-lines && npm run qa:poetry-layout && npm run qa:poetry-kunten && npm run qa:poetry-kunten-recovery && npm run qa:poetry-kunten-multiline && npm run qa:poetry-kunten-zoom && npm run qa:summer-translations && npm run qa:html && npm run qa:first-love && npm run qa:voucher && npm run qa:order-core && npm run qa:shop && npm run qa:pv-configurator && npm run qa:pv-edition && npm run qa:shop-locales && npm run qa:poetry-migration && npm run qa:author-shop && npm run qa:h10-print && npm run qa:tuner && npm run qa:browser && npm run qa:a11y-functional && npm run qa:editorial && npm run qa:404 && npm run qa:about && npm run qa:fonts && npm run qa:wrap",
"qa:editorial": "node tools/editorialcheck.mjs",
"qa:shop": "node tools/shopcheck.mjs",
"qa:pv-configurator": "node tools/pv-configuratorcheck.mjs",
@@ -33,6 +33,7 @@
"qa:poetry-kunten-reader": "python3 tools/poetrykuntenreadercheck.py",
"qa:poetry-kunten-lines": "python3 tools/poetrykuntenlinecheck.py",
"qa:poetry-kunten": "node tools/poetrykuntencheck.mjs",
+ "qa:poetry-kunten-recovery": "node tools/poetrykuntenrecoverycheck.mjs",
"qa:poetry-kunten-multiline": "node tools/poetrykuntenmultilinecheck.mjs",
"qa:poetry-kunten-zoom": "node tools/poetrykuntenzoomcheck.mjs"
},
diff --git a/tools/poetry_pages.py b/tools/poetry_pages.py
index 4a999c1..4c16055 100644
--- a/tools/poetry_pages.py
+++ b/tools/poetry_pages.py
@@ -1,6 +1,7 @@
"""Static catalogue, chronology, collection readers and independent work pages."""
from __future__ import annotations
+import hashlib
import html
import json
import re
@@ -38,6 +39,10 @@ def __init__(self, base: Any):
self.ui = read(base.CONTENT / 'poetry/ui.json')
self.template = (base.TEMPLATES / 'poetry.html').read_text(encoding='utf-8')
+ def fingerprinted(self, name: str) -> str:
+ data = (self.base.ROOT / 'assets' / name).read_bytes()
+ return f'/assets/{name}?v={hashlib.sha256(data).hexdigest()[:12]}'
+
def languages(self, route: str, locale: str) -> str:
result = []
for language in self.base.LANGUAGES:
@@ -71,8 +76,10 @@ def shell(self, *, route: str, locale: str, copy: dict[str, Any], heading: str,
social = b.social_meta_html(locale_id=locale,title=title,description=description,url=canonical,
image_url=image,image_alt=title,width=1200,height=630,image_type='image/png',og_type='article')
if locale == 'ja' and 'data-poetry-kanbun-viewer=' in content:
- extra_head += (''
- '')
+ # Fingerprinted like the frames it drives: an installed iOS web app
+ # must not pair a fresh frame with a stale controller.
+ extra_head += (f''
+ f'')
values = {
'HTML_LANG':b.LANG_BY_ID[locale]['html_lang'], 'LOCALE':locale,
'POETRY_TITLE':esc(title), 'POETRY_DESCRIPTION':esc(description),
diff --git a/tools/poetrykuntenrecoverycheck.mjs b/tools/poetrykuntenrecoverycheck.mjs
new file mode 100644
index 0000000..ed40a26
--- /dev/null
+++ b/tools/poetrykuntenrecoverycheck.mjs
@@ -0,0 +1,107 @@
+#!/usr/bin/env node
+// The kanbun reader recovers when its frame loads badly: a missing
+// typesetter (the frame reports an error), a missing viewer (the frame never
+// answers), or a ready report that never reaches the host. Each must end with
+// the visual reader shown, not the plain vertical fallback.
+import assert from 'node:assert/strict';
+import {createServer} from 'node:http';
+import {readFile} from 'node:fs/promises';
+import path from 'node:path';
+import {chromium, webkit} from 'playwright';
+
+const root = process.cwd();
+const page_url = '/ja/poetry/linjiangxian-20260909/';
+const id = 'linjiangxian-20260909-text';
+const mime = {'.html':'text/html; charset=utf-8','.js':'text/javascript','.css':'text/css',
+ '.woff2':'font/woff2','.json':'application/json','.png':'image/png'};
+const server = createServer(async(req,res) => {
+ try {
+ let u = decodeURIComponent(new URL(req.url,'http://localhost').pathname);
+ if (u.endsWith('/')) u += 'index.html';
+ const file = path.resolve(root,'.'+u);
+ if (!file.startsWith(root + path.sep)) throw Error('Path outside project');
+ res.writeHead(200,{'Content-Type':mime[path.extname(file)]||'application/octet-stream'});
+ res.end(await readFile(file));
+ } catch { res.writeHead(404);res.end('Not found'); }
+});
+await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
+const base = 'http://127.0.0.1:' + server.address().port;
+
+// Fail only the first request for a viewer asset; later reloads succeed.
+async function failFirst(context, pattern) {
+ let failed = 0;
+ await context.route(pattern, route => {
+ if (failed++ === 0) return route.abort();
+ return route.continue();
+ });
+}
+
+const scenarios = {
+ 'typesetter missing on first load': context => failFirst(context, /kanbun-render\.js/),
+ 'viewer missing on first load': context => failFirst(context, /kanbun-viewer\/viewer\.js/),
+ 'first ready report lost': async context => {
+ // Swallow the frame's first ready message before the host sees it.
+ await context.addInitScript(() => {
+ if (window.top !== window) return;
+ let dropped = false;
+ addEventListener('message', event => {
+ if (!dropped && event.data?.type === 'hanpu-kanbun-viewer-v1' && event.data.state === 'ready') {
+ dropped = true;
+ event.stopImmediatePropagation();
+ }
+ }, true);
+ });
+ },
+};
+
+let count = 0;
+try {
+ for (const [name,engine] of [['chromium',chromium],['webkit',webkit]]) {
+ const browser = await engine.launch();
+ try {
+ for (const [label, inject] of Object.entries(scenarios)) {
+ const context = await browser.newContext({viewport:{width:390,height:844}});
+ await inject(context);
+ const page = await context.newPage();
+ const errors = [];
+ page.on('pageerror', e=>errors.push(e.message));
+ await page.goto(base+page_url+'?layout=vertical&kunten=on',{waitUntil:'load'});
+ const stage = page.locator(`[data-poetry-kanbun-viewer="${id}"]`);
+ await page.locator(`[data-poetry-kanbun-viewer="${id}"][data-kanbun-ready="true"]`)
+ .waitFor({state:'visible',timeout:20000})
+ .catch(() => { throw Error(`${name}: ${label}: reader never became ready`); });
+ assert.ok(await page.locator('.poem-version.source').first()
+ .evaluate(el=>el.classList.contains('poetry-kanbun-showing')), `${name}: ${label}: source not replaced`);
+ const glyphs = await stage.locator('iframe[data-kanbun-frame]').contentFrame()
+ .locator('#kanbun-display .kaeriten').count();
+ assert.ok(glyphs > 0, `${name}: ${label}: no return marks rendered`);
+ assert.deepEqual(errors, [], `${name}: ${label}: page errors`);
+ count++;
+ await context.close();
+ }
+
+ // A reader stuck before ready restarts when the reader chooses
+ // 縦書き+訓点 again: block the frame entirely, then let it through.
+ const context = await browser.newContext({viewport:{width:390,height:844}});
+ let blocked = true;
+ await context.route(/kanbun-viewer\/works\//, route => blocked ? route.abort() : route.continue());
+ const page = await context.newPage();
+ await page.goto(base+page_url+'?layout=vertical&kunten=on',{waitUntil:'load'});
+ // Exhaust the automatic attempts (3 pings + 2 reloads at 1.5s).
+ await page.waitForTimeout(14000);
+ const stage = page.locator(`[data-poetry-kanbun-viewer="${id}"]`);
+ assert.notEqual(await stage.getAttribute('data-kanbun-ready'),'true',`${name}: blocked frame reported ready`);
+ assert.ok(await page.locator('.poem-version.source .body').first().isVisible(),
+ `${name}: plain source must remain while the reader is unavailable`);
+ blocked = false;
+ await page.locator('[data-poetry-kunten-choice="off"]').click();
+ await page.locator('[data-poetry-kunten-choice="on"]').click();
+ await page.locator(`[data-poetry-kanbun-viewer="${id}"][data-kanbun-ready="true"]`)
+ .waitFor({state:'visible',timeout:20000})
+ .catch(() => { throw Error(`${name}: layout choice did not retry the reader`); });
+ count++;
+ await context.close();
+ } finally { await browser.close(); }
+ }
+} finally { server.close(); }
+console.log(`kanbun recovery: ${count} scenarios passed`);
乙五《無題 · 十六字令》
銅。鑰齒新裁付掌中。天寒了,加飯且從容。
-
+
B5 · 無題 · 十六字令の調べ
銅。
歯を新しく刻んだ鍵を、あなたの掌へ。
@@ -391,7 +391,7 @@
+A3 · 鷓鴣天の調べ · あなたから鍵を贈られ、数語の書きつけが添えられていた。灯の下で広げて読む
新しく刻まれた鍵の歯。私はそれを大切にする。
筆を取り上げる。まだ一字も書かぬうちから、もうためらう。
@@ -424,7 +424,7 @@
+A4 · 臨江仙の調べ · 甲一を回想して。かつて学友と幾晩か黙って座った。別れてから便りはしだいに薄れ、長い時を経てようやく心を定めた
かつて、ひっそりした廊下で二人は黙って座った。
あのときは、ただありふれたことだと思っていた。
@@ -460,7 +460,7 @@
+A5 · 臨江仙の調べ · 甲二を回想して。酒のあと、頬に手を添えて階上へ上がり、夜更けに人に問われ、初めて隠し立てをしなかった
あの夜、誰が客と主人を分けただろう。
肩を並べ、夜更けに寄りかかっていた。
@@ -495,7 +495,7 @@
+A6 · 鷓鴣天の調べ · 甲三を回想して。箱の中の古い鍵は緑青に覆われていた。往復した手紙を思い返し、もの寂しく書く
小さな鍵。年を経て、錆は暗く広がった。
古い手紙を読み終えても、目はまだ冴えている。
@@ -528,7 +528,7 @@
+A7 · 臨江仙の調べ · 蘭を株分けしてある人へ送る。かつて香草を贈られた時を思い、もの寂しく書く
手で古い蘭を分け、新しく植える。
あの春の温もりを、いまも覚えているだろうか。
@@ -577,7 +577,7 @@
乙六《得某所寄》
-
+
B6 · 送られてきたものを受け取って
古い鉢に、移し植えた蘭。
春の痕が幾度かずれた。
@@ -653,7 +653,7 @@
甲八《夜聞舊曲》
-
+
A8 · 夜、昔の曲を聞く
あのときは、窓の下で別れた。
いまは、軒先の雨になった。
@@ -696,7 +696,7 @@
+B7 · 忘川、忘却の川 · 箱を探して古い草稿を見つけた。これ以上持ち歩かず、すべてある人に送り、この詩を添える
灯の下で、昔の詞を読む。
夜も深くなって、ようやく眠れた。
@@ -731,7 +731,7 @@
+A9 · 鷓鴣天の調べ · 送られてきた古い草稿を受け取り、灯の下で読み終え、箱に残していたものと合わせて甲・乙として編んだ。綴じ上げてからは客が来るたび見せ、この末尾に記す
古びた紙束を二つ、机上にひろげる。
昔の灯影のもと、それぞれが一篇をなしていた。
@@ -766,7 +766,7 @@
-甲十《中 遲,是誤了; 歸,也是誤了。
+
A10 · 中呂 · 山坡羊の調べ · 旧居の前をまた通る
風が顔に当たる。ここで立ち止まる。
鼻先が先に動く。魂のほうが、私より先に着く。
diff --git a/ja/poetry/linjiangxian-20180108/index.html b/ja/poetry/linjiangxian-20180108/index.html
index 7732ec3..15fca1c 100644
--- a/ja/poetry/linjiangxian-20180108/index.html
+++ b/ja/poetry/linjiangxian-20180108/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -127,7 +127,7 @@
《臨江仙》
閬苑蓬萊身已遠,青驄且繫書樓。
別期未至不題愁。
炭冰摧過後,吟捻憶君眸。
-
+
臨江仙の調べ
麹と麦芽、塩と梅に、古い調合法はない。塵が来ても、芳しい流れにはまだ波が立たない。
簾の前、去ろうとすれば、影が先に留まる。
diff --git a/ja/poetry/linjiangxian-20260909/index.html b/ja/poetry/linjiangxian-20260909/index.html
index 8812ddf..205015f 100644
--- a/ja/poetry/linjiangxian-20260909/index.html
+++ b/ja/poetry/linjiangxian-20260909/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -127,7 +127,7 @@
《臨江仙》
他日重簾垂到地,垂來一似從前。
手臨燈畔數迴旋。
明朝風或緊,窗且掩周全。
-
+
臨江仙の調べ
簾の影、幾晩も続いた長い話。
降りはじめた露が欄干を濡らす。
diff --git a/ja/poetry/linjiangxianyin-20191028/index.html b/ja/poetry/linjiangxianyin-20191028/index.html
index f7d56e7..57f1bab 100644
--- a/ja/poetry/linjiangxianyin-20191028/index.html
+++ b/ja/poetry/linjiangxianyin-20191028/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -128,7 +128,7 @@
《臨江仙引》
+
臨江仙引の調べ
囲いは裂け、宴は散る。鐘は夕べに垂れる。しばし、安らぎを盗みとる。
星の街に、あのよい歳月はもうない。
diff --git a/ja/poetry/manjianghong-2022/index.html b/ja/poetry/manjianghong-2022/index.html
index 1f006c6..bb124c7 100644
--- a/ja/poetry/manjianghong-2022/index.html
+++ b/ja/poetry/manjianghong-2022/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -134,7 +134,7 @@
-《滿江紅 · 更何須揀擇,過門皆璧。 初景寒柯違業識,豐城元禮宜同席。 看收場、針腳又周全,無痕跡。
+
満江紅の調べ · 引き止めたくとも、こうなるとわかっている
古い裂け目を縫ったばかりなのに、春風はまた、しきりに客を引き止める。
扉は閉じようとし、かえって簾の影が翻り、春を忍び込ませる。
@@ -172,7 +172,7 @@
-《 縱衣冠屢換,總稱圭璧。 初景寒柯違業識,豐城元禮宜同席。 曉山塗,晝影祭寒華,猶無息。
+
満江紅の調べ · 引き止めたくとも、こうなるとわかっている · 別稿
水時計は尽き、鐘は返る。夜明けを促すところ、余韻はなお塞がれている。
扉は閉じようとし、粗末な襟は去ろうとする。それでもまた、よい客を迎える。
diff --git a/ja/poetry/manjianghong-rereading-202610/index.html b/ja/poetry/manjianghong-rereading-202610/index.html
index e288de9..d55cf8d 100644
--- a/ja/poetry/manjianghong-rereading-202610/index.html
+++ b/ja/poetry/manjianghong-rereading-202610/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -129,7 +129,7 @@
+満江紅の調べ · 旧作を読み返し、年月を経て再び消息を交わしたことを思う
古い詞をまた開けば、まず、あの頃注いだ力がわかる。
かつて細かく解きほぐし、わずかな言葉を千たび転じた。息はかすか、声は渋い。
diff --git a/ja/poetry/mantingfang-20191219/index.html b/ja/poetry/mantingfang-20191219/index.html
index 504e473..7707cd4 100644
--- a/ja/poetry/mantingfang-20191219/index.html
+++ b/ja/poetry/mantingfang-20191219/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -129,7 +129,7 @@
-《滿庭芳 · 遙思 把千里關山,盡斂襟前。 幾月光陰忽薄,回眸際、指過眉山。 從頭認,詩還半闋,雙影壓箋邊。
+
満庭芳の調べ · 遥かに楚潭を思う
風の佩玉がいま静まり、雨が朝の木槿を梳く。文は先に眉の弧を知っていた。
ひと声、低く呼ぶ。夢の弦が驚いて散る。
diff --git a/ja/poetry/queqiaoxian-20181222/index.html b/ja/poetry/queqiaoxian-20181222/index.html
index cb41f1d..7520695 100644
--- a/ja/poetry/queqiaoxian-20181222/index.html
+++ b/ja/poetry/queqiaoxian-20181222/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -125,7 +125,7 @@
《鵲橋仙》
聞人道好,隨人道好,瓦釜雷鳴何故。
逢君仍問近來安,說罷了、回身讓路。
-
+
鵲橋仙の調べ
手紙は要らない。古い品は大切にしている。紙にはありふれた言葉ばかり。
柔らかな手の傷を、思いがけず唇が濡らした。幾年を経て、なおどれほど残るのか。
diff --git a/ja/poetry/september-2026/index.html b/ja/poetry/september-2026/index.html
index 9b8408a..1e846cc 100644
--- a/ja/poetry/september-2026/index.html
+++ b/ja/poetry/september-2026/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -129,7 +129,7 @@
-《鷓鴣 言漸杳,露初零。獨餘一語未分明。 頻頻蘸得毫端黑,墨至濃時偏不成。
+
鷓鴣天の調べ · 幾晩か灯の下で書こうとした、未完
幾晩か、簾の前で一緒に灯芯を切った。
一生ぶんの話をして、また三更まで来た。
@@ -161,7 +161,7 @@
《臨江仙》
他日重簾垂到地,垂來一似從前。
手臨燈畔數迴旋。
明朝風或緊,窗且掩周全。
-
+
臨江仙の調べ
簾の影、幾晩も続いた長い話。
降りはじめた露が欄干を濡らす。
diff --git a/ja/poetry/yingtianchang-20261008/index.html b/ja/poetry/yingtianchang-20261008/index.html
index c5a6bde..aa4f1ad 100644
--- a/ja/poetry/yingtianchang-20261008/index.html
+++ b/ja/poetry/yingtianchang-20261008/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -129,7 +129,7 @@
《應天長·喚回》
-
+
應天長の調べ · 呼び戻す
呼び戻してなお、誰も着ていない衣を整える。
袖口の余温は指に残る。
diff --git a/ja/poetry/zhegutian-20260909/index.html b/ja/poetry/zhegutian-20260909/index.html
index 5a475ad..05a7acb 100644
--- a/ja/poetry/zhegutian-20260909/index.html
+++ b/ja/poetry/zhegutian-20260909/index.html
@@ -67,7 +67,7 @@
-
+
詞へスキップ
@@ -125,7 +125,7 @@
-《鷓鴣 言漸杳,露初零。獨餘一語未分明。 頻頻蘸得毫端黑,墨至濃時偏不成。
+
鷓鴣天の調べ · 幾晩か灯の下で書こうとした、未完
幾晩か、簾の前で一緒に灯芯を切った。
一生ぶんの話をして、また三更まで来た。
diff --git a/package.json b/package.json
index a5e2d67..00808ad 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
"qa:first-love": "python3 tools/firstlovecheck.py",
"qa:voucher": "node tools/vouchertypecheck.mjs",
"qa:404": "node tools/notfoundcheck.mjs",
- "qa": "npm run qa:poetry-library && npm run qa:poetry-kunten-manuscript && npm run qa:poetry-kunten-reader && npm run qa:poetry-kunten-lines && npm run qa:poetry-layout && npm run qa:poetry-kunten && npm run qa:poetry-kunten-multiline && npm run qa:poetry-kunten-zoom && npm run qa:summer-translations && npm run qa:html && npm run qa:first-love && npm run qa:voucher && npm run qa:order-core && npm run qa:shop && npm run qa:pv-configurator && npm run qa:pv-edition && npm run qa:shop-locales && npm run qa:poetry-migration && npm run qa:author-shop && npm run qa:h10-print && npm run qa:tuner && npm run qa:browser && npm run qa:a11y-functional && npm run qa:editorial && npm run qa:404 && npm run qa:about && npm run qa:fonts && npm run qa:wrap",
+ "qa": "npm run qa:poetry-library && npm run qa:poetry-kunten-manuscript && npm run qa:poetry-kunten-reader && npm run qa:poetry-kunten-lines && npm run qa:poetry-layout && npm run qa:poetry-kunten && npm run qa:poetry-kunten-recovery && npm run qa:poetry-kunten-multiline && npm run qa:poetry-kunten-zoom && npm run qa:summer-translations && npm run qa:html && npm run qa:first-love && npm run qa:voucher && npm run qa:order-core && npm run qa:shop && npm run qa:pv-configurator && npm run qa:pv-edition && npm run qa:shop-locales && npm run qa:poetry-migration && npm run qa:author-shop && npm run qa:h10-print && npm run qa:tuner && npm run qa:browser && npm run qa:a11y-functional && npm run qa:editorial && npm run qa:404 && npm run qa:about && npm run qa:fonts && npm run qa:wrap",
"qa:editorial": "node tools/editorialcheck.mjs",
"qa:shop": "node tools/shopcheck.mjs",
"qa:pv-configurator": "node tools/pv-configuratorcheck.mjs",
@@ -33,6 +33,7 @@
"qa:poetry-kunten-reader": "python3 tools/poetrykuntenreadercheck.py",
"qa:poetry-kunten-lines": "python3 tools/poetrykuntenlinecheck.py",
"qa:poetry-kunten": "node tools/poetrykuntencheck.mjs",
+ "qa:poetry-kunten-recovery": "node tools/poetrykuntenrecoverycheck.mjs",
"qa:poetry-kunten-multiline": "node tools/poetrykuntenmultilinecheck.mjs",
"qa:poetry-kunten-zoom": "node tools/poetrykuntenzoomcheck.mjs"
},
diff --git a/tools/poetry_pages.py b/tools/poetry_pages.py
index 4a999c1..4c16055 100644
--- a/tools/poetry_pages.py
+++ b/tools/poetry_pages.py
@@ -1,6 +1,7 @@
"""Static catalogue, chronology, collection readers and independent work pages."""
from __future__ import annotations
+import hashlib
import html
import json
import re
@@ -38,6 +39,10 @@ def __init__(self, base: Any):
self.ui = read(base.CONTENT / 'poetry/ui.json')
self.template = (base.TEMPLATES / 'poetry.html').read_text(encoding='utf-8')
+ def fingerprinted(self, name: str) -> str:
+ data = (self.base.ROOT / 'assets' / name).read_bytes()
+ return f'/assets/{name}?v={hashlib.sha256(data).hexdigest()[:12]}'
+
def languages(self, route: str, locale: str) -> str:
result = []
for language in self.base.LANGUAGES:
@@ -71,8 +76,10 @@ def shell(self, *, route: str, locale: str, copy: dict[str, Any], heading: str,
social = b.social_meta_html(locale_id=locale,title=title,description=description,url=canonical,
image_url=image,image_alt=title,width=1200,height=630,image_type='image/png',og_type='article')
if locale == 'ja' and 'data-poetry-kanbun-viewer=' in content:
- extra_head += (''
- '')
+ # Fingerprinted like the frames it drives: an installed iOS web app
+ # must not pair a fresh frame with a stale controller.
+ extra_head += (f''
+ f'')
values = {
'HTML_LANG':b.LANG_BY_ID[locale]['html_lang'], 'LOCALE':locale,
'POETRY_TITLE':esc(title), 'POETRY_DESCRIPTION':esc(description),
diff --git a/tools/poetrykuntenrecoverycheck.mjs b/tools/poetrykuntenrecoverycheck.mjs
new file mode 100644
index 0000000..ed40a26
--- /dev/null
+++ b/tools/poetrykuntenrecoverycheck.mjs
@@ -0,0 +1,107 @@
+#!/usr/bin/env node
+// The kanbun reader recovers when its frame loads badly: a missing
+// typesetter (the frame reports an error), a missing viewer (the frame never
+// answers), or a ready report that never reaches the host. Each must end with
+// the visual reader shown, not the plain vertical fallback.
+import assert from 'node:assert/strict';
+import {createServer} from 'node:http';
+import {readFile} from 'node:fs/promises';
+import path from 'node:path';
+import {chromium, webkit} from 'playwright';
+
+const root = process.cwd();
+const page_url = '/ja/poetry/linjiangxian-20260909/';
+const id = 'linjiangxian-20260909-text';
+const mime = {'.html':'text/html; charset=utf-8','.js':'text/javascript','.css':'text/css',
+ '.woff2':'font/woff2','.json':'application/json','.png':'image/png'};
+const server = createServer(async(req,res) => {
+ try {
+ let u = decodeURIComponent(new URL(req.url,'http://localhost').pathname);
+ if (u.endsWith('/')) u += 'index.html';
+ const file = path.resolve(root,'.'+u);
+ if (!file.startsWith(root + path.sep)) throw Error('Path outside project');
+ res.writeHead(200,{'Content-Type':mime[path.extname(file)]||'application/octet-stream'});
+ res.end(await readFile(file));
+ } catch { res.writeHead(404);res.end('Not found'); }
+});
+await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
+const base = 'http://127.0.0.1:' + server.address().port;
+
+// Fail only the first request for a viewer asset; later reloads succeed.
+async function failFirst(context, pattern) {
+ let failed = 0;
+ await context.route(pattern, route => {
+ if (failed++ === 0) return route.abort();
+ return route.continue();
+ });
+}
+
+const scenarios = {
+ 'typesetter missing on first load': context => failFirst(context, /kanbun-render\.js/),
+ 'viewer missing on first load': context => failFirst(context, /kanbun-viewer\/viewer\.js/),
+ 'first ready report lost': async context => {
+ // Swallow the frame's first ready message before the host sees it.
+ await context.addInitScript(() => {
+ if (window.top !== window) return;
+ let dropped = false;
+ addEventListener('message', event => {
+ if (!dropped && event.data?.type === 'hanpu-kanbun-viewer-v1' && event.data.state === 'ready') {
+ dropped = true;
+ event.stopImmediatePropagation();
+ }
+ }, true);
+ });
+ },
+};
+
+let count = 0;
+try {
+ for (const [name,engine] of [['chromium',chromium],['webkit',webkit]]) {
+ const browser = await engine.launch();
+ try {
+ for (const [label, inject] of Object.entries(scenarios)) {
+ const context = await browser.newContext({viewport:{width:390,height:844}});
+ await inject(context);
+ const page = await context.newPage();
+ const errors = [];
+ page.on('pageerror', e=>errors.push(e.message));
+ await page.goto(base+page_url+'?layout=vertical&kunten=on',{waitUntil:'load'});
+ const stage = page.locator(`[data-poetry-kanbun-viewer="${id}"]`);
+ await page.locator(`[data-poetry-kanbun-viewer="${id}"][data-kanbun-ready="true"]`)
+ .waitFor({state:'visible',timeout:20000})
+ .catch(() => { throw Error(`${name}: ${label}: reader never became ready`); });
+ assert.ok(await page.locator('.poem-version.source').first()
+ .evaluate(el=>el.classList.contains('poetry-kanbun-showing')), `${name}: ${label}: source not replaced`);
+ const glyphs = await stage.locator('iframe[data-kanbun-frame]').contentFrame()
+ .locator('#kanbun-display .kaeriten').count();
+ assert.ok(glyphs > 0, `${name}: ${label}: no return marks rendered`);
+ assert.deepEqual(errors, [], `${name}: ${label}: page errors`);
+ count++;
+ await context.close();
+ }
+
+ // A reader stuck before ready restarts when the reader chooses
+ // 縦書き+訓点 again: block the frame entirely, then let it through.
+ const context = await browser.newContext({viewport:{width:390,height:844}});
+ let blocked = true;
+ await context.route(/kanbun-viewer\/works\//, route => blocked ? route.abort() : route.continue());
+ const page = await context.newPage();
+ await page.goto(base+page_url+'?layout=vertical&kunten=on',{waitUntil:'load'});
+ // Exhaust the automatic attempts (3 pings + 2 reloads at 1.5s).
+ await page.waitForTimeout(14000);
+ const stage = page.locator(`[data-poetry-kanbun-viewer="${id}"]`);
+ assert.notEqual(await stage.getAttribute('data-kanbun-ready'),'true',`${name}: blocked frame reported ready`);
+ assert.ok(await page.locator('.poem-version.source .body').first().isVisible(),
+ `${name}: plain source must remain while the reader is unavailable`);
+ blocked = false;
+ await page.locator('[data-poetry-kunten-choice="off"]').click();
+ await page.locator('[data-poetry-kunten-choice="on"]').click();
+ await page.locator(`[data-poetry-kanbun-viewer="${id}"][data-kanbun-ready="true"]`)
+ .waitFor({state:'visible',timeout:20000})
+ .catch(() => { throw Error(`${name}: layout choice did not retry the reader`); });
+ count++;
+ await context.close();
+ } finally { await browser.close(); }
+ }
+} finally { server.close(); }
+console.log(`kanbun recovery: ${count} scenarios passed`);