fix(textbook): рабочий deep-link к § (/textbook/<slug>#sec-pN открывает нужный §)
Раньше статические страницы алгебры/геометрии игнорировали location.hash (init всегда
goTo('p10')), а textbook-tracker матчил только #pN через .para-pill — поэтому ссылки
exam-prep вида #sec-pN вели на главу, но не на §.
- server.js: /textbook/:slug всегда инжектит хелпер (и в обычном режиме, и в embed),
_renderEmbed → _renderTextbook (кэш по filePath|mode, заголовки no-store сохранены).
- frontend/js/textbook-deeplink.js: по #sec-pN / #pN кликает .psel-card[data-id]
(фолбэк .para-pill[data-para] → goTo → scrollIntoView). Универсально для статических
и движковых страниц, идемпотентно, не конфликтует с textbook-tracker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+14
-11
@@ -421,16 +421,22 @@ const EMBED_INJECT = `
|
|||||||
</script>
|
</script>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function _renderEmbed(filePath, slug) {
|
// Always injected (plain + embed): deep-link helper so /textbook/<slug>#sec-pN
|
||||||
|
// actually opens § N. Without it the page ignores the hash and shows §1.
|
||||||
|
const DEEPLINK_INJECT = `\n<script defer src="/js/textbook-deeplink.js"></script>\n`;
|
||||||
|
|
||||||
|
function _renderTextbook(filePath, slug, embed) {
|
||||||
let stat; try { stat = fs.statSync(filePath); } catch { return null; }
|
let stat; try { stat = fs.statSync(filePath); } catch { return null; }
|
||||||
const cached = _embedCache.get(filePath);
|
const cacheKey = `${filePath}|${embed ? 'e' : 'p'}`;
|
||||||
|
const cached = _embedCache.get(cacheKey);
|
||||||
if (cached && cached.mtime === stat.mtimeMs && cached.slug === slug) return cached.html;
|
if (cached && cached.mtime === stat.mtimeMs && cached.slug === slug) return cached.html;
|
||||||
let html;
|
let html;
|
||||||
try { html = fs.readFileSync(filePath, 'utf8'); } catch { return null; }
|
try { html = fs.readFileSync(filePath, 'utf8'); } catch { return null; }
|
||||||
const inject = EMBED_INJECT.replace('__LS_SLUG__', JSON.stringify(slug));
|
let inject = DEEPLINK_INJECT;
|
||||||
|
if (embed) inject += EMBED_INJECT.replace('__LS_SLUG__', JSON.stringify(slug));
|
||||||
if (html.includes('</head>')) html = html.replace('</head>', inject + '</head>');
|
if (html.includes('</head>')) html = html.replace('</head>', inject + '</head>');
|
||||||
else html = inject + html;
|
else html = inject + html;
|
||||||
_embedCache.set(filePath, { mtime: stat.mtimeMs, slug, html });
|
_embedCache.set(cacheKey, { mtime: stat.mtimeMs, slug, html });
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,13 +449,10 @@ app.get('/textbook/:slug', (req, res, next) => {
|
|||||||
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||||
res.setHeader('Pragma', 'no-cache');
|
res.setHeader('Pragma', 'no-cache');
|
||||||
res.setHeader('Expires', '0');
|
res.setHeader('Expires', '0');
|
||||||
if (req.query.embed === '1') {
|
const html = _renderTextbook(filePath, req.params.slug, req.query.embed === '1');
|
||||||
const html = _renderEmbed(filePath, req.params.slug);
|
if (html == null) return next();
|
||||||
if (html == null) return next();
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
return res.send(html);
|
||||||
return res.send(html);
|
|
||||||
}
|
|
||||||
res.sendFile(filePath, err => { if (err) next(); });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Catalog: /textbooks → frontend/textbooks.html (explicit to avoid conflict with /textbooks/ directory)
|
// Catalog: /textbooks → frontend/textbooks.html (explicit to avoid conflict with /textbooks/ directory)
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/* textbook-deeplink.js
|
||||||
|
Injected by the server into every /textbook/<slug> page.
|
||||||
|
|
||||||
|
Makes deep links like /textbook/<slug>#sec-p13 (and #p13) actually open § 13.
|
||||||
|
The static algebra/geometry pages ignore location.hash in their own init()
|
||||||
|
(they always show §1), and textbook-tracker.js only matches the bare "#pN"
|
||||||
|
form via .para-pill — so exam-prep's "#sec-pN" links never navigated.
|
||||||
|
|
||||||
|
This helper is page-agnostic: it activates the target section by clicking the
|
||||||
|
matching paragraph card/pill, which works on both the static pages
|
||||||
|
(.psel-card[data-id]) and the math6-engine pages (.psel-card / .para-pill),
|
||||||
|
falling back to goTo()/scrollIntoView. Idempotent; safe alongside
|
||||||
|
textbook-tracker.js (a repeated activation of the same § is a no-op). */
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// "#sec-p13" | "#p13" -> "p13" (null if not a paragraph anchor)
|
||||||
|
function targetId() {
|
||||||
|
var h = (location.hash || '').replace(/^#/, '');
|
||||||
|
if (!h) return null;
|
||||||
|
var m = h.match(/^sec-(p\d+)$/) || h.match(/^(p\d+)$/);
|
||||||
|
return m ? m[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function go() {
|
||||||
|
var id = targetId();
|
||||||
|
if (!id) return;
|
||||||
|
var card = document.querySelector('.psel-card[data-id="' + id + '"]');
|
||||||
|
if (card) { card.click(); return; }
|
||||||
|
var pill = document.querySelector('.para-pill[data-para="' + id + '"]');
|
||||||
|
if (pill) { pill.click(); return; }
|
||||||
|
if (typeof window.goTo === 'function') { try { window.goTo(id); return; } catch (e) {} }
|
||||||
|
var el = document.getElementById('sec-' + id) || document.getElementById(id);
|
||||||
|
if (el && el.scrollIntoView) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// The page's own init builds the cards/sections on DOMContentLoaded; run after
|
||||||
|
// it (microtask + a slower pass for engine-rendered math5/6 pages).
|
||||||
|
function boot() {
|
||||||
|
setTimeout(go, 60);
|
||||||
|
setTimeout(go, 350);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', boot);
|
||||||
|
} else {
|
||||||
|
boot();
|
||||||
|
}
|
||||||
|
window.addEventListener('hashchange', go);
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user