feat: make smolnet usable by a site that is not randogoth.com
Three changes, each found by pointing it at zonetoast, which files by tag
rather than category and writes its links by hand.
A single-bucket taxonomy is dropped. Pelican gives every uncategorised article
its DEFAULT_CATEGORY, so a tag-filed site still has one category holding
everything, and offering "misc, 14" as the only way in is worse than offering
nothing at all. Below SMOLNET_MIN_SECTIONS the whole section index goes.
randogoth.com still gets its six areas.
Links written against the HTML build are rewritten. {filename} is the portable
form, but content predating that knowledge hardcodes the output URL instead,
and zonetoast is full of ](/go-west.html). A capsule has no .html and the
daemon resolves a name carrying an extension literally, so all seventeen of
them 404'd. They now map onto the flat capsule slug, /pages/glossary.html
included, and a target matching no document is reported rather than silently
rewritten into a second broken link.
The line under an article title is a setting. Date and category is the right
default, but a site whose category is always `misc` has something better to
say: zonetoast carries `era`, which is what it is actually organised around,
so its articles read "17 May 2026 · 1996".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4efe975599
commit
0d7e98a0d9
1 changed files with 51 additions and 4 deletions
|
|
@ -17,7 +17,9 @@ setting, and every setting has a default, so a site can switch it on with just
|
||||||
SMOLNET_OUTPUT 'smolnet' directory under OUTPUT_PATH
|
SMOLNET_OUTPUT 'smolnet' directory under OUTPUT_PATH
|
||||||
SMOLNET_SECTION_DIR 'section' subdirectory holding one index per category
|
SMOLNET_SECTION_DIR 'section' subdirectory holding one index per category
|
||||||
SMOLNET_LATEST 15 entries on the front page; 0 drops the section
|
SMOLNET_LATEST 15 entries on the front page; 0 drops the section
|
||||||
|
SMOLNET_META [date, category] fields for the line under an article title
|
||||||
SMOLNET_SECTION_ORDER {} category name -> sort key, else alphabetical
|
SMOLNET_SECTION_ORDER {} category name -> sort key, else alphabetical
|
||||||
|
SMOLNET_MIN_SECTIONS 2 below this, the section index is dropped
|
||||||
SMOLNET_LABELS {} override any heading, see LABELS
|
SMOLNET_LABELS {} override any heading, see LABELS
|
||||||
SMOLNET_ASSETS None directories to copy; defaults to STATIC_PATHS
|
SMOLNET_ASSETS None directories to copy; defaults to STATIC_PATHS
|
||||||
SMOLNET_SIDECAR '.itsybitsy.toml'
|
SMOLNET_SIDECAR '.itsybitsy.toml'
|
||||||
|
|
@ -67,6 +69,13 @@ META_CONT = re.compile(r"^\s+\S")
|
||||||
# Pelican's internal link syntax. Left alone these reach the capsule verbatim.
|
# Pelican's internal link syntax. Left alone these reach the capsule verbatim.
|
||||||
INTERNAL = re.compile(r"\{(filename|static|attach)\}([^)\s]+)")
|
INTERNAL = re.compile(r"\{(filename|static|attach)\}([^)\s]+)")
|
||||||
|
|
||||||
|
# Links written against the HTML build, e.g. ](/go-west.html). Pelican's
|
||||||
|
# {filename} form is the portable one, but plenty of content predates knowing
|
||||||
|
# that and hardcodes the output URL instead. A capsule has no .html, and the
|
||||||
|
# daemon resolves a name carrying an extension literally, so these 404 unless
|
||||||
|
# they are rewritten.
|
||||||
|
HARDCODED = re.compile(r"\]\(\s*(/[^)\s]*?\.html)([^)]*)\)")
|
||||||
|
|
||||||
SUMMARY_MARKER = "<!-- PELICAN_END_SUMMARY -->"
|
SUMMARY_MARKER = "<!-- PELICAN_END_SUMMARY -->"
|
||||||
|
|
||||||
# Markdown image syntax, for dropping the ones whose host is gone. Matched on
|
# Markdown image syntax, for dropping the ones whose host is gone. Matched on
|
||||||
|
|
@ -137,6 +146,26 @@ def rewrite_internal(text, by_source, warn):
|
||||||
return INTERNAL.sub(sub, text)
|
return INTERNAL.sub(sub, text)
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_html_links(text, slugs, warn):
|
||||||
|
"""Point links written against the HTML build at the capsule instead.
|
||||||
|
|
||||||
|
`/pages/glossary.html` and `/go-west.html` both become `/<slug>`, since the
|
||||||
|
capsule is flat. A target whose slug is not among the documents built is
|
||||||
|
reported and left alone rather than silently rewritten into a second broken
|
||||||
|
link.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def sub(match):
|
||||||
|
target, rest = match.group(1), match.group(2)
|
||||||
|
slug = os.path.basename(target)[: -len(".html")]
|
||||||
|
if slug in slugs:
|
||||||
|
return "](/%s%s)" % (slug, rest)
|
||||||
|
warn("smolnet: link to %s matches no document", target)
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
return HARDCODED.sub(sub, text)
|
||||||
|
|
||||||
|
|
||||||
def reveal_rot13(text, attr):
|
def reveal_rot13(text, attr):
|
||||||
"""Replace a block of HTML carrying a rot13 address with the address itself.
|
"""Replace a block of HTML carrying a rot13 address with the address itself.
|
||||||
|
|
||||||
|
|
@ -237,8 +266,10 @@ class CapsuleGenerator(Generator):
|
||||||
# Map source basename -> slug, so {filename} can be resolved. Built from
|
# Map source basename -> slug, so {filename} can be resolved. Built from
|
||||||
# everything published, pages included, since an article may link one.
|
# everything published, pages included, since an article may link one.
|
||||||
by_source = {}
|
by_source = {}
|
||||||
|
slugs = set()
|
||||||
for doc in articles + pages:
|
for doc in articles + pages:
|
||||||
by_source[os.path.basename(doc.source_path)] = doc.slug
|
by_source[os.path.basename(doc.source_path)] = doc.slug
|
||||||
|
slugs.add(doc.slug)
|
||||||
|
|
||||||
os.makedirs(out, exist_ok=True)
|
os.makedirs(out, exist_ok=True)
|
||||||
root_titles = {}
|
root_titles = {}
|
||||||
|
|
@ -247,6 +278,7 @@ class CapsuleGenerator(Generator):
|
||||||
body = strip_metadata(self._read(doc))
|
body = strip_metadata(self._read(doc))
|
||||||
body = body.replace(SUMMARY_MARKER, "")
|
body = body.replace(SUMMARY_MARKER, "")
|
||||||
body = rewrite_internal(body, by_source, self._warn)
|
body = rewrite_internal(body, by_source, self._warn)
|
||||||
|
body = rewrite_html_links(body, slugs, self._warn)
|
||||||
body = reveal_rot13(body, rot_attr)
|
body = reveal_rot13(body, rot_attr)
|
||||||
body = drop_dead_images(body, dead_hosts)
|
body = drop_dead_images(body, dead_hosts)
|
||||||
# Blank lines, not newlines: a heading and the line under it are
|
# Blank lines, not newlines: a heading and the line under it are
|
||||||
|
|
@ -261,12 +293,21 @@ class CapsuleGenerator(Generator):
|
||||||
fh.write(text)
|
fh.write(text)
|
||||||
root_titles[name] = doc.title
|
root_titles[name] = doc.title
|
||||||
|
|
||||||
|
# Which metadata goes under the title. Default is date and category,
|
||||||
|
# but a site that files by tag has no useful category and may well have
|
||||||
|
# a field that means more: zonetoast carries `era`, which is the point
|
||||||
|
# of the site in a way that Pelican's default `misc` is not.
|
||||||
|
meta_fields = s.get("SMOLNET_META", ["date", "category"])
|
||||||
for article in articles:
|
for article in articles:
|
||||||
bits = []
|
bits = []
|
||||||
if getattr(article, "date", None):
|
for field in meta_fields:
|
||||||
bits.append(article.date.strftime("%d %b %Y"))
|
value = getattr(article, field, None)
|
||||||
if getattr(article, "category", None):
|
if not value:
|
||||||
bits.append(str(article.category))
|
continue
|
||||||
|
if field == "date":
|
||||||
|
bits.append(value.strftime("%d %b %Y"))
|
||||||
|
else:
|
||||||
|
bits.append(str(value))
|
||||||
emit(article, " · ".join(bits))
|
emit(article, " · ".join(bits))
|
||||||
for page in pages:
|
for page in pages:
|
||||||
emit(page, "")
|
emit(page, "")
|
||||||
|
|
@ -276,6 +317,12 @@ class CapsuleGenerator(Generator):
|
||||||
self.context.get("categories", []),
|
self.context.get("categories", []),
|
||||||
key=lambda pair: (order.get(str(pair[0]), 99), str(pair[0])),
|
key=lambda pair: (order.get(str(pair[0]), 99), str(pair[0])),
|
||||||
)
|
)
|
||||||
|
# A site that files by tag rather than category still has one category,
|
||||||
|
# Pelican's DEFAULT_CATEGORY, holding everything. Offering "misc, 14" as
|
||||||
|
# the only way in is worse than offering nothing, so one bucket is not a
|
||||||
|
# taxonomy and the whole section index goes.
|
||||||
|
if len(sections) < s.get("SMOLNET_MIN_SECTIONS", 2):
|
||||||
|
sections = []
|
||||||
if sections:
|
if sections:
|
||||||
os.makedirs(os.path.join(out, section_dir), exist_ok=True)
|
os.makedirs(os.path.join(out, section_dir), exist_ok=True)
|
||||||
section_titles = {}
|
section_titles = {}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue