Arcology Engine

Arcology Web Templates

Contents

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 — extends app.peb; renders page body + sidebar (default template)

  • wide.peb — extends app.peb; TOC inline at top, sidebar panels (backlinks, tags, references, keywords) stacked at bottom; references also inlined under each heading

  • topic.peb — extends app.peb; like wide.peb but backlinks AND references are inlined under each heading, sidebar backlinks/refs panels suppressed

  • sidebar.peb — partial: TOC + backlinks + tags + references + keywords

  • 404.peb — not-found page

  • sitemap.peb/ route index

  • feed.peb — Atom feed (rendered by the FeedPublisher, Phase 5)

  • robots.peb — robots.txt (stub for Phase 9)

Pebble filter notes:

  • |raw skips HTML escaping (Django's |safe equivalent). Used for htmlContent, 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.

html#+name: app-peb:tangle ../src/jvmMain/resources/templates/app.peb
<!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">
          &bull; <a class="internal" href="https://thelionsrear.com">Life</a>
          &bull; <a class="internal" href="https://arcology.garden">Tech</a>
          &bull; <a class="internal" href="https://cce.whatthefuck.computer">Emacs</a>
          &bull; <a class="internal" href="https://engine.arcology.garden">Arcology</a>
          &bull;
        </nav>
      </div>
    </header>

    <div class="content">
      {% block content %}{% endblock %}
    </div>

    <footer>
      <hr/>
      &copy; 02026 <a href="https://arcology.garden/people/rrix">Ryan Rix</a>
      &lt;<a href="mailto:site@whatthefuck.computer">site@whatthefuck.computer</a>&gt;

      <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">&larr;</a>
        <a href="https://fediring.net/">Fediring</a>
        <a href="https://fediring.net/next?host=arcology.garden">&rarr;</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.

html#+name: page-peb:tangle ../src/jvmMain/resources/templates/page.peb:noweb yes
{% 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.

html#+name: wide-peb:tangle ../src/jvmMain/resources/templates/wide.peb:noweb yes
{% 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.

html#+name: topic-peb:tangle ../src/jvmMain/resources/templates/topic.peb:noweb yes
{% 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 tags

  • references — 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.

html#+name: sidebar-heading-toc:noweb-ref sidebar-heading-toc
{% 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 %}
html#+name: sidebar-tags:noweb-ref sidebar-tags
{% 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 %}
html#+name: sidebar-refs:noweb-ref sidebar-refs
{% 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 %}
html#+name: sidebar-keywords:noweb-ref sidebar-keywords
{% 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.

html#+name: 404-peb:tangle ../src/jvmMain/resources/templates/404.peb
<!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.

html#+name: sitemap-peb:tangle ../src/jvmMain/resources/templates/sitemap.peb
<!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. &lt;p&gt;) — Pebble's default {{ }} escaping does this, so the body renders with {{ entry.html }} and no |raw.

xml#+name: feed-peb:tangle ../src/jvmMain/resources/templates/feed.peb:comments none
<?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)

text#+name: robots-peb:tangle ../src/jvmMain/resources/templates/robots.peb
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.

css#+name: arcology-css:tangle ../src/jvmMain/resources/static/arcology.css
/* ── 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.

css#+name: vulf-css:tangle ../src/jvmMain/resources/static/vulf.css
@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:

  1. 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 defining ref) work because all maps are read before linking runs.

  2. Registers language aliases via hljs.registerAliases so that emacs-lisp elisp el source blocks (common in this repo's literate Emacs config) highlight using the bundled lisp grammar, and fennel source blocks highlight using the bundled scheme grammar. The bundled highlight.min.js ships lisp and scheme but not emacs-lisp or fennel; without the alias, =<code class="language-emacs-lisp">= and =<code class="language-fennel">= blocks get no highlighting. 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.

  3. Registers a highlight.js plugin via hljs.addPlugin whose after:highlightElement hook 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.

  4. Calls hljs.highlightAll() to highlight every =<pre><code class="language-X">= on the page.

  5. 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 defining foo gets 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.

javascript#+name: arcology-src-blocks-js:tangle ../src/jvmMain/resources/static/arcology-src-blocks.js
(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

css#+name: lionsrear-css:tangle ../src/jvmMain/resources/static/arcology/css/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

css#+name: garden-css:tangle ../src/jvmMain/resources/static/arcology/css/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

css#+name: cce-css:tangle ../src/jvmMain/resources/static/arcology/css/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.

css#+name: arcology-engine-css:tangle ../src/jvmMain/resources/static/arcology/css/arcology-engine.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;
}

Related Modules

  • Web Server — installs the Pebble plugin, builds PageModel, returns PebbleContent from route handlers

  • Publishing Layer — provides SiteMeta (title, cssFile, linkColor) consumed by app.peb

  • HTML Renderer — produces the htmlContent and sidenotesHtml strings passed to page.peb

  • Domain Map Configuration — the domains.json source for site metadata