feat: make the capsule subscribable over Gemini
Link labels in every capsule index are now ISO 8601 and lead with the date, which is what Gemini's subscription convention reads a feed entry from: the first ten characters of a label must parse as YYYY-MM-DD or the line is ignored. The board's `09 Oct 26` parsed as nothing, so the front page, the archive and all six areas registered zero entries and the capsule was invisible to every aggregator in Geminispace. `/archive` now carries 36. The Connect page advertised `/feeds/all.rss.xml` and `/feeds/all.atom.xml`, which exist only in the HTML build and 404 over Gemini. That paragraph now sits behind an itsybitsy `only html` format gate, with prose outside it that is true on both faces. The gate can only cut that way round: Pelican does not implement the directive and renders it as an inert comment, so a gate subtracts from the capsule and never from the site. Also adds dormant `SMOLNET_COMMENTS` support, for a per-article thread link gated to the text formats. Off until kapara is deployed, since setting it would put a dead link under every article. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
17f3a4e79e
commit
9817e7190c
3 changed files with 69 additions and 4 deletions
|
|
@ -25,6 +25,8 @@ setting, and every setting has a default, so a site can switch it on with just
|
|||
SMOLNET_SIDECAR '.itsybitsy.toml'
|
||||
SMOLNET_ROT13_ATTR None attribute holding a rot13 address to reveal
|
||||
SMOLNET_TAGLINE None one line at the foot of the index
|
||||
SMOLNET_COMMENTS None thread URL per article, `{slug}` substituted
|
||||
SMOLNET_COMMENTS_FORMATS formats that get it, default gemtext and text
|
||||
|
||||
Why link-only paragraphs rather than lists: gemtext has one kind of link, a line
|
||||
beginning `=>`, so a renderer has to lift links out of the prose. itsybitsy's
|
||||
|
|
@ -58,6 +60,7 @@ LABELS = {
|
|||
"archive": "All entries",
|
||||
"pages": "Pages",
|
||||
"back": "Back to the index",
|
||||
"comments": "Comments",
|
||||
}
|
||||
|
||||
# Pelican's metadata block: bare `Key: value` lines from the first line on,
|
||||
|
|
@ -240,6 +243,22 @@ def link(label, href):
|
|||
return "[%s](%s)" % (label.replace("]", ""), href)
|
||||
|
||||
|
||||
def gate(block, formats):
|
||||
"""Wrap a block in an itsybitsy format gate.
|
||||
|
||||
A gate names output format ids, not protocols: `gemtext` is the one Gemini,
|
||||
Spartan, Nex and Gopher are all served from, so there is no gating on a
|
||||
protocol name. Blank lines because a gate encloses *blocks*, and a
|
||||
directive run together with the line under it is one block.
|
||||
|
||||
Note which way this can cut. Pelican does not implement the directive and
|
||||
renders it as an inert HTML comment, so a gate can only ever subtract from
|
||||
the capsule, never from the site. That is the right direction for anything
|
||||
this plugin writes, because the capsule is all it writes.
|
||||
"""
|
||||
return "<!-- only %s -->\n\n%s\n\n<!-- end -->" % (" ".join(formats), block)
|
||||
|
||||
|
||||
class CapsuleGenerator(Generator):
|
||||
"""Writes the capsule once the content generators have read everything."""
|
||||
|
||||
|
|
@ -257,6 +276,14 @@ class CapsuleGenerator(Generator):
|
|||
dead_hosts = {h.lower().lstrip(".") for h in
|
||||
s.get("LOSTIMAGES_DEAD_HOSTS", [])}
|
||||
sidecar_name = s.get("SMOLNET_SIDECAR", ".itsybitsy.toml")
|
||||
# kapara serves the same remark42 thread the HTML widget shows, over
|
||||
# Gemini, Spartan and Nex. Articles only: the widget is on articles
|
||||
# only, and a page with a thread nobody can reach from the site is a
|
||||
# second conversation, which is the one thing the gateway exists to
|
||||
# avoid. Gated because this tree can also be served as WML or
|
||||
# XHTML-MP, and a handset cannot open a gemini:// link.
|
||||
comments = s.get("SMOLNET_COMMENTS")
|
||||
comments_formats = s.get("SMOLNET_COMMENTS_FORMATS", ("gemtext", "text"))
|
||||
|
||||
articles = [a for a in self.context.get("articles", [])]
|
||||
pages = [p for p in self.context.get("pages", [])]
|
||||
|
|
@ -274,7 +301,7 @@ class CapsuleGenerator(Generator):
|
|||
os.makedirs(out, exist_ok=True)
|
||||
root_titles = {}
|
||||
|
||||
def emit(doc, trail):
|
||||
def emit(doc, trail, thread=False):
|
||||
body = strip_metadata(self._read(doc))
|
||||
body = body.replace(SUMMARY_MARKER, "")
|
||||
body = rewrite_internal(body, by_source, self._warn)
|
||||
|
|
@ -288,6 +315,11 @@ class CapsuleGenerator(Generator):
|
|||
if trail:
|
||||
head += "%s\n\n" % trail
|
||||
text = head + body.strip() + "\n"
|
||||
if thread and comments:
|
||||
text += "\n%s\n" % gate(
|
||||
link(labels["comments"], comments.format(slug=doc.slug)),
|
||||
comments_formats,
|
||||
)
|
||||
name = "%s.md" % doc.slug
|
||||
with open(os.path.join(out, name), "w", encoding="utf-8") as fh:
|
||||
fh.write(text)
|
||||
|
|
@ -308,7 +340,7 @@ class CapsuleGenerator(Generator):
|
|||
bits.append(value.strftime("%d %b %Y"))
|
||||
else:
|
||||
bits.append(str(value))
|
||||
emit(article, " · ".join(bits))
|
||||
emit(article, " · ".join(bits), thread=True)
|
||||
for page in pages:
|
||||
emit(page, "")
|
||||
|
||||
|
|
@ -381,12 +413,25 @@ class CapsuleGenerator(Generator):
|
|||
relative targets would need `../` and would break again at any further
|
||||
depth; `/slug` is correct from anywhere and the daemon resolves it
|
||||
against the site root.
|
||||
|
||||
The date is ISO 8601 and it leads the label, which is protocol surface
|
||||
rather than a style choice. Gemini's subscription convention takes a
|
||||
link line for a feed entry when the first ten characters of its label
|
||||
are a `YYYY-MM-DD` date, reads the rest as the title, and ignores every
|
||||
line where they are not, so this one format is what makes each index a
|
||||
feed: the front page, the archive and all six areas, subscribable in a
|
||||
client and submittable to an aggregator. A board's `09 Oct 26` parses
|
||||
as nothing and registers zero entries.
|
||||
|
||||
The separator is the one the convention's own examples use. Clients are
|
||||
asked to sanitise whatever sits between the date and the title, but not
|
||||
all of them do, and a `·` that survives arrives as part of the title.
|
||||
"""
|
||||
rows = []
|
||||
for doc in docs:
|
||||
label = doc.title
|
||||
if getattr(doc, "date", None):
|
||||
label = "%s · %s" % (doc.date.strftime("%d %b %y"), doc.title)
|
||||
label = "%s - %s" % (doc.date.strftime("%Y-%m-%d"), doc.title)
|
||||
rows.append(link(label, "/%s" % doc.slug))
|
||||
# Hard breaks, which is two trailing spaces in CommonMark. gemtext lifts
|
||||
# each link to its own => line either way, but the inline formats lay a
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue