Introduction
This document is the source of truth for the Arcology web templates. Templates are written in [Pebble](https://pebbletemplates.io/) and served by the Ktor Pebble plugin (see server.org). Each template tangles to a .peb file under src/jvmMain/resources/templates/, loaded at server startup via ClasspathLoader.
The template hierarchy mirrors the Django arcology templates:
app.peb— base layout (<html><head>, header, footer)page.peb— extendsapp.peb; renders page body + sidebar (default template)wide.peb— extendsapp.peb; TOC inline at top, sidebar panels (backlinks, tags, references, keywords) stacked at bottom; references also inlined under each headingtopic.peb— extendsapp.peb; likewide.pebbut backlinks AND references are inlined under each heading, sidebar backlinks/refs panels suppressedsidebar.peb— partial: TOC + backlinks + tags + references + keywords404.peb— not-found pagesitemap.peb—/route indexfeed.peb— Atom feed (rendered by the FeedPublisher, Phase 5)robots.peb— robots.txt (stub for Phase 9)
Pebble filter notes:
|rawskips HTML escaping (Django's|safeequivalent). Used forhtmlContent,heading.html, and sidebar fragments that are already-rendered HTML.|default(value)provides a fallback for null/empty.
Static assets under src/jvmMain/resources/static/ that are not tangled from this file: highlight.min.js, htmx.min.js, the =fonts/= and =hljs/= trees. These are copied in directly as binary assets — they can't be tangled from org. htmx.min.js (v2.0.4) drives the Phase 4 attachment progressive enhancement: the renderer emits =<img src=small hx-get=large-html hx-trigger="load" hx-swap="outerHTML">=, and htmx swaps in the large crushed variant on page load.
app.peb — Base Layout
The base layout every page extends. Defines title, extra_head, and content blocks. Loads the base stylesheet, per-site CSS (if set), and the dynamic /sites.css for cross-site link coloring. Header shows the site title with cross-site nav. Footer is the colophon + fediring.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="author" content="Ryan Rix"/>
<meta name="generator" content="Arcology Site Engine https://engine.arcology.garden/"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="/static/arcology.css"/>
<link rel="stylesheet" href="/static/vulf.css"/>
{% if site.cssFile %}<link rel="stylesheet" href="/static/{{ site.cssFile }}"/>{% endif %}
<link rel="stylesheet" href="/static/hljs/{{ site.hljsTheme | default("github-dark") }}.min.css"/>
<link rel="stylesheet" href="/sites.css"/>
<script src="/static/highlight.min.js" defer></script>
<script src="/static/arcology-src-blocks.js" defer></script>
<script src="/static/htmx.min.js" defer></script>
<title>{% block title %}{{ headTitle | default("The Arcology Project") }}{% endblock %}</title>
{% block extra_head %}{% endblock %}
</head>
<body>
<header>
<div class="header-content">
{% block h1 %}
<h1><a href="/">{{ site.title | default(site.key) }}</a></h1>
<h2>{{ pageTitle | default("") }}</h2>
{% endblock %}
<nav class="cross-site">
• <a class="internal" href="https://thelionsrear.com">Life</a>
• <a class="internal" href="https://arcology.garden">Tech</a>
• <a class="internal" href="https://cce.whatthefuck.computer">Emacs</a>
• <a class="internal" href="https://engine.arcology.garden">Arcology</a>
•
</nav>
</div>
</header>
<div class="content">
{% block content %}{% endblock %}
</div>
<footer>
<hr/>
© 02026 <a href="https://arcology.garden/people/rrix">Ryan Rix</a>
<<a href="mailto:site@whatthefuck.computer">site@whatthefuck.computer</a>>
<p>
Care has been taken to publish accurate information to
long-lived URLs, but context and content as well as URLs may
change without notice.
</p>
<p>
This site collects no personal information from visitors, nor
stores any identifying tokens. If you or your personal
information ended up in public notes please email me for
correction or removal.
</p>
<p>
Email me with questions, comments, insights, kind criticism.
blow horn, good luck.
</p>
<p>
View the <a href="/sitemap">Site Map</a> or the <a href="/tags">Tag Index</a> to explore the sites.
</p>
<p>
<a href="https://fediring.net/previous?host=arcology.garden">←</a>
<a href="https://fediring.net/">Fediring</a>
<a href="https://fediring.net/next?host=arcology.garden">→</a>
</p>
</footer>
</body>
</html>page.peb — Default Page Template
Extends app.peb. Renders the org-mode body in <main>, the renderer's sidenotes immediately after, then includes the sidebar.peb partial.
On wide screens the TOC lives in the right sidebar (rendered by sidebar.peb). On narrow screens the sidebar collapses below the content, so a second, collapsible =<details class="top-toc">= TOC is rendered at the top of the content block and shown via CSS media query; the sidebar TOC panel is hidden in the same query. Both TOCs share the same headings list and indentation classes, so the outline looks identical either way.
{% extends "app.peb" %}
{% block title %}{{ headTitle }}{% endblock %}
{% block extra_head %}
{% for feed in feeds %}
<link rel="alternate" type="application/atom+xml" href="{{ feed.url }}" title="{{ feed.title }}"/>
{% endfor %}
{% if allowCrawl %}
<meta name="robots" content=""/>
{% else %}
<meta name="robots" content="noarchive noimageindex noindex nofollow"/>
{% endif %}
{% endblock %}
{% block content %}
<div class="top-toc">
<<sidebar-heading-toc>>
</div>
<main>{{ htmlContent | raw }}</main>
{{ sidenotesHtml | raw }}
<section class="sidebar">
<<sidebar-heading-toc>>
<<sidebar-backlinks>>
<<sidebar-tags>>
<<sidebar-refs>>
<<sidebar-keywords>>
</section>
{% endblock %}wide.peb — Wide Page Template
Extends app.peb. Moves the TOC to an inline block at the top and stacks the sidebar panels (backlinks, tags, references, keywords) at the bottom. References are also rendered inline under each heading by the renderer, so the sidebar references panel is suppressed via the refsInline flag. Extra <style> overrides the .content grid so <main> spans full width.
{% extends "app.peb" %}
{% block title %}{{ headTitle }}{% endblock %}
{% block extra_head %}
{% for feed in feeds %}
<link rel="alternate" type="application/atom+xml" href="{{ feed.url }}" title="{{ feed.title }}"/>
{% endfor %}
{% if allowCrawl %}
<meta name="robots" content=""/>
{% else %}
<meta name="robots" content="noarchive noimageindex noindex nofollow"/>
{% endif %}
<style>
.content { max-width: 100ch; display: block !important; }
.content::before { border: none !important; margin: 0 !important; }
.content .main { display: block !important; }
</style>
{% endblock %}
{% block content %}
<div class="wide">
<main>{{ htmlContent | raw }}</main>
{{ sidenotesHtml | raw }}
<section>
<<sidebar-backlinks>>
<<sidebar-tags>>
<<sidebar-refs>>
<<sidebar-keywords>>
</section>
</div>
{% endblock %}topic.peb — Topic Page Template
Extends app.peb. Like wide.peb: full-width main, TOC inline at top. Both backlinks and references are rendered inline under each heading by the renderer (via the headingBacklinks / headingRefs constructor params), so the sidebar's backlinks and references panels are suppressed via the topic flag. Tags and keywords panels still stack at the bottom. Extra <style> overrides the .content grid so <main> spans full width.
{% extends "app.peb" %}
{% block title %}{{ headTitle }}{% endblock %}
{% block extra_head %}
{% for feed in feeds %}
<link rel="alternate" type="application/atom+xml" href="{{ feed.url }}" title="{{ feed.title }}"/>
{% endfor %}
{% if allowCrawl %}
<meta name="robots" content=""/>
{% else %}
<meta name="robots" content="noarchive noimageindex noindex nofollow"/>
{% endif %}
<style>
.content { max-width: 100ch; display: block !important; }
.content::before { border: none !important; margin: 0 !important; }
.content .main { display: block !important; }
</style>
{% endblock %}
{% block content %}
<div class="wide">
<main>{{ htmlContent | raw }}</main>
{{ sidenotesHtml | raw }}
<section >
<<sidebar-backlinks>>
<<sidebar-tags>>
<<sidebar-keywords>>
</section>
</div>
{% endblock %}Sidebar Sections
Included by page.peb, wide.peb, and topic.peb. Five panels, each gated on a non-empty list:
headings— table of contents (skipped in wide mode; rendered inline at top instead)backlinks— pages linking here (suppressed in topic mode; inlined under each heading by the renderer)tags— page tagsreferences— external references (suppressed in wide and topic mode; inlined under each heading by the renderer)keywords— page metadata keywords
The wide, topic, and refsInline flags are not passed via Pebble's {% include ... with %} — Ktor's PebbleContent model does not forward =with=-scoped variables to the partial's conditions. They are computed server-side in modelToMap (see web server) from the resolved template: topic.peb sets wide /=topic= /=refsInline= (suppresses backlinks and references panels), wide.peb sets wide /=refsInline= (suppresses references only), and page.peb sets none, so all panels show.
{% if headings | length > 1 %}
<details class="headings" open>
<summary>Contents</summary>
<ul class="headings">
{% for heading in headings %}
<li class="level-{{ heading.level }}"><a href="#{{ heading.anchor }}">{{ heading.title | raw }}</a></li>
{% endfor %}
</ul>
</details>
{% endif %}{% if backlinks | length > 0 %}
<details class="backlinks" open>
<summary>Pages Linking Here</summary>
<ul class="backlinks">
{% for backlink in backlinks %}
<li><a href="{{ backlink.url }}">{{ backlink.title }}</a></li>
{% endfor %}
</ul>
</details>
{% endif %}{% if tags | length > 0 %}
<details class="tags" open>
<summary>Page Tags</summary>
<ul class="tags">
{% for tag in tags %}
<li><a href="/tags/{{ tag }}">{{ tag }}</a></li>
{% endfor %}
</ul>
</details>
{% endif %}{% if not refsInline and not topic and references | length > 0 %}
<details class="references" open>
<summary>External References</summary>
<ul class="references">
{% for ref in references %}
<li><a target="_blank" href="{{ ref }}">{{ ref }}</a></li>
{% endfor %}
</ul>
</details>
{% endif %}{% if keywords | length > 0 %}
<details class="keywords" open>
<summary>Page Metadata Keywords</summary>
<ul class="keywords">
{% for keyword in keywords %}
<li><pre>#+{{ keyword.key }}: {{ keyword.value }}</pre></li>
{% endfor %}
</ul>
</details>
{% endif %}404.peb — Not Found Page
Replaces the hand-rolled serveNotFoundPage string builder in server.org. Shows the missing node ID if present.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="/static/arcology.css"/>
<link rel="stylesheet" href="/sites.css"/>
<title>Page Not Found</title>
<style>.unpublished{color:#999;text-decoration:line-through}</style>
</head>
<body>
<h1>Page Not Found</h1>
<section>
<p>The page you tried to open either has not been written by the author or the author has chosen to not publish it at this time.</p>
{% if nodeId %}
<pre>MISSING NODE = {{ nodeId }}</pre>
{% endif %}
</section>
</body>
</html>sitemap.peb — Sitemap Index
The / route. Lists all sites and their route counts.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="/static/arcology.css"/>
<link rel="stylesheet" href="/sites.css"/>
<title>Site Map</title>
</head>
<body>
<h1>Site Map</h1>
<section>
{% if localhost %}
<p><em>Local preview — showing all routes including drafts and archived.</em></p>
{% endif %}
<ul>
{% for site in sites %}
<li>
<h2>{{ site.title | default(site.key) }}</h2>
<ul>
{% for route in site.routes %}
<li><a href="/{{ route.path }}">{{ route.path }}</a>{% if route.count > 1 %} ({{ route.count }}){% endif %}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</section>
</body>
</html>feed.peb — Atom Feed
Rendered by the FeedPublisher through ArcologyServer.feedXmlFor for /{path}.xml routes. The model carries title, pageUrl (the feed's anchor page), feedUrl (the feed's own URL), author, updatedAt (newest entry pubdate, RFC-3339), and feedEntries — each with title, url (absolute, cross-domain-resolved), nodeId, published, updated, and html (rendered body). Phase 5 implementation: see feeds.org.
Per RFC 4287, entry content goes in =<content type="html">= (==summary= is plain text and reserved for a future hand-written summary; RFC 4287 §4.2.13). The HTML is escaped per RFC 4287 §4.1.3 (HTML in =type="text"/"html"= constructs must be entity-escaped, e.g. <p>) — Pebble's default {{ }} escaping does this, so the body renders with {{ entry.html }} and no |raw.
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>{{ title }}</title>
<link href="{{ pageUrl }}"/>
<link rel="self" href="{{ feedUrl }}"/>
<updated>{{ updatedAt }}</updated>
<author>
<name>{{ author }}</name>
</author>
<id>{{ pageUrl }}</id>
{% for entry in feedEntries %}
<entry>
<title>{{ entry.title }}</title>
<link href="{{ entry.url }}"/>
<id>urn:uid:{{ entry.nodeId }}</id>
<published>{{ entry.published }}</published>
<updated>{{ entry.updated }}</updated>
<content type="html">{{ entry.html }}</content>
</entry>
{% endfor %}
</feed>robots.peb — robots.txt (Phase 9 stub)
User-agent: *
Disallow:arcology.css — Base Stylesheet
Ported from the Django arcology project's app.css + default-colors.css, adapted for the KMP template structure. Defines CSS custom properties (colors, layout widths), the flexbox content+sidebar layout, typography (Vulf Mono italic body, sans-serif headings), code block styling, task states with emoji, blockquote/verse styling, the .content::before dotted divider, and the Tufte-style sidenote margin column with narrow-viewport collapse.
/* ── Color variables (default — per-site CSS overrides these) ── */
:root {
--alert: #cc6960;
--primary: #707231;
--secondary: #ebbe7b;
--success: #67b4f8;
--warning: #7e5c41;
--white: #fcf6ed;
--light-gray: #f6e5cb;
--medium-gray: #baad9b;
--dark-gray: #82796c;
--black: #211f1c;
--max-content: 55ch;
--sidebar-width: 22ch;
}
/* ── Body ── */
body {
font-family: "Vulf Mono", monospace;
font-style: italic;
font-size: medium;
background-color: var(--white);
color: var(--black);
margin: 0;
}
/* ── Links ── */
a { color: var(--primary); }
a:visited { color: var(--warning); }
a:hover { text-decoration: underline; }
/* Additional in per-site css... */
code { font-style: normal; }
/* ── Headings ── */
h1, h2, h3, h4, h5, h6 {
font-family: "Vulf Sans", -apple-system, "Segoe UI", Roboto, sans-serif;
font-style: normal;
font-weight: 800;
line-height: 1.2;
}
/* ── Header ── */
header {
background-color: var(--light-gray);
border-bottom: 2px solid var(--dark-gray);
}
header > .header-content {
padding: 1em;
max-width: 120ch;
margin-left: auto;
margin-right: auto;
}
header h1, header h2 {
margin-top: 0;
display: inline;
font-style: normal;
}
header h2:before { content: " — "; }
/* ── Footer ── */
footer {
margin-left: auto;
margin-right: auto;
max-width: 120ch;
font-size: smaller;
text-align: center;
padding: 1em;
}
footer a { font-weight: 500; }
footer p { max-width: 60ch; margin: 0.5em auto; }
nav.cross-site {
margin-top: 0.5rem;
font-size: 0.9rem;
color: var(--dark-gray);
}
/* ── Content layout (flexbox: main + sidebar) ── */
.content {
margin-left: auto;
margin-right: auto;
padding: 1em;
padding-top: 0;
display: flex;
flex-flow: row wrap;
max-width: 120ch;
}
.content > section, main {
display: inline-block;
flex-grow: 1;
flex-shrink: 1;
flex-basis: 40em;
padding: 1em;
overflow: auto;
}
.content > section.sidebar {
flex-grow: 0;
flex-shrink: 1;
flex-basis: 30ch;
}
/* ── Sidebar ── */
section.sidebar {
display: flex;
flex-flow: column wrap;
}
section.sidebar > div.backlinks { flex-grow: 1; }
.content > .sidebar ul { list-style: none; padding-left: 0; }
.content > .sidebar li { margin: 0.25rem 0; }
.content > .sidebar h3 {
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--black);
margin: 1rem 0 0.5rem;
}
/* ── Inline heading backlinks and references ── */
/* Clustered under each heading (topic.peb / wide.peb) rather than in the sidebar.
The renderer emits <section class="heading-backlinks"> / <section class="heading-refs">
after headings that have incoming links or external references. */
.heading-backlinks, .heading-refs {
margin: 0.5rem 0 1rem;
padding-left: 1rem;
border-left: 2px solid var(--medium-gray);
font-size: 0.85rem;
color: var(--dark-gray);
}
.heading-backlinks h4, .heading-refs h4 {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 0.25rem 0;
color: var(--black);
}
.heading-backlinks ul, .heading-refs ul {
list-style: none;
padding-left: 0;
}
.heading-backlinks li, .heading-refs li { margin: 0.15rem 0; }
.heading-backlinks a, .heading-refs a { color: var(--primary); }
.heading-backlinks a:visited, .heading-refs a:visited { color: var(--secondary); }
/* ── Mobile collapsible TOC ── */
/* Hidden on wide screens where the sidebar TOC is visible. Shown via the
narrow-viewport media query below. Indentation mirrors the sidebar rules
so the outline looks the same collapsed or expanded. */
.top-toc { display: none; }
.top-toc > summary, .wide .heading > summary {
font-family: "Vulf Sans", -apple-system, "Segoe UI", Roboto, sans-serif;
font-weight: 800;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--black);
margin: 1rem 0 0.5rem;
cursor: pointer;
}
ul.headings { list-style: none; padding-left: 0; }
ul.headings li { margin: 0.25rem 0; }
ul.headings li.level-2 { margin-left: 1em; }
ul.headings li.level-3 { margin-left: 2em; }
ul.headings li.level-4 { margin-left: 3em; }
ul.headings li.level-5 { margin-left: 4em; }
/* ── Images ── */
.content img {
display: block;
width: 80%;
margin: 0 auto;
}
/* ── Task states (org TODO keywords) ── */
.task.task-DONE::before { content: '\002611 '; }
.task.task-DONE { color: var(--success); }
.task.task-NEXT::before { content: '\01F195 '; }
.task.task-NEXT { color: var(--primary); }
.task.task-INPROGRESS::before { content: '\01F51C '; }
.task.task-INPROGRESS { color: var(--secondary); }
.task.task-WAITING::before { content: '\00231A '; }
.task.task-WAITING { color: var(--warning); }
.task.task-CANCELLED::before { content: '\002612 '; }
.task.task-CANCELLED { color: var(--alert); }
.task {
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-style: normal;
}
/* ── Code blocks ── */
pre, code, .verse, .latex, .math {
font-family: "Vulf Mono", monospace;
font-size: 0.9em;
}
pre {
background-color: var(--light-gray);
padding: 0.75rem 1rem;
overflow-x: auto;
}
pre code { background: none; padding: 0; font-style: normal; }
/* ── Source block figures (literate programming) ── */
figure.src-block {
margin: 1rem 0;
}
figure.src-block > pre {
margin: 0;
/* Let the highlight.js theme own the code block background. The theme CSS
targets `code.hljs` / `.hljs`; we undo arcology's light-gray pre bg so the
theme's bg shows through. */
background: transparent;
padding: 0;
}
figure.src-block > pre > code {
display: block;
padding: 0.75rem 1rem;
overflow-x: auto;
}
figcaption.src-block-meta {
font-family: "Vulf Mono", monospace;
font-size: 0.75rem;
font-style: normal;
color: var(--dark-gray);
background-color: var(--light-gray);
padding: 0.25rem 0.5rem;
border-bottom: 1px solid var(--medium-gray);
display: flex;
flex-flow: row wrap;
gap: 0.5rem;
}
figcaption.src-block-meta .src-lang {
font-weight: 800;
color: var(--black);
text-transform: uppercase;
}
figcaption.src-block-meta .src-arg {
white-space: nowrap;
}
figcaption.src-block-meta .src-name {
color: var(--primary);
font-weight: 600;
}
/* Noweb directive links inside code bodies */
.noweb-link a {
color: var(--primary);
text-decoration: underline;
text-decoration-style: dotted;
}
.noweb-link a:visited {
color: var(--secondary);
}
.noweb-link {
font-style: normal;
}
/* Noweb backlink footers at the bottom of named source blocks */
footer.src-block-backlinks {
font-family: "Vulf Mono", monospace;
font-size: 0.75rem;
font-style: normal;
color: var(--dark-gray);
background-color: var(--light-gray);
padding: 0.25rem 0.5rem;
border-top: 1px solid var(--medium-gray);
}
footer.src-block-backlinks a {
color: var(--primary);
text-decoration: underline;
text-decoration-style: dotted;
}
footer.src-block-backlinks a:visited {
color: var(--secondary);
}
/* ── Tables ── */
table { border-collapse: collapse; margin: 1rem 0; }
th, td { border: 1px solid var(--medium-gray); padding: 0.4rem 0.6rem; text-align: left; }
/* ── Blockquote, verse, fixed ── */
blockquote {
border-left: 3pt solid var(--secondary);
padding-left: 0.5em;
color: var(--dark-gray);
}
.verse { white-space: pre-wrap; }
.fixed { white-space: pre-wrap; }
/* ── Sidenotes: Tufte-style margin column ── */
.sidenote {
font-size: 0.8rem;
line-height: 1.3;
color: var(--dark-gray);
margin: 1rem 0;
padding-left: 1rem;
border-left: 2px solid var(--medium-gray);
}
.sidenote sup { font-weight: 600; color: var(--black); }
.margin-toggle { display: none; }
/* ── Narrow viewport: sidenotes collapse to inline toggled blocks ── */
@media (max-width: 768px) {
.content {
flex-direction: column;
max-width: 100%;
padding: 1rem;
}
.content > main { max-width: 100%; }
.content > .sidebar {
flex: 1 1 auto;
border-left: none;
border-top: 1px solid var(--medium-gray);
padding-left: 0;
padding-top: 1rem;
}
/* Hide the sidebar TOC on mobile — the collapsible .top-toc at the
top of the content block takes over. Other sidebar panels
(backlinks, tags, references, keywords) still stack below. */
section.sidebar > .headings { display: none; }
.top-toc { display: block; }
.margin-toggle { display: inline; }
.margin-toggle[type="checkbox"] { display: none; }
.margin-toggle + .sidenote-ref { display: inline; }
label.margin-toggle { cursor: pointer; color: var(--primary); font-size: 0.8rem; }
.sidenote {
display: none;
border-left: none;
padding: 0.5rem 0.75rem;
background: var(--light-gray);
}
.margin-toggle:checked ~ .sidenote { display: block; }
}
/* ── Misc ── */
.unpublished { color: #999; text-decoration: line-through; }
.hashtag { color: var(--dark-gray); }
.cloze { background: var(--light-gray); padding: 0 0.2rem; border-radius: 0.2rem; }vulf.css — Font Declarations
@font-face declarations for the Vulf Sans and Vulf Mono typefaces. The .woff2 files are served from /static/fonts/ (copied directly into src/jvmMain/resources/static/fonts/ as binary assets — they can't be tangled from org). Each family has six weights: Regular (500), Bold (800), Italic, Bold Italic, Light (300), Light Italic.
@font-face {
font-family: "Vulf Sans";
src: url('/static/fonts/VulfSans-Regular.woff2') format('woff2');
font-weight: 500;
}
@font-face {
font-family: "Vulf Mono";
src: url('/static/fonts/VulfMono-Regular.woff2') format('woff2');
font-weight: 500;
}
@font-face {
font-family: "Vulf Sans";
src: url('/static/fonts/VulfSans-Bold.woff2') format('woff2');
font-weight: 800;
}
@font-face {
font-family: "Vulf Mono";
src: url('/static/fonts/VulfMono-Bold.woff2') format('woff2');
font-weight: 800;
}
@font-face {
font-family: "Vulf Sans";
src: url('/static/fonts/VulfSans-Italic.woff2') format('woff2');
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: "Vulf Mono";
src: url('/static/fonts/VulfMono-Italic.woff2') format('woff2');
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: "Vulf Sans";
src: url('/static/fonts/VulfSans-Bold_Italic.woff2') format('woff2');
font-weight: 800;
font-style: italic;
}
@font-face {
font-family: "Vulf Mono";
src: url('/static/fonts/VulfMono-Bold_Italic.woff2') format('woff2');
font-weight: 800;
font-style: italic;
}
@font-face {
font-family: "Vulf Sans";
src: url('/static/fonts/VulfSans-Light.woff2') format('woff2');
font-weight: 300;
}
@font-face {
font-family: "Vulf Mono";
src: url('/static/fonts/VulfMono-Light.woff2') format('woff2');
font-weight: 300;
}
@font-face {
font-family: "Vulf Sans";
src: url('/static/fonts/VulfSans-Light_Italic.woff2') format('woff2');
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: "Vulf Mono";
src: url('/static/fonts/VulfMono-Light_Italic.woff2') format('woff2');
font-weight: 500;
font-style: italic;
}arcology-src-blocks.js — highlight.js plugin and noweb linking
This script does five things on page load:
Merges every =<script type="application/json" class="noweb-ref-map">= embedded in the rendered body into a single
ref → [anchors]map. The renderer emits one map per public render call; a list page with multiple entries may have several. Forward references (a<<ref>>that appears before the block definingref) work because all maps are read before linking runs.Registers language aliases via
hljs.registerAliasesso thatemacs-lispelispelsource blocks (common in this repo's literate Emacs config) highlight using the bundledlispgrammar, andfennelsource blocks highlight using the bundledschemegrammar. The bundledhighlight.min.jsshipslispandschemebut notemacs-lisporfennel; without the alias, =<code class="language-emacs-lisp">= and =<code class="language-fennel">= blocks get no highlighting.registerAliasesis the documented hljs API for mapping a language name to an existing grammar — preferred over abefore:highlightplugin hook, which is fragile when the source language isn't registered. emacs-lisp is dialect-compatible with the generic lisp grammar, and fennel with the scheme grammar, for highlighting purposes.Registers a highlight.js plugin via
hljs.addPluginwhoseafter:highlightElementhook walks the highlighted<code>element's text nodes, finds<<ref>>directives, and wraps each in an anchor link to the matching block anchor. Operating on text nodes after hljs has built its<span>tree preserves the syntax highlighting around the directive. When a ref resolves to multiple anchors (duplicate =#+name=s or =:noweb-ref=s), the link renders as<<ref[1][2]…>>with one numbered<a>per anchor.Calls
hljs.highlightAll()to highlight every =<pre><code class="language-X">= on the page.After highlighting, merges every =<script type="application/json" class="noweb-backlink-map">= into a
ref → [{anchor, label}, …]map and appends "embedded in" =<footer class="src-block-backlinks">= elements to named source blocks. This is the inverse of the forward links: a block definingfoogets a footer listing the blocks that consume<<foo>>, each linking back to the consumer's anchor. The renderer scans each named block's body for noweb-ref directives during the render pass and emits the backlink map at the end, so forward references (consumer before definition) work naturally.
The <<ref>> regex matches org noweb syntax: << followed by letters, digits, dashes, or underscores, followed by >>. Digits-only refs (like <<1>>) are not matched — org-mode noweb refs are identifier-like.
(function () {
"use strict";
function mergeRefMaps() {
var map = {};
var nodes = document.querySelectorAll('script.noweb-ref-map[type="application/json"]');
for (var i = 0; i < nodes.length; i++) {
var text = nodes[i].textContent || "";
if (text.trim() === "") continue;
try {
var parsed = JSON.parse(text);
} catch (e) {
console.warn("arcology-src-blocks: skipping malformed noweb-ref-map", e);
continue;
}
for (var ref in parsed) {
if (!Object.prototype.hasOwnProperty.call(parsed, ref)) continue;
if (!map[ref]) map[ref] = [];
for (var j = 0; j < parsed[ref].length; j++) {
map[ref].push(parsed[ref][j]);
}
}
}
return map;
}
var NOWEB_RE = /<<([A-Za-z][A-Za-z0-9_-]*)>>/g;
function linkNowebRefs(el, refMap) {
var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
acceptNode: function (node) {
return NOWEB_RE.test(node.nodeValue) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
});
var textNodes = [];
var n;
while ((n = walker.nextNode())) textNodes.push(n);
for (var i = 0; i < textNodes.length; i++) {
var node = textNodes[i];
var text = node.nodeValue;
var parent = node.parentNode;
var last = 0;
var match;
NOWEB_RE.lastIndex = 0;
var frag = document.createDocumentFragment();
var changed = false;
while ((match = NOWEB_RE.exec(text)) !== null) {
var ref = match[1];
var anchors = refMap[ref];
if (!anchors || anchors.length === 0) continue;
changed = true;
if (match.index > last) {
frag.appendChild(document.createTextNode(text.slice(last, match.index)));
}
var span = document.createElement("span");
span.className = "noweb-link";
span.appendChild(document.createTextNode("<<"));
if (anchors.length === 1) {
var a = document.createElement("a");
a.href = "#" + anchors[0];
a.textContent = ref;
span.appendChild(a);
} else {
span.appendChild(document.createTextNode(ref));
for (var k = 0; k < anchors.length; k++) {
var aN = document.createElement("a");
aN.href = "#" + anchors[k];
aN.textContent = "[" + (k + 1) + "]";
span.appendChild(aN);
}
}
span.appendChild(document.createTextNode(">>"));
frag.appendChild(span);
last = match.index + match[0].length;
}
if (!changed) continue;
if (last < text.length) {
frag.appendChild(document.createTextNode(text.slice(last)));
}
parent.replaceChild(frag, node);
}
}
function mergeBacklinkMaps() {
var map = {};
var nodes = document.querySelectorAll('script.noweb-backlink-map[type="application/json"]');
for (var i = 0; i < nodes.length; i++) {
var text = nodes[i].textContent || "";
if (text.trim() === "") continue;
try {
var parsed = JSON.parse(text);
} catch (e) {
console.warn("arcology-src-blocks: skipping malformed noweb-backlink-map", e);
continue;
}
for (var ref in parsed) {
if (!Object.prototype.hasOwnProperty.call(parsed, ref)) continue;
if (!map[ref]) map[ref] = [];
for (var j = 0; j < parsed[ref].length; j++) {
map[ref].push(parsed[ref][j]);
}
}
}
return map;
}
function renderBacklinks(backlinkMap) {
var blocks = document.querySelectorAll("figure.src-block[data-name]");
for (var i = 0; i < blocks.length; i++) {
var fig = blocks[i];
// A block may be known by both its #+name and its :noweb-ref. Check both
// against the backlink map and merge any hits.
var names = [];
var dataName = fig.getAttribute("data-name");
if (dataName) names.push(dataName);
var dataNowebRef = fig.getAttribute("data-noweb-ref");
if (dataNowebRef && dataNowebRef !== dataName) names.push(dataNowebRef);
var links = [];
for (var n = 0; n < names.length; n++) {
var entries = backlinkMap[names[n]];
if (!entries) continue;
for (var k = 0; k < entries.length; k++) {
links.push(entries[k]);
}
}
if (links.length === 0) continue;
var footer = document.createElement("footer");
footer.className = "src-block-backlinks";
footer.appendChild(document.createTextNode("embedded in: "));
for (var m = 0; m < links.length; m++) {
if (m > 0) footer.appendChild(document.createTextNode(", "));
var a = document.createElement("a");
a.href = "#" + links[m].anchor;
a.textContent = links[m].label;
footer.appendChild(a);
}
fig.appendChild(footer);
}
}
function init() {
if (typeof hljs === "undefined") {
console.warn("arcology-src-blocks: highlight.js not loaded");
return;
}
// The bundled highlight.min.js ships `lisp` and `scheme` but not
// `emacs-lisp` / `elisp` or `fennel`. Org source blocks in this repo
// frequently use `#+begin_src emacs-lisp` or `#+begin_src fennel`,
// which tangle to <code class="language-emacs-lisp"> /
// <code class="language-fennel">. Without an alias, hljs skips
// highlighting (the grammar lookup fails). registerAliases is
// the documented hljs API for mapping a language name to an existing
// grammar — preferred over a `before:highlight` plugin hook, which is
// fragile when the source language isn't registered. emacs-lisp is
// dialect-compatible with the generic lisp grammar and fennel with the
// scheme grammar for highlighting purposes (parens, symbols, keywords,
// strings).
if (hljs.registerAliases) {
hljs.registerAliases("emacs-lisp", { languageName: "lisp" });
hljs.registerAliases("elisp", { languageName: "lisp" });
hljs.registerAliases("el", { languageName: "lisp" });
hljs.registerAliases("fennel", { languageName: "scheme" });
}
var refMap = mergeRefMaps();
hljs.addPlugin({
"after:highlightElement": function (ctx) {
linkNowebRefs(ctx.el, refMap);
}
});
hljs.highlightAll();
// After highlighting, append "embedded in" backlink footers to named
// source blocks. This runs after highlightAll so the figure DOM is
// complete; it reads the backlink map the renderer embedded in the page.
renderBacklinks(mergeBacklinkMaps());
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();Per-Site Color Overrides
Each site has a CSS file that overrides the :root color variables for that site's palette. These are loaded after arcology.css and vulf.css so they override the defaults. The file paths are referenced in domains.json via the cssFile field.
lionsrear.css
:root {
--alert: #cc6960;
--primary: #707231;
--secondary: #beaa38;
--success: #67b4f8;
--warning: #7e5c41;
--white: #cfdcc2;
--light-gray: #87af87;
--medium-gray: #a0aa96;
--dark-gray: #82796c;
--black: #211f1c;
}garden.css
:root {
--alert: #cc6960;
--primary: #707231;
--secondary: #ebbe7b;
--success: #67b4f8;
--warning: #7e5c41;
--white: #fcf6ed;
--light-gray: #f6e5cb;
--medium-gray: #baad9b;
--dark-gray: #82796c;
--black: #211f1c;
}cce.css
:root {
--alert: #cc6960;
--primary: #707231;
--secondary: #ebbe7b;
--success: #67b4f8;
--warning: #7e5c41;
--white: #fcfcfc;
--light-gray: #dcdcec;
--medium-gray: #cacada;
--dark-gray: #808090;
--black: #211f1c;
}arcology-engine.css
The arcology site (engine) uses the same defaults as the base arcology.css — this file is identical to default-colors.css from Django, kept for consistency so every site has a CSS file.
:root {
--alert: #cc6960;
--primary: #707231;
--secondary: #ebbe7b;
--success: #67b4f8;
--warning: #7e5c41;
--white: #fcf6ed;
--light-gray: #f6e5cb;
--medium-gray: #baad9b;
--dark-gray: #82796c;
--black: #211f1c;
}Related Modules
Web Server — installs the
Pebbleplugin, buildsPageModel, returnsPebbleContentfrom route handlersPublishing Layer — provides
SiteMeta(title, cssFile, linkColor) consumed byapp.pebHTML Renderer — produces the
htmlContentandsidenotesHtmlstrings passed topage.pebDomain Map Configuration — the
domains.jsonsource for site metadata