diff --git a/flux.vision/content/flimmerkiste-2-wildscreening.md b/flux.vision/content/flimmerkiste-2-wildscreening.md
index 5402a4c..81597fd 100755
--- a/flux.vision/content/flimmerkiste-2-wildscreening.md
+++ b/flux.vision/content/flimmerkiste-2-wildscreening.md
@@ -33,7 +33,7 @@ generator, a nice 5000 ANSI projector, manfrotto tripod, V4 and laptops
we walked through the Kreuzberg'ish 'Jungbusch' area towards the center
marketplace where the huge city hall is located.
-
+
Equipment was set up quickly and we had a lot of fun, doing spontaneous
mapping masks, jamming along, spreading Flimmerkiste flyers to curious
diff --git a/flux.vision/content/library/p1010789-492x276.jpg b/flux.vision/content/library/p1010789-492x276.jpg
new file mode 100644
index 0000000..624c0de
Binary files /dev/null and b/flux.vision/content/library/p1010789-492x276.jpg differ
diff --git a/flux.vision/pelicanconf.py b/flux.vision/pelicanconf.py
index c3f37ee..16c316d 100644
--- a/flux.vision/pelicanconf.py
+++ b/flux.vision/pelicanconf.py
@@ -7,9 +7,18 @@ TIMEZONE = 'Europe/Berlin'
DEFAULT_LANG = 'en'
THEME = 'flux'
-PLUGIN_PATHS = ['plugins', '../tobias.raayoni-last.com/plugins']
+PLUGIN_PATHS = ['plugins', '../plugins']
PLUGINS = ['assetcheck', 'tileweight', 'lostimages']
+# Hosts that no longer exist, so their hotlinked images can never resolve.
+# Checked against Wayback: of twelve such images only one was ever captured,
+# and it now lives in content/library/. See lostimages.py.
+LOSTIMAGES_DEAD_HOSTS = [
+ 'visualberlin.org', 'mutanten.org', 'web.mac.com',
+ 'kino.web.de', 'lh4.google.it', 'l.yimg.com',
+]
+
+
ARTICLE_PATHS = ['']
ARTICLE_EXCLUDES = ['pages', 'library', 'images', 'recover']
PAGE_PATHS = ['pages']
diff --git a/plugins/lostimages.py b/plugins/lostimages.py
new file mode 100644
index 0000000..e5ab17f
--- /dev/null
+++ b/plugins/lostimages.py
@@ -0,0 +1,84 @@
+"""Drop
tags in article bodies whose image is gone for good.
+
+Two kinds of gone, same symptom:
+
+**Local.** The same situation as assetcheck.py, one level down: the recovered
+bodies kept their original /library/ references, and 22 of 200 point at files no
+archive holds (checked against Wayback; the CDX API returns no captures).
+
+**Remote.** Older posts hotlink images from other people's servers, and some of
+those servers no longer exist: visualberlin.org, mutanten.org, web.mac.com,
+kino.web.de, lh4.google.it, l.yimg.com. Name them in LOSTIMAGES_DEAD_HOSTS and
+their images are treated exactly like a lost local file. The host is matched
+rather than the URL fetched, because a build must not depend on the network and
+a dead host is a fact about the world, not about this document.
+
+Left alone either kind renders as a broken-image box, which on a phone reads as
+an unexplained 40px hole between paragraphs. On a gemtext capsule it is worse:
+an image becomes a visible link line, so the page advertises a dead host.
+
+The reference is still worth keeping in the Markdown source, so this strips it
+only from the generated output and leaves a comment naming the file, which keeps
+the output self-documenting for anyone who later finds the originals.
+"""
+import os
+import re
+
+from pelican import signals
+
+IMG = re.compile(r'
]*\bsrc="([^"]+)"[^>]*>')
+REMOTE = re.compile(r'^https?://([^/]+)/', re.IGNORECASE)
+ANCHOR = re.compile(r']*\bhref="(/library/[^"]+)"[^>]*>(.*?)', re.S)
+
+
+def _strip(content):
+ body = getattr(content, "_content", None)
+ if not body or "
" % name_of(src)
+
+ body = IMG.sub(sub, body)
+ # a thumbnail that linked to the full-size file leaves the behind, and
+ # its href 404s just as the
did
+ body = ANCHOR.sub(anchor, body)
+ if not lost:
+ return
+ # a paragraph that held nothing but the lost image would still occupy space
+ body = re.sub(r"\s*((?:\s*)+)
", r"\1", body)
+ content._content = body
+ content.lost_images = lost
+
+
+def register():
+ signals.content_object_init.connect(_strip)
diff --git a/plugins/smolnet.py b/plugins/smolnet.py
index a34c6e4..b757234 100644
--- a/plugins/smolnet.py
+++ b/plugins/smolnet.py
@@ -69,6 +69,12 @@ INTERNAL = re.compile(r"\{(filename|static|attach)\}([^)\s]+)")
SUMMARY_MARKER = ""
+# Markdown image syntax, for dropping the ones whose host is gone. Matched on
+# the source rather than the rendered HTML because this plugin reads the
+# Markdown directly, so lostimages.py, which works on Pelican's output, never
+# sees the capsule.
+MD_IMAGE = re.compile(r'!\[[^\]]*\]\(\s*(https?://[^\s)]+)[^)]*\)')
+
def rot13(text):
"""The obfuscation used on addresses in the HTML templates."""
@@ -156,6 +162,32 @@ def reveal_rot13(text, attr):
return "".join(out)
+def drop_dead_images(text, dead):
+ """Remove images hotlinked from hosts that no longer exist.
+
+ It matters more here than on the web. A browser shows a broken image as a
+ silent gap, but gemtext has no inline images: every one becomes a visible
+ `=>` link line, so the page would advertise a dead host as though it were
+ somewhere worth going.
+
+ Shares LOSTIMAGES_DEAD_HOSTS with lostimages.py so there is one list, and
+ matches on the host rather than fetching, because a build must not depend on
+ the network.
+ """
+ if not dead:
+ return text
+
+ def sub(match):
+ host = match.group(1).split("//", 1)[1].split("/")[0].lower().split(":")[0]
+ if any(host == d or host.endswith("." + d) for d in dead):
+ return ""
+ return match.group(0)
+
+ text = MD_IMAGE.sub(sub, text)
+ # a line that held nothing but the dropped image would leave a blank
+ return re.sub(r"\n[ \t]*\n[ \t]*\n+", "\n\n", text)
+
+
def toml_escape(value):
return str(value).replace("\\", "\\\\").replace('"', '\\"')
@@ -193,6 +225,8 @@ class CapsuleGenerator(Generator):
order = s.get("SMOLNET_SECTION_ORDER", {}) or {}
latest_n = s.get("SMOLNET_LATEST", 15)
rot_attr = s.get("SMOLNET_ROT13_ATTR")
+ dead_hosts = {h.lower().lstrip(".") for h in
+ s.get("LOSTIMAGES_DEAD_HOSTS", [])}
sidecar_name = s.get("SMOLNET_SIDECAR", ".itsybitsy.toml")
articles = [a for a in self.context.get("articles", [])]
@@ -214,6 +248,7 @@ class CapsuleGenerator(Generator):
body = body.replace(SUMMARY_MARKER, "")
body = rewrite_internal(body, by_source, self._warn)
body = reveal_rot13(body, rot_attr)
+ body = drop_dead_images(body, dead_hosts)
# Blank lines, not newlines: a heading and the line under it are
# separate blocks only if a blank line divides them, otherwise the
# date runs into the first sentence as one paragraph.
diff --git a/randogoth.com/pelicanconf.py b/randogoth.com/pelicanconf.py
index 5d7914f..b7f1455 100644
--- a/randogoth.com/pelicanconf.py
+++ b/randogoth.com/pelicanconf.py
@@ -10,7 +10,16 @@ DEFAULT_LANG = 'en'
# and the one actually wanted here. mc-pelican stays in the tree as the fallback.
THEME = 'bbs'
PLUGIN_PATHS = ['plugins', '../plugins']
-PLUGINS = ['crtimages', 'smolnet']
+PLUGINS = ['crtimages', 'smolnet', 'lostimages']
+
+# Hosts that no longer exist, so their hotlinked images can never resolve.
+# Checked against Wayback: of twelve such images only one was ever captured,
+# and it now lives in content/library/. See lostimages.py.
+LOSTIMAGES_DEAD_HOSTS = [
+ 'visualberlin.org', 'mutanten.org', 'web.mac.com',
+ 'kino.web.de', 'lh4.google.it', 'l.yimg.com',
+]
+
# The sections are message areas, numbered so they can be dialled by key the way
# a board's main menu works. No descriptions: the names are the whole listing,
diff --git a/tobias.raayoni-last.com/pelicanconf.py b/tobias.raayoni-last.com/pelicanconf.py
index b07879f..a45c59d 100644
--- a/tobias.raayoni-last.com/pelicanconf.py
+++ b/tobias.raayoni-last.com/pelicanconf.py
@@ -6,7 +6,7 @@ TIMEZONE = 'Asia/Jerusalem'
DEFAULT_LANG = 'en'
THEME = 'raayoni'
-PLUGIN_PATHS = ['plugins']
+PLUGIN_PATHS = ['plugins', '../plugins']
PLUGINS = ['assetcheck', 'lostimages']
# Dark is the register this work belongs to, so it is the default rather than
# following the system. An explicit toggle (persisted in localStorage) wins.
diff --git a/tobias.raayoni-last.com/plugins/lostimages.py b/tobias.raayoni-last.com/plugins/lostimages.py
deleted file mode 100644
index c7b36ef..0000000
--- a/tobias.raayoni-last.com/plugins/lostimages.py
+++ /dev/null
@@ -1,55 +0,0 @@
-"""Drop
tags in article bodies whose file did not survive the recovery.
-
-The same situation as assetcheck.py, one level down: the recovered bodies kept
-their original /library/ references, and 22 of 200 point at files no archive
-holds (checked against Wayback; the CDX API returns no captures for them).
-
-Left alone they render as a broken-image box, which on a phone reads as an
-unexplained 40px hole between paragraphs. The reference is still worth keeping
-in the Markdown source, so this strips it only from the generated HTML and
-leaves a comment naming the file, which keeps the output self-documenting for
-anyone who later finds the originals on the offline drive.
-"""
-import os
-import re
-
-from pelican import signals
-
-IMG = re.compile(r'
]*\bsrc="(/[^"]+)"[^>]*>')
-ANCHOR = re.compile(r']*\bhref="(/library/[^"]+)"[^>]*>(.*?)', re.S)
-
-
-def _strip(content):
- body = getattr(content, "_content", None)
- if not body or "
" % os.path.basename(rel)
-
- body = IMG.sub(sub, body)
- # a thumbnail that linked to the full-size file leaves the behind, and
- # its href 404s just as the
did
- body = ANCHOR.sub(anchor, body)
- if not lost:
- return
- # a paragraph that held nothing but the lost image would still occupy space
- body = re.sub(r"\s*((?:\s*)+)
", r"\1", body)
- content._content = body
- content.lost_images = lost
-
-
-def register():
- signals.content_object_init.connect(_strip)