diff --git a/.memory/worklog.json b/.memory/worklog.json
index aac9648..919a131 100644
--- a/.memory/worklog.json
+++ b/.memory/worklog.json
@@ -489,6 +489,13 @@
"message": "auto-save 2026-04-18 15:40 (~1)",
"hash": "d46152c",
"files_changed": 1
+ },
+ {
+ "ts": "2026-04-18T15:48:56+08:00",
+ "type": "commit",
+ "message": "auto-save 2026-04-18 15:48 (~2)",
+ "hash": "15b2812",
+ "files_changed": 2
}
]
}
diff --git a/web/Dockerfile b/web/Dockerfile
index fbfc09e..5c8cfa3 100644
--- a/web/Dockerfile
+++ b/web/Dockerfile
@@ -2,24 +2,7 @@ FROM nginx:alpine
COPY . /usr/share/nginx/html/
-RUN printf 'types {\n\
- text/html html htm;\n\
- text/css css;\n\
- text/plain txt;\n\
- text/markdown md;\n\
- application/javascript js;\n\
- application/json json;\n\
- image/jpeg jpg jpeg;\n\
- image/png png;\n\
- image/webp webp;\n\
- image/svg+xml svg;\n\
- image/x-icon ico;\n\
- font/woff woff;\n\
- font/woff2 woff2;\n\
-}\n\
-default_type application/octet-stream;\n\
-\n\
-server {\n\
+RUN printf 'server {\n\
listen 80;\n\
server_name _;\n\
root /usr/share/nginx/html;\n\
@@ -33,7 +16,13 @@ server {\n\
access_log off;\n\
}\n\
\n\
- # html / md: always revalidate\n\
+ # novel.md specifically — force text/markdown so gzip triggers\n\
+ location = /novel.md {\n\
+ default_type "text/markdown; charset=utf-8";\n\
+ add_header Cache-Control "public, max-age=0, must-revalidate";\n\
+ }\n\
+\n\
+ # html / md catch-all: always revalidate\n\
location ~* \\.(html|md)$ {\n\
add_header Cache-Control "public, max-age=0, must-revalidate";\n\
}\n\
diff --git a/web/index.html b/web/index.html
index 6ce8093..e6b7eab 100644
--- a/web/index.html
+++ b/web/index.html
@@ -922,13 +922,21 @@ function closeLightbox() {
}
async function loadNovel() {
+ const container = document.getElementById('reader-content');
try {
- const resp = await fetch('./novel.md');
+ container.innerHTML = '
正在下载全本 …
';
+ const resp = await fetch('./novel.md', { cache: 'no-cache' });
+ if (!resp.ok) throw new Error('HTTP ' + resp.status);
+ container.innerHTML = '正在渲染章节 …
';
const text = await resp.text();
+ // Yield to the browser so the loading message paints before heavy work
+ await new Promise(r => setTimeout(r, 0));
renderNovel(text);
} catch (e) {
- document.getElementById('reader-content').innerHTML =
- '小说载入失败:' + e.message + '
';
+ console.error('loadNovel failed:', e);
+ container.innerHTML =
+ '';
}
}
@@ -944,24 +952,28 @@ function renderNovel(md) {
let m = heading.match(/^(第(\d+)章|终章)\s+(.+)$/);
let isPart2 = false;
let num, title;
+ const cnMap = { '一': '01', '二': '02', '三': '03', '四': '04', '五': '05', '六': '06', '七': '07', '八': '08', '九': '09', '十': '10' };
+ const cnToInt = { '一':1,'二':2,'三':3,'四':4,'五':5,'六':6,'七':7,'八':8,'九':9,'十':10 };
+ let kicker, chN;
if (m) {
const isEpilogue = !m[2];
num = isEpilogue ? '终' : m[2].padStart(2, '0');
title = m[3];
- // kicker assigned below
- var kicker = isEpilogue ? 'EPILOGUE' : 'CHAPTER ' + num;
+ kicker = isEpilogue ? 'EPILOGUE' : 'CHAPTER ' + num;
+ chN = isEpilogue ? 0 : parseInt(m[2]);
} else {
const m2 = heading.match(/^第二部\s*·\s*第(.+?)章\s*·\s*(.+)$/);
if (!m2) return;
isPart2 = true;
- const cnMap = { '一': '01', '二': '02', '三': '03', '四': '04', '五': '05', '六': '06', '七': '07', '八': '08', '九': '09', '十': '10' };
const arabic = cnMap[m2[1]] || m2[1];
num = 'II · ' + arabic;
title = m2[2];
- var kicker = 'PART II · CHAPTER ' + arabic;
+ kicker = 'PART II · CHAPTER ' + arabic;
+ chN = -(cnToInt[m2[1]] || parseInt(m2[1])); // -1 = Part II Ch1, -2 = Part II Ch2, ...
}
- const ch = CHAPTERS[idx];
- const imgPath = ch ? `./images/${ch.img}` : '';
+ // Match chapter by semantic n (immune to section index drift caused by non-chapter ## headings)
+ const ch = CHAPTERS.find(c => c.n === chN);
+ const imgPath = (ch && ch.img) ? `./images/${ch.img}` : '';
const paragraphs = body.split(/\n\s*\n/).map(p => {
const line = p.trim();
if (!line) return '';