First real commit of site/. Until now outreach/.git tracked exactly one file
(README.md) and every content pack stamped git_sha=5619a40 regardless of what it
shipped — a signature with no provenance behind it. The site had no witness, which
is why a deploy could erase two sessions of work with nothing to diff against and
nothing to restore from.
Tracked here (authored): site/content, site/config, site/i18n, site/public/images,
justfile, scripts/, .just/, run*.sh, rustelo.manifest.toml.
Ignored (reproducible, or secret):
.k, .env real keys — outreach mirrors to a PUBLIC remote
site/r, site/public/r content indexes, rebuilt by `just content`
provisioning/.content-packs 228 MB of deploy tarballs
cache, data, logs_*.out runtime droppings
Three trees are deliberately NOT ignored despite looking generated, because their
declared source cannot currently reproduce them — ignoring them would delete the
only copy:
site/public/images 196 of 209 images exist nowhere else (assets/site is empty)
site/content/{adr,catalog} projections whose generators are unwired (gen-adr-pages)
site/content/domains or failing (`just graph`)
They become ignorable when an audit proves regeneration is a no-op, not before.
Fixes carried in this import
---------------------------
content: the mirror ran one way and reverted its own work. content_processor writes
site/r, but the recipe ended with `rsync -a site/public/r/ -> site/r/`, so each fresh
index was clobbered by the stale deploy-tree copy: the tool reported "index.json with
69 posts" while the disk kept 58. Meanwhile public/r is the tree the deploy pack ships,
so no new content could ever reach production. The two trees own different files, so
the mirror now runs both ways: content indexes r -> public/r (with --delete, or a
renamed slug survives as a live URL), and the four nu artifacts (about, adr-map,
taglines, content_graph) back public/r -> r, excluded from the outbound leg so stale
copies cannot overwrite the fresh originals.
templates: the assembler has two layers (framework defaults, source-project templates)
but the level chain has three (rustelo -> website-htmx-rustelo -> outreach/site). The
site level had no overlay slot, so its template work had nowhere legitimate to live and
ended up in htmx-templates/ — the one tree `just templates` rm -rf's. That is how the
nav submenus were lost. site/templates-overlay/ is the missing third slot; 214 lines
(nav submenus, subscribe CTA, post engagement block) now live in a declared source and
survive regeneration byte-identical.
adr: projected ADR-059..069 into the content tree. They were written months ago and
never reached the site because gen-adr-pages.nu is wired into no recipe — a generator
outside the dependency chain is a generator that does not run.
Gates added
-----------
templates-check reassembles into a temp tree and asserts an empty diff against the
live one: any drift means htmx-templates/ holds work that exists
live one: any drift means htmx-templates/ holds work that exists
nowhere else and the next run deletes it.
Both gates run before the destructive operation, not after. A check you can only run
afterwards is not a gate, it is an autopsy.
Refs: ADR-062 (projection-not-own-repo: site is a Projection of outreach, not its own
repo — different deploy target is not a different visibility boundary), ADR-048
(materialization), ADR-066 (the check decides, never the reporter — which the content
pipeline, the surface that publishes ontoref, was the last place not to honour).
140 lines
5 KiB
JavaScript
140 lines
5 KiB
JavaScript
// This adds the "preload" extension to htmx. By default, this will
|
|
// preload the targets of any tags with `href` or `hx-get` attributes
|
|
// if they also have a `preload` attribute as well. See documentation
|
|
// for more details
|
|
htmx.defineExtension('preload', {
|
|
|
|
onEvent: function(name, event) {
|
|
// Only take actions on "htmx:afterProcessNode"
|
|
if (name !== 'htmx:afterProcessNode') {
|
|
return
|
|
}
|
|
|
|
// SOME HELPER FUNCTIONS WE'LL NEED ALONG THE WAY
|
|
|
|
// attr gets the closest non-empty value from the attribute.
|
|
var attr = function(node, property) {
|
|
if (node == undefined) { return undefined }
|
|
return node.getAttribute(property) || node.getAttribute('data-' + property) || attr(node.parentElement, property)
|
|
}
|
|
|
|
// load handles the actual HTTP fetch, and uses htmx.ajax in cases where we're
|
|
// preloading an htmx resource (this sends the same HTTP headers as a regular htmx request)
|
|
var load = function(node) {
|
|
// Called after a successful AJAX request, to mark the
|
|
// content as loaded (and prevent additional AJAX calls.)
|
|
var done = function(html) {
|
|
if (!node.preloadAlways) {
|
|
node.preloadState = 'DONE'
|
|
}
|
|
|
|
if (attr(node, 'preload-images') == 'true') {
|
|
document.createElement('div').innerHTML = html // create and populate a node to load linked resources, too.
|
|
}
|
|
}
|
|
|
|
return function() {
|
|
// If this value has already been loaded, then do not try again.
|
|
if (node.preloadState !== 'READY') {
|
|
return
|
|
}
|
|
|
|
// Special handling for HX-GET - use built-in htmx.ajax function
|
|
// so that headers match other htmx requests, then set
|
|
// node.preloadState = TRUE so that requests are not duplicated
|
|
// in the future
|
|
var hxGet = node.getAttribute('hx-get') || node.getAttribute('data-hx-get')
|
|
if (hxGet) {
|
|
htmx.ajax('GET', hxGet, {
|
|
source: node,
|
|
handler: function(elt, info) {
|
|
done(info.xhr.responseText)
|
|
}
|
|
})
|
|
return
|
|
}
|
|
|
|
// Otherwise, perform a standard xhr request, then set
|
|
// node.preloadState = TRUE so that requests are not duplicated
|
|
// in the future.
|
|
if (node.getAttribute('href')) {
|
|
var r = new XMLHttpRequest()
|
|
r.open('GET', node.getAttribute('href'))
|
|
r.onload = function() { done(r.responseText) }
|
|
r.send()
|
|
}
|
|
}
|
|
}
|
|
|
|
// This function processes a specific node and sets up event handlers.
|
|
// We'll search for nodes and use it below.
|
|
var init = function(node) {
|
|
// If this node DOES NOT include a "GET" transaction, then there's nothing to do here.
|
|
if (node.getAttribute('href') + node.getAttribute('hx-get') + node.getAttribute('data-hx-get') == '') {
|
|
return
|
|
}
|
|
|
|
// Guarantee that we only initialize each node once.
|
|
if (node.preloadState !== undefined) {
|
|
return
|
|
}
|
|
|
|
// Get event name from config.
|
|
var on = attr(node, 'preload') || 'mousedown'
|
|
const always = on.indexOf('always') !== -1
|
|
if (always) {
|
|
on = on.replace('always', '').trim()
|
|
}
|
|
|
|
// FALL THROUGH to here means we need to add an EventListener
|
|
|
|
// Apply the listener to the node
|
|
node.addEventListener(on, function(evt) {
|
|
if (node.preloadState === 'PAUSE') { // Only add one event listener
|
|
node.preloadState = 'READY' // Required for the `load` function to trigger
|
|
|
|
// Special handling for "mouseover" events. Wait 100ms before triggering load.
|
|
if (on === 'mouseover') {
|
|
window.setTimeout(load(node), 100)
|
|
} else {
|
|
load(node)() // all other events trigger immediately.
|
|
}
|
|
}
|
|
})
|
|
|
|
// Special handling for certain built-in event handlers
|
|
switch (on) {
|
|
case 'mouseover':
|
|
// Mirror `touchstart` events (fires immediately)
|
|
node.addEventListener('touchstart', load(node))
|
|
|
|
// WHhen the mouse leaves, immediately disable the preload
|
|
node.addEventListener('mouseout', function(evt) {
|
|
if ((evt.target === node) && (node.preloadState === 'READY')) {
|
|
node.preloadState = 'PAUSE'
|
|
}
|
|
})
|
|
break
|
|
|
|
case 'mousedown':
|
|
// Mirror `touchstart` events (fires immediately)
|
|
node.addEventListener('touchstart', load(node))
|
|
break
|
|
}
|
|
|
|
// Mark the node as ready to run.
|
|
node.preloadState = 'PAUSE'
|
|
node.preloadAlways = always
|
|
htmx.trigger(node, 'preload:init') // This event can be used to load content immediately.
|
|
}
|
|
|
|
// Search for all child nodes that have a "preload" attribute
|
|
event.target.querySelectorAll('[preload]').forEach(function(node) {
|
|
// Initialize the node with the "preload" attribute
|
|
init(node)
|
|
|
|
// Initialize all child elements that are anchors or have `hx-get` (use with care)
|
|
node.querySelectorAll('a,[hx-get],[data-hx-get]').forEach(init)
|
|
})
|
|
}
|
|
})
|