Remove non-TLS support
This commit is contained in:
1 parent
84c07a912b
commit
b59ef914da
9 files changed
+88
-171
No files matched your search
+22
-57
@@ -19,13 +19,22 @@
|
|||||||
The pages on disk are fragments and the shell is a template, and it is the
|
The pages on disk are fragments and the shell is a template, and it is the
|
||||||
worker that puts them together. So this page has one job: register the
|
worker that puts them together. So this page has one job: register the
|
||||||
worker and reload once it is running, which lands on the real app at the
|
worker and reload once it is running, which lands on the real app at the
|
||||||
address that was asked for. It is only ever seen once per device. -->
|
address that was asked for. It is only ever seen once per device.
|
||||||
|
|
||||||
|
There is no fallback for a device that cannot install one. There used to
|
||||||
|
be -- this page loaded the renderer itself and composed the page in the
|
||||||
|
browser -- and it existed for one case: the app served over plain http://
|
||||||
|
from a LAN address, which is not a secure context, so no worker can
|
||||||
|
register. This app is served over TLS, where that case cannot arise, and
|
||||||
|
the fallback was a second way of building every page that nothing
|
||||||
|
exercised. What is left when registration fails is a sentence saying so,
|
||||||
|
because there is nothing honest to show instead. -->
|
||||||
|
|
||||||
<main class="boot container py-5 text-center">
|
<main class="boot container py-5 text-center">
|
||||||
<div class="spinner-border text-secondary mb-3" role="status" aria-hidden="true"></div>
|
<div class="spinner-border text-secondary mb-3" role="status" aria-hidden="true"></div>
|
||||||
<p id="boot-status" class="text-body-secondary">Preparing the offline copy…</p>
|
<p id="boot-status" class="text-body-secondary">Preparing the offline copy…</p>
|
||||||
<noscript><p class="alert alert-warning d-inline-block">This app builds its
|
<noscript><p class="alert alert-warning d-inline-block">This app builds its
|
||||||
pages in the browser and needs JavaScript.</p></noscript>
|
pages on the device and needs JavaScript.</p></noscript>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -50,7 +59,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function start() {
|
function start() {
|
||||||
if (tries >= 2) return compose('The offline copy could not start.');
|
if (tries >= 2) return fail('The offline copy could not start.');
|
||||||
try { sessionStorage.setItem('boot:tries', String(tries + 1)); } catch (err) {}
|
try { sessionStorage.setItem('boot:tries', String(tries + 1)); } catch (err) {}
|
||||||
|
|
||||||
navigator.serviceWorker.register('sw.js').then(function (registration) {
|
navigator.serviceWorker.register('sw.js').then(function (registration) {
|
||||||
@@ -58,70 +67,26 @@
|
|||||||
// controlled yet. Otherwise wait for the one installing to get there.
|
// controlled yet. Otherwise wait for the one installing to get there.
|
||||||
if (registration.active) return go();
|
if (registration.active) return go();
|
||||||
var worker = registration.installing || registration.waiting;
|
var worker = registration.installing || registration.waiting;
|
||||||
if (!worker) return compose('The offline copy did not install.');
|
if (!worker) return fail('The offline copy did not install.');
|
||||||
worker.addEventListener('statechange', function (event) {
|
worker.addEventListener('statechange', function (event) {
|
||||||
if (event.target.state === 'activated') go();
|
if (event.target.state === 'activated') go();
|
||||||
});
|
});
|
||||||
}, function (err) {
|
}, function (err) {
|
||||||
compose('Could not install the offline copy: ' + err.message);
|
fail('Could not install the offline copy: ' + err.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everything below is the path where there will never be a worker: a browser
|
// The end of the road. Every page in this app is put together by the
|
||||||
// without them, or -- much more likely -- the app served over plain http://
|
// worker, so a device that cannot install one has nothing to read here --
|
||||||
// from a LAN address, which is not a secure context. Rather than spin here
|
// and a spinner that turns forever says less than the reason does.
|
||||||
// forever, the page composes itself, using the same renderer and the same
|
function fail(why) {
|
||||||
// templates the worker would have used. No offline copy and no bookmark, but
|
var spinner = document.querySelector('.boot .spinner-border');
|
||||||
// the text reads. Delete this if the app is only ever served over TLS.
|
if (spinner) spinner.remove();
|
||||||
function compose(why) {
|
status(why);
|
||||||
console.warn(why);
|
|
||||||
status('Loading…');
|
|
||||||
script('js/nunjucks.min.js')
|
|
||||||
.then(function () { return script('js/render.js'); })
|
|
||||||
.then(function () {
|
|
||||||
var view = createRenderer(readText);
|
|
||||||
var page = location.pathname.split('/').pop();
|
|
||||||
return readText('./' + page + location.search)
|
|
||||||
.catch(function () {
|
|
||||||
return view.notice('Not available',
|
|
||||||
'This page could not be loaded. Check your connection and try again.');
|
|
||||||
})
|
|
||||||
// mark is null: the bookmark lives in the worker's IndexedDB, and
|
|
||||||
// there is no worker to ask. bookmark.njk renders the slot hidden
|
|
||||||
// and it fills itself the moment one appears.
|
|
||||||
.then(function (fragment) { return view.compose(fragment, null); });
|
|
||||||
})
|
|
||||||
.then(become, function (err) { status('Could not start: ' + err.message); });
|
|
||||||
}
|
|
||||||
|
|
||||||
function script(src) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
var el = document.createElement('script');
|
|
||||||
el.src = src;
|
|
||||||
el.onload = function () { resolve(); };
|
|
||||||
el.onerror = function () { reject(new Error('cannot load ' + src)); };
|
|
||||||
document.head.appendChild(el);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function readText(path) {
|
|
||||||
return fetch(path, { credentials: 'same-origin' }).then(function (res) {
|
|
||||||
if (!res.ok) throw new Error(path + ': ' + res.status);
|
|
||||||
return res.text();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replaces this document in place: same URL, same history entry, no second
|
|
||||||
// request. Written rather than assigned into the DOM because the shell
|
|
||||||
// carries <script> tags, and scripts put into a page any other way never run.
|
|
||||||
function become(html) {
|
|
||||||
document.open();
|
|
||||||
document.write(html);
|
|
||||||
document.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('serviceWorker' in navigator) start();
|
if ('serviceWorker' in navigator) start();
|
||||||
else compose('This browser has no service worker support.');
|
else fail('This browser has no service worker support.');
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+10
-27
@@ -1,7 +1,7 @@
|
|||||||
// Page startup.
|
// Page startup.
|
||||||
//
|
//
|
||||||
// Pages arrive whole -- composed by the service worker, or by boot.html on the
|
// Pages arrive whole -- the service worker composes every one of them -- so
|
||||||
// visit before there is one -- so there is nothing here that builds a page.
|
// there is nothing here that builds a page.
|
||||||
// What is left is the three things a composed document cannot do for itself:
|
// What is left is the three things a composed document cannot do for itself:
|
||||||
// put a swapped-in page where it belongs before it is painted, land the
|
// put a swapped-in page where it belongs before it is painted, land the
|
||||||
// reference picker where the reader asked to go, and tell the chrome when a
|
// reference picker where the reader asked to go, and tell the chrome when a
|
||||||
@@ -94,11 +94,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A worker-composed page is an ordinary document and the browser scrolls to
|
// The browser scrolls to the anchor itself on a document it navigated to,
|
||||||
// the anchor itself. One composed by boot.html is not: it replaced a
|
// so this is for the one it does not: a reload that landed on boot.html and
|
||||||
// document the browser had already finished with, so the fragment identifier
|
// was reloaded again into the real page. Costs nothing when the browser has
|
||||||
// has been spent. Repeating it costs nothing in the first case and is what
|
// already been there.
|
||||||
// makes a link to GEN#v1_1 land on the verse in the second.
|
|
||||||
function scrollToHash() {
|
function scrollToHash() {
|
||||||
var el = hashTarget();
|
var el = hashTarget();
|
||||||
if (el) jumpTo(el);
|
if (el) jumpTo(el);
|
||||||
@@ -166,25 +165,12 @@
|
|||||||
//
|
//
|
||||||
// Toggling the class rather than the style because Bootstrap declares
|
// Toggling the class rather than the style because Bootstrap declares
|
||||||
// .d-none !important, which an inline display would lose to.
|
// .d-none !important, which an inline display would lose to.
|
||||||
//
|
|
||||||
// Only the digest line is missing on a page boot.html composed -- there was
|
|
||||||
// no worker to name a version -- so the element itself is always here.
|
|
||||||
function showVersion() {
|
function showVersion() {
|
||||||
var el = document.getElementById('app-version');
|
var el = document.getElementById('app-version');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.classList.toggle('d-none', !document.querySelector('#content .index'));
|
el.classList.toggle('d-none', !document.querySelector('#content .index'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// api/bookmark has no origin server -- it is answered by the worker -- so
|
|
||||||
// there is nothing to ask until the worker is controlling this page.
|
|
||||||
// controller, not ready: ready can resolve a tick before clients.claim()
|
|
||||||
// takes effect, and a request sent in that gap reaches the network and 404s.
|
|
||||||
function whenControlled(fn) {
|
|
||||||
if (!('serviceWorker' in navigator)) return;
|
|
||||||
if (navigator.serviceWorker.controller) return fn();
|
|
||||||
navigator.serviceWorker.addEventListener('controllerchange', fn, { once: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deferred scripts run before DOMContentLoaded fires, so this always lands,
|
// Deferred scripts run before DOMContentLoaded fires, so this always lands,
|
||||||
// and it lands after htmx has processed the shell.
|
// and it lands after htmx has processed the shell.
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
@@ -192,15 +178,12 @@
|
|||||||
showVersion();
|
showVersion();
|
||||||
|
|
||||||
// Registered from the page it is going to serve, not from a page of its
|
// Registered from the page it is going to serve, not from a page of its
|
||||||
// own: there is nothing to wait for and nothing to reload. This load is
|
// own: this load is already the real app, composed by the worker that is
|
||||||
// already the real app -- the worker installs alongside it, and takes over
|
// controlling it. What this catches is the copy on disk having changed
|
||||||
// composing, the bookmark and the offline download from the next
|
// since -- the browser refetches sw.js, sees different bytes, and installs
|
||||||
// navigation on.
|
// the new one alongside, which takes over from the next navigation on.
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.register('sw.js').catch(function () {});
|
navigator.serviceWorker.register('sw.js').catch(function () {});
|
||||||
}
|
}
|
||||||
whenControlled(function () {
|
|
||||||
document.body.dispatchEvent(new CustomEvent('worker-ready'));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
+16
-19
@@ -1,17 +1,15 @@
|
|||||||
// The renderer, shared by the two things that ever build a page: the service
|
// The renderer: the templates in templates/, and the two things sw.js does
|
||||||
// worker, which composes every navigation once it is installed, and boot.html,
|
// with them -- compose a fragment into the shell, and stand a notice in for a
|
||||||
// which composes the one navigation that arrives before there is a worker to
|
// page that could not be delivered.
|
||||||
// do it.
|
|
||||||
//
|
//
|
||||||
// Sharing it is the whole point. A shell only the worker could build is what
|
// A file of its own rather than more of sw.js because it is the half with
|
||||||
// made a first visit show a spinner and reload; with the same templates and
|
// nothing to do with the network: no routes, no caches, no requests. It was
|
||||||
// the same code reachable from the page, the first visit is already the app
|
// loaded by boot.html as well once, which composed pages in the browser for a
|
||||||
// and the worker is what makes the second one fast and the hundredth one
|
// device that could not install a worker; nothing but the worker builds a page
|
||||||
// offline.
|
// now.
|
||||||
//
|
//
|
||||||
// Loaded as a plain script by both -- importScripts in the worker, a <script>
|
// Loaded with importScripts, so it defines a global rather than exporting.
|
||||||
// tag in the page -- so it defines a global rather than exporting. Nunjucks
|
// Nunjucks must already be loaded.
|
||||||
// must already be loaded.
|
|
||||||
(function (scope) {
|
(function (scope) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
@@ -123,10 +121,9 @@
|
|||||||
//
|
//
|
||||||
// chrome is what the shell needs and the fragment cannot carry: the
|
// chrome is what the shell needs and the fragment cannot carry: the
|
||||||
// bookmark, the navbar picker's model, and the two versions the worker
|
// bookmark, the navbar picker's model, and the two versions the worker
|
||||||
// was built with. All three are null from boot.html, which has no worker
|
// was built with. reference is null on the book list, which is the
|
||||||
// to ask for any of them; the first two render as a hidden slot that
|
// picker's hidden state; the other two are always there, because the
|
||||||
// fills itself if one ever appears, and the third renders as nothing,
|
// worker composing this is the thing that knows them.
|
||||||
// because no worker is the whole of what there is to report.
|
|
||||||
compose: function (fragment, chrome) {
|
compose: function (fragment, chrome) {
|
||||||
var title = (TITLE_RE.exec(fragment) || ['', ''])[1].trim();
|
var title = (TITLE_RE.exec(fragment) || ['', ''])[1].trim();
|
||||||
var content = fragment.replace(TITLE_RE, '');
|
var content = fragment.replace(TITLE_RE, '');
|
||||||
@@ -134,9 +131,9 @@
|
|||||||
return render(name, {
|
return render(name, {
|
||||||
title: title,
|
title: title,
|
||||||
content: content,
|
content: content,
|
||||||
mark: (chrome && chrome.mark) || null,
|
mark: chrome.mark,
|
||||||
reference: (chrome && chrome.reference) || null,
|
reference: chrome.reference,
|
||||||
version: (chrome && chrome.version) || null
|
version: chrome.version
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+14
-24
@@ -9,8 +9,7 @@
|
|||||||
//
|
//
|
||||||
// GET / the book list
|
// GET / the book list
|
||||||
// GET /:page one book -- /GEN, /EXO
|
// GET /:page one book -- /GEN, /EXO
|
||||||
// GET /api/bookmark the navbar slot
|
// POST /api/bookmark set the bookmark, and return the navbar slot
|
||||||
// POST /api/bookmark set the bookmark, and return the slot
|
|
||||||
// GET /api/reference the navbar's chapter and verse picker
|
// GET /api/reference the navbar's chapter and verse picker
|
||||||
// GET /api/verse the dialog for one verse
|
// GET /api/verse the dialog for one verse
|
||||||
// GET /api/toast one confirmation, by name
|
// GET /api/toast one confirmation, by name
|
||||||
@@ -28,11 +27,9 @@
|
|||||||
// navigation -- one route, both callers, which is the whole reason the request
|
// navigation -- one route, both callers, which is the whole reason the request
|
||||||
// object carries `navigate`.
|
// object carries `navigate`.
|
||||||
//
|
//
|
||||||
// The composing is in js/render.js, which boot.html can load too. Normally it
|
// The composing is in js/render.js. Every page in the app comes through it:
|
||||||
// does not: boot.html registers this worker and reloads into it. It composes in
|
// boot.html registers this worker and reloads into it, and from there nothing
|
||||||
// the page when no worker is possible at all -- no secure context, or a browser
|
// else ever builds one.
|
||||||
// without them -- and then it does so from these same templates rather than
|
|
||||||
// leaving a spinner up forever.
|
|
||||||
//
|
//
|
||||||
// Every same-origin GET is network-first: ask the network, fall back to the
|
// Every same-origin GET is network-first: ask the network, fall back to the
|
||||||
// cache when it fails. Offline, fetch rejects immediately, so the fallback is
|
// cache when it fails. Offline, fetch rejects immediately, so the fallback is
|
||||||
@@ -461,16 +458,12 @@ router.get('/', (call) => servePage('./', '', call));
|
|||||||
router.get('/:page', (call) =>
|
router.get('/:page', (call) =>
|
||||||
servePage('./' + call.params.page, call.params.page, call));
|
servePage('./' + call.params.page, call.params.page, call));
|
||||||
|
|
||||||
router.get('/api/bookmark', () => bookmarkFragment(''));
|
|
||||||
|
|
||||||
// The picker, and the two moves it makes.
|
// The picker, and the two moves it makes.
|
||||||
//
|
//
|
||||||
// no parameters the whole picker, standing where the anchor in the address
|
// no parameters the whole picker, standing where the anchor in the address
|
||||||
// bar puts it. Asked for by the picker itself: on load,
|
// bar puts it. Asked for by the picker itself: on load, and
|
||||||
// after a history restore -- which swaps the history element
|
// after a history restore -- which swaps the history element
|
||||||
// straight in and processes no out-of-band elements -- and
|
// straight in and processes no out-of-band elements.
|
||||||
// when a worker first appears under a page boot.html
|
|
||||||
// composed.
|
|
||||||
// at=ch3 the verses of that chapter, and land on the chapter.
|
// at=ch3 the verses of that chapter, and land on the chapter.
|
||||||
// at=v3_16 the same verses, that one selected, and land on the verse.
|
// at=v3_16 the same verses, that one selected, and land on the verse.
|
||||||
//
|
//
|
||||||
@@ -490,9 +483,8 @@ router.get('/api/bookmark', () => bookmarkFragment(''));
|
|||||||
// is rendered at 1:1 like any other. The page then asks this route for itself,
|
// is rendered at 1:1 like any other. The page then asks this route for itself,
|
||||||
// and that request is the one carrying the anchor.
|
// and that request is the one carrying the anchor.
|
||||||
//
|
//
|
||||||
// Which also puts the picker where it belongs after a back or forward, and
|
// Which also puts the picker where it belongs after a back or forward, for
|
||||||
// under a page boot.html composed once a worker turns up, for nothing extra:
|
// nothing extra: those requests carry the address bar too.
|
||||||
// those requests carry the address bar too.
|
|
||||||
//
|
//
|
||||||
// Two response headers carry what a change means beyond the options:
|
// Two response headers carry what a change means beyond the options:
|
||||||
//
|
//
|
||||||
@@ -734,13 +726,11 @@ function getBookmark() {
|
|||||||
.then((mark) => mark || DEFAULT_BOOKMARK);
|
.then((mark) => mark || DEFAULT_BOOKMARK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The navbar slot, and the toast that says what just happened to it. The
|
||||||
|
// toast rides out of band, so one response updates the slot and raises it
|
||||||
|
// without the page having to ask for the second one -- same template and same
|
||||||
|
// table api/toast answers from, so the two cannot drift apart.
|
||||||
async function bookmarkFragment(say) {
|
async function bookmarkFragment(say) {
|
||||||
let html = await view.render('bookmark.njk', { mark: await getBookmark() });
|
return await view.render('bookmark.njk', { mark: await getBookmark() })
|
||||||
if (say) {
|
+ '\n' + await toastFragment(say, true);
|
||||||
// Out of band, so one response updates the navbar slot and raises the
|
|
||||||
// toast without the page having to ask for the second one. Same template
|
|
||||||
// and same table api/toast answers from, so the two cannot drift apart.
|
|
||||||
html += '\n' + await toastFragment(say, true);
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
}
|
||||||
@@ -2,22 +2,12 @@
|
|||||||
worker holds, and returned on its own by api/bookmark after a write. Both
|
worker holds, and returned on its own by api/bookmark after a write. Both
|
||||||
paths must produce the same element for the swap to be seamless.
|
paths must produce the same element for the swap to be seamless.
|
||||||
|
|
||||||
mark is null in one case only: boot.html composed this page, so there was
|
Always filled: getBookmark() answers with the default when nothing has been
|
||||||
no worker to ask. The slot is then hidden rather than absent and fetches
|
set and when storage cannot be read at all, so there is no empty state to
|
||||||
itself once app.js reports a worker is controlling -- which is why the htmx
|
render. There was one -- a hidden slot that fetched itself once a worker
|
||||||
attributes are on the empty state and not the filled one. A filled slot is
|
turned up -- for pages boot.html composed before there was a worker to ask.
|
||||||
only ever replaced by the modal's form, which names #bookmark itself.
|
Nothing composes a page but the worker now, and the worker always knows. #}
|
||||||
|
<span class="bookmark navbar-text d-flex align-items-center gap-1" id="bookmark">
|
||||||
Empty state is d-none, not the hidden attribute: Bootstrap declares .d-flex
|
|
||||||
after Reboot's [hidden] rule, both !important and at the same specificity,
|
|
||||||
so the utility would win. #}
|
|
||||||
<span class="bookmark navbar-text {% if mark %}d-flex{% else %}d-none{% endif %} align-items-center gap-1"
|
|
||||||
id="bookmark"
|
|
||||||
{%- if not mark %}
|
|
||||||
hx-get="api/bookmark" hx-trigger="worker-ready from:body"
|
|
||||||
hx-target="this" hx-swap="outerHTML"
|
|
||||||
{%- endif %}>
|
|
||||||
{%- if mark %}
|
|
||||||
{# A word and a colon, not a badge: this is one line of text that happens to
|
{# A word and a colon, not a badge: this is one line of text that happens to
|
||||||
end in a link, and a pill in front of it read as a control. Outside the
|
end in a link, and a pill in front of it read as a control. Outside the
|
||||||
anchor so that only the reference is underlined -- Reboot underlines an
|
anchor so that only the reference is underlined -- Reboot underlines an
|
||||||
@@ -28,5 +18,4 @@
|
|||||||
anchor, which a swap into #content would not do. #}
|
anchor, which a swap into #content would not do. #}
|
||||||
<a class="bookmark-link link-light text-truncate" href="{{ mark.href }}"
|
<a class="bookmark-link link-light text-truncate" href="{{ mark.href }}"
|
||||||
hx-boost="false">{{ mark.ref }}</a>
|
hx-boost="false">{{ mark.ref }}</a>
|
||||||
{%- endif %}
|
|
||||||
</span>
|
</span>
|
||||||
@@ -28,8 +28,7 @@
|
|||||||
line, so one response moves both.
|
line, so one response moves both.
|
||||||
a restore htmx swaps the history element straight in and processes
|
a restore htmx swaps the history element straight in and processes
|
||||||
no out-of-band elements at all, so the picker asks for
|
no out-of-band elements at all, so the picker asks for
|
||||||
itself instead. worker-ready is in the same trigger, for a
|
itself instead.
|
||||||
page boot.html composed before there was a worker to ask.
|
|
||||||
an anchor a link to GEN#v3_16 should open with the picker reading
|
an anchor a link to GEN#v3_16 should open with the picker reading
|
||||||
3:16, and the fragment never reaches the worker: a browser
|
3:16, and the fragment never reaches the worker: a browser
|
||||||
asks for /GEN and keeps the rest. So a picker that arrived
|
asks for /GEN and keeps the rest. So a picker that arrived
|
||||||
@@ -57,7 +56,7 @@
|
|||||||
id="reference"
|
id="reference"
|
||||||
{%- if oob %} hx-swap-oob="true"{% endif %}
|
{%- if oob %} hx-swap-oob="true"{% endif %}
|
||||||
hx-get="api/reference"
|
hx-get="api/reference"
|
||||||
hx-trigger="{% if reference and not settled %}load[!!location.hash], {% endif %}worker-ready from:body, htmx:historyRestore from:body"
|
hx-trigger="{% if reference and not settled %}load[!!location.hash], {% endif %}htmx:historyRestore from:body"
|
||||||
hx-target="this" hx-swap="outerHTML">
|
hx-target="this" hx-swap="outerHTML">
|
||||||
{%- if reference %}
|
{%- if reference %}
|
||||||
<span class="reference-book text-light fw-semibold"
|
<span class="reference-book text-light fw-semibold"
|
||||||
|
|||||||
@@ -10,11 +10,6 @@
|
|||||||
without this document being rebuilt, so the value only becomes current on
|
without this document being rebuilt, so the value only becomes current on
|
||||||
the next full load -- which is the reload you would do to check it anyway.
|
the next full load -- which is the reload you would do to check it anyway.
|
||||||
|
|
||||||
version is null in one case: boot.html composed this page, so there was no
|
|
||||||
worker to ask. Nothing is rendered at all then, which is the honest answer
|
|
||||||
-- no worker means no version, and the bookmark slot in the navbar is empty
|
|
||||||
for the same reason. app.js finds no element and leaves it at that.
|
|
||||||
|
|
||||||
Rendered on every page and shown under the book list alone. Populating it
|
Rendered on every page and shown under the book list alone. Populating it
|
||||||
only for the index would look like the smaller change and would not work:
|
only for the index would look like the smaller change and would not work:
|
||||||
the shell is composed once per full navigation, and a boosted link to the
|
the shell is composed once per full navigation, and a boosted link to the
|
||||||
@@ -22,10 +17,8 @@
|
|||||||
decides, not the composer -- app.js looks for the index in #content after
|
decides, not the composer -- app.js looks for the index in #content after
|
||||||
every swap. The body class this was rendered with cannot be asked, for the
|
every swap. The body class this was rendered with cannot be asked, for the
|
||||||
same reason: it goes stale the moment a link is clicked. #}
|
same reason: it goes stale the moment a link is clicked. #}
|
||||||
{%- if version %}
|
|
||||||
<div class="app-version d-none text-center mt-5 mb-2" id="app-version">
|
<div class="app-version d-none text-center mt-5 mb-2" id="app-version">
|
||||||
<p class="app-version-digests form-text m-0">
|
<p class="app-version-digests form-text m-0">
|
||||||
shell {{ version.shell }} · library {{ version.library }}
|
shell {{ version.shell }} · library {{ version.library }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{%- endif %}
|
|
||||||
@@ -11,13 +11,13 @@ Beyond serving files it has three jobs.
|
|||||||
|
|
||||||
The first is the boot page. The pages here are fragments and the shell around
|
The first is the boot page. The pages here are fragments and the shell around
|
||||||
them is a template, so a browser asking for one directly would get a bare
|
them is a template, so a browser asking for one directly would get a bare
|
||||||
fragment with no chrome. Navigations are answered with boot.html, which loads
|
fragment with no chrome. Navigations are answered with boot.html instead, which
|
||||||
the same renderer the service worker uses, composes the page that was asked
|
registers the service worker and reloads once it is running -- landing on the
|
||||||
for, and replaces itself with it -- so a first visit is the real app at the
|
real app at the address that was asked for. Requests the worker and htmx make
|
||||||
real URL, with no reload. Requests the worker and htmx make are ordinary
|
are ordinary fetches rather than navigations, so they pass straight through and
|
||||||
fetches rather than navigations, so they pass straight through and get the
|
get the fragment. Once a worker is installed it intercepts the navigation
|
||||||
fragment. Once a worker is installed it intercepts the navigation first and
|
first and composes it there, and this page is not reached at all: it is seen
|
||||||
composes it there, and this page is not reached at all.
|
once per device.
|
||||||
|
|
||||||
The second is stamping the service worker. A browser only notices a new worker
|
The second is stamping the service worker. A browser only notices a new worker
|
||||||
when the bytes of sw.js change, so something has to record what is on disk.
|
when the bytes of sw.js change, so something has to record what is on disk.
|
||||||
@@ -30,11 +30,12 @@ The third is cache headers: the worker owns the offline copy, and if the
|
|||||||
worker or the boot page that registers it are served stale, a new version can
|
worker or the boot page that registers it are served stale, a new version can
|
||||||
never reach a device that already installed the old one.
|
never reach a device that already installed the old one.
|
||||||
|
|
||||||
Expects to sit behind something that terminates TLS. Without it the app still
|
Expects to sit behind something that terminates TLS, and does not work without
|
||||||
works -- boot.html composes every page in the browser -- but there is no
|
it. Every page is composed by the service worker, browsers only register one in
|
||||||
offline copy: browsers only register a service worker in a secure context, and
|
a secure context, and boot.html has no fallback that builds a page without one
|
||||||
the bookmark lives in that worker, so over plain http:// from a LAN address the
|
-- so over plain http:// from a LAN address the boot page says it could not
|
||||||
app reads online-only and the bookmark slot stays empty.
|
install and stops there. localhost is a secure context, which is why running
|
||||||
|
this directly for development still works.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -235,8 +236,9 @@ class Handler(SimpleHTTPRequestHandler):
|
|||||||
self.send_header("Cache-Control", "public, max-age=604800")
|
self.send_header("Cache-Control", "public, max-age=604800")
|
||||||
# One URL, two bodies: a navigation to /GEN gets the boot page and a
|
# One URL, two bodies: a navigation to /GEN gets the boot page and a
|
||||||
# fetch of it gets the fragment. Without this the browser cache keeps
|
# fetch of it gets the fragment. Without this the browser cache keeps
|
||||||
# one entry for both, and boot.html asking for its own fragment is
|
# one entry for both, and the worker asking for the fragment it is
|
||||||
# handed back a copy of itself -- a document nested inside a document.
|
# about to compose is handed back the boot page instead -- which is a
|
||||||
|
# document nested inside a document, or a spinner where the text goes.
|
||||||
self.send_header("Vary", "Sec-Fetch-Mode")
|
self.send_header("Vary", "Sec-Fetch-Mode")
|
||||||
# Lets the worker claim the whole origin even if it moves off root.
|
# Lets the worker claim the whole origin even if it moves off root.
|
||||||
self.send_header("Service-Worker-Allowed", "/")
|
self.send_header("Service-Worker-Allowed", "/")
|
||||||
|
|||||||
@@ -23,9 +23,8 @@ different jobs and the seam is where a second output format would attach:
|
|||||||
WRITING That tree becomes one semantic fragment per book: a <title> and
|
WRITING That tree becomes one semantic fragment per book: a <title> and
|
||||||
one root element carrying data-template, not a document. The
|
one root element carrying data-template, not a document. The
|
||||||
shell around it is assembled from the nunjucks templates in
|
shell around it is assembled from the nunjucks templates in
|
||||||
assets/templates/ -- by the service worker, or by boot.html when
|
assets/templates/ by the service worker, so chrome changes never
|
||||||
there can never be one -- so chrome changes never require running
|
require running this. The markup carries structure and no presentation: every
|
||||||
this. The markup carries structure and no presentation: every
|
|
||||||
USFM distinction survives as a class or data attribute, so the
|
USFM distinction survives as a class or data attribute, so the
|
||||||
whole corpus can be restyled from the stylesheet without being
|
whole corpus can be restyled from the stylesheet without being
|
||||||
regenerated.
|
regenerated.
|
||||||
|
|||||||
Reference in new issue
Block a user