summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-07-11 12:08:14 -0600
committert <t@tjp.lol>2026-07-11 12:10:15 -0600
commit2ab0e23eca3c5cac82d948e506b3f1cb81bedd18 (patch)
tree46dc28dcea6265ece0f4763cdff372f84578e172
parent9faffbf6bc5083239d69a2daedac273e6d814bc3 (diff)
Migrate site pages to Weft templates and Markdown
-rw-r--r--.gitignore8
-rw-r--r--.stignore.shared1
-rw-r--r--_docs/PROSE_AUDIT.md22
-rw-r--r--about/index.html17
-rw-r--r--about/index.html.tmpl50
-rw-r--r--feed.xml2
-rw-r--r--feed.xml.tmpl20
-rw-r--r--index.html24
-rw-r--r--index.html.tmpl45
-rw-r--r--notes/_plain-html.html70
-rw-r--r--notes/_plain-html.md28
-rw-r--r--notes/index.html11
-rw-r--r--notes/index.html.tmpl31
-rw-r--r--projects/index.html12
-rw-r--r--projects/index.html.tmpl28
-rw-r--r--site.tmpl90
-rw-r--r--weblog/_moving-off-gemini.html88
-rw-r--r--weblog/_moving-off-gemini.md46
-rw-r--r--weblog/index.html13
-rw-r--r--weblog/index.html.tmpl27
20 files changed, 416 insertions, 217 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..19f2e83
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+# Weft outputs (generated in place at every depth).
+*.html
+/feed.xml
+/.weft-generated.json
+/.weft-txn-*
+
+# Device-local Syncthing configuration; .stignore.shared is synchronized.
+/.stignore
diff --git a/.stignore.shared b/.stignore.shared
new file mode 100644
index 0000000..2d19fc7
--- /dev/null
+++ b/.stignore.shared
@@ -0,0 +1 @@
+*.html
diff --git a/_docs/PROSE_AUDIT.md b/_docs/PROSE_AUDIT.md
index eafde4b..8dd3b2b 100644
--- a/_docs/PROSE_AUDIT.md
+++ b/_docs/PROSE_AUDIT.md
@@ -5,15 +5,15 @@ approve the copy for publication or supply replacement prose.
| Source | Prose locations | Disposition |
| --- | --- | --- |
-| `index.html` | document title and description; brand tagline; navigation labels; two introductory paragraphs; section headings; “all posts” label; elsewhere labels and destinations; footer text | Keep for manual review. Fictional post and tag listings were removed. “From the notes” is commented out. |
-| `about/index.html` | document title and description; navigation labels; About heading; two biography/contact paragraphs; Elsewhere heading, labels, and destinations; Colophon heading; colophon paragraph and six list items; footer text | Keep for manual rewrite. The colophon contains obsolete hand-written/no-build claims and must gain the planned licence and AI-assistance wording. |
-| `projects/index.html` | document title and description; navigation labels; introductory sentence; Maintained and Archived / for fun headings; footer text | Keep for manual review. Placeholder instructions, fictional project names/descriptions, tag labels, and archived-project labels were removed. |
-| `weblog/index.html` | document title and description; navigation labels; introductory paragraph and subscription label; footer text | Keep for manual review. Fictional year headings, dates, post titles, and links were removed. |
-| `weblog/_moving-off-gemini.html` | document title and description; navigation labels; post date and title; placeholder notice; all post headings, paragraphs, list items, quotation, code sample, and inline link labels; post navigation; footer text | Archived fictional reference only. Its tag labels were removed. Do not publish, list, render, or use as authored site copy. |
-| `notes/index.html` | document title and description; navigation labels; introductory paragraph; Start here and All notes headings; footer text | Keep hidden for manual review until real vault publication works. Fictional note labels and links were removed. |
-| `notes/_plain-html.html` | document title and description; navigation labels; tended/planted metadata and title; placeholder notice; all note headings, paragraphs, list items, wiki-link labels, backlinks heading and labels; footer text | Archived fictional reference only. Do not publish, list, render, or use as authored site copy. |
-| `feed.xml` | feed title; subtitle; author name and email | Keep for manual review. All fictional entry titles, summaries, dates, IDs, and links were removed; the feed is intentionally empty. |
+| `index.html.tmpl` | document title and description; brand tagline; navigation labels; two introductory paragraphs; section headings; “all posts” label; elsewhere labels and destinations; footer text | Migrated to a direct homepage template. Fictional post and tag listings were removed. “From the notes” is commented out. |
+| `about/index.html.tmpl` | document title and description; navigation labels; About heading; two biography/contact paragraphs; Elsewhere heading, labels, and destinations; Colophon heading; colophon paragraph and seven list items; footer text | Migrated to a direct template because its custom colophon structure and inline classes are not representable in raw-HTML-disabled Markdown. Obsolete hand-written/no-build claims were replaced with the generator, licence, and AI-assistance disclosure required by the plan. |
+| `projects/index.html.tmpl` | document title and description; navigation labels; introductory sentence; Maintained and Archived / for fun headings; footer text | Migrated to a direct index template. Placeholder instructions, fictional project names/descriptions, tag labels, and archived-project labels were removed. |
+| `weblog/index.html.tmpl` | document title and description; navigation labels; introductory paragraph and subscription label; footer text | Migrated to a direct index template. Fictional year headings, dates, post titles, and links were removed. |
+| `weblog/_moving-off-gemini.md` | document description; post date and title; placeholder notice; all post headings, paragraphs, list items, quotation, code sample, and inline link labels | Archived fictional Markdown reference only. Its tag labels were removed. The underscore prefix keeps it out of discovery, rendering, listings, and feeds. |
+| `notes/index.html.tmpl` | document title and description; navigation labels; introductory paragraph; Start here and All notes headings; footer text | Migrated to a direct index template and kept hidden until real vault publication works. Fictional note labels and links were removed. |
+| `notes/_plain-html.md` | document description; tended/planted metadata and title; placeholder notice; all note headings, paragraphs, list items, wiki-link labels, backlinks heading and labels | Archived fictional Markdown reference only. The underscore prefix keeps it out of discovery, rendering, and listings. |
+| `feed.xml.tmpl` | feed title; subtitle; author name and email | Migrated to a direct Atom template. All fictional entries remain removed; the feed is valid and intentionally empty until real weblog Markdown exists. |
-Repeated site-wide prose also needing one manual decision in the future shared
-templates: brand text, navigation labels, feed label, footer text, licence
-notice, and the currently commented Notes and webring labels.
+Repeated site-wide prose lives in `site.tmpl`: brand text, navigation labels,
+the “feed” label, the direct CC BY 4.0 footer notice, and the currently
+commented Notes and webring labels.
diff --git a/about/index.html b/about/index.html
index 71a8cd0..99f9a22 100644
--- a/about/index.html
+++ b/about/index.html
@@ -14,9 +14,9 @@
<h1 class="brand"><a href="../index.html">~tjp<span class="cur"> $_</span></a></h1>
<nav class="main">
<a href="../weblog/index.html">weblog</a>
- <!-- <a href="../notes/index.html">notes</a> -->
+
<a href="../projects/index.html">projects</a>
- <a href="../about/index.html" aria-current="page">about</a>
+ <a href="index.html" aria-current="page">about</a>
<a href="../feed.xml">feed</a>
</nav>
</header>
@@ -42,12 +42,13 @@
<div class="backlinks">
<h2>Colophon</h2>
<div class="note-body">
- <p>This site is hand-written HTML and a single <a href="../style.css">stylesheet</a>. There is no JavaScript anywhere &mdash; not deferred, not optional, none. It renders identically with scripts disabled because there is nothing to disable.</p>
+ <p>This site is generated from Markdown and Go templates by Weft, with a single <a href="../style.css">stylesheet</a>. There is no JavaScript anywhere &mdash; not deferred, not optional, none. It renders identically with scripts disabled because there is nothing to disable.</p>
<ul>
<li><strong>Type:</strong> your system's UI font for prose, your system's monospace for headings and code. No web fonts, no network requests for type.</li>
<li><strong>Colour:</strong> light and dark themes via <code>prefers-color-scheme</code> &mdash; it follows your OS setting, in pure CSS.</li>
- <li><strong>Weblog:</strong> written as gemtext in my <a href="../projects/index.html">capsule</a>, converted to HTML with a small script.</li>
+ <li><strong>Weblog:</strong> published from Markdown as static HTML.</li>
<li><strong>Notes:</strong> authored in Obsidian, synced across my devices with Syncthing, published as static pages.</li>
+ <li><strong>Build:</strong> Weft recursively renders Markdown and Go templates. AI may assist with coding and batch refactoring, but not with published post prose.</li>
<li><strong>Hosting:</strong> static files. Nothing to run, nothing to patch.</li>
<li><strong>Licence:</strong> content is <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a> unless a page says otherwise. Take it, remix it, just credit me.</li>
</ul>
@@ -57,13 +58,11 @@
</main>
<footer>
- <!-- <div class="ring">
- <span>webring:</span>
- <a href="#">&larr; prev</a><a href="#">rand</a><a href="#">next &rarr;</a>
- </div> -->
- <p>hand-written in slc &middot; &copy;2026 tjp &middot; <a href="../feed.xml">feed</a></p>
+
+ <p>&copy;2026 tjp &middot; content <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a> unless noted &middot; <a href="../feed.xml">feed</a></p>
</footer>
</div>
</body>
</html>
+
diff --git a/about/index.html.tmpl b/about/index.html.tmpl
new file mode 100644
index 0000000..065fdc3
--- /dev/null
+++ b/about/index.html.tmpl
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>About &amp; colophon — tjp.lol</title>
+<meta name="description" content="About tjp and how this site is built.">
+<link rel="stylesheet" href="../style.css">
+</head>
+<body>
+<div class="wrap">
+
+{{template "header" .}}
+ <main>
+ <article class="post">
+ <div class="post-head">
+ <h1>About</h1>
+ </div>
+ <div class="note-body">
+ <p>I'm tjp &mdash; an elder-millennial software engineer living in Salt Lake City, Utah. I write mostly Python, spend too long in the terminal, and care about software that's small, legible, and built to last.</p>
+ <p>This site is where I keep my <a href="../weblog/index.html">writing</a>. If you want to reach me, <a href="mailto:t@tjp.lol">t@tjp.lol</a> is the best way.</p>
+
+ <h2>Elsewhere</h2>
+ <ul>
+ <li><span class="mono">email</span> &mdash; <a href="mailto:t@tjp.lol">t@tjp.lol</a></li>
+ <li><span class="mono">code</span> &nbsp;&mdash; <a href="https://github.com/teepark">github.com/teepark</a></li>
+ <li><span class="mono">fedi</span> &nbsp;&mdash; <a href="https://tilde.zone/@tjpcc" rel="me">@tjpcc@tilde.zone</a></li>
+ <li><span class="mono">gemini</span> &mdash; <code>gemini://gemini.ctrl-c.club/~tjp/</code></li>
+ </ul>
+ </div>
+
+ <div class="backlinks">
+ <h2>Colophon</h2>
+ <div class="note-body">
+ <p>This site is generated from Markdown and Go templates by Weft, with a single <a href="../style.css">stylesheet</a>. There is no JavaScript anywhere &mdash; not deferred, not optional, none. It renders identically with scripts disabled because there is nothing to disable.</p>
+ <ul>
+ <li><strong>Type:</strong> your system's UI font for prose, your system's monospace for headings and code. No web fonts, no network requests for type.</li>
+ <li><strong>Colour:</strong> light and dark themes via <code>prefers-color-scheme</code> &mdash; it follows your OS setting, in pure CSS.</li>
+ <li><strong>Weblog:</strong> published from Markdown as static HTML.</li>
+ <li><strong>Notes:</strong> authored in Obsidian, synced across my devices with Syncthing, published as static pages.</li>
+ <li><strong>Build:</strong> Weft recursively renders Markdown and Go templates. AI may assist with coding and batch refactoring, but not with published post prose.</li>
+ <li><strong>Hosting:</strong> static files. Nothing to run, nothing to patch.</li>
+ <li><strong>Licence:</strong> content is <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a> unless a page says otherwise. Take it, remix it, just credit me.</li>
+ </ul>
+ </div>
+ </div>
+ </article>
+ </main>
+
+{{template "footer" .}}
diff --git a/feed.xml b/feed.xml
index 5e213eb..ebbad13 100644
--- a/feed.xml
+++ b/feed.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>tjp.lol</title>
<subtitle>Notes, code, and longer thoughts from an elder-millennial software engineer in Salt Lake City.</subtitle>
@@ -10,4 +9,5 @@
<name>tjp</name>
<email>t@tjp.lol</email>
</author>
+
</feed>
diff --git a/feed.xml.tmpl b/feed.xml.tmpl
new file mode 100644
index 0000000..961ee41
--- /dev/null
+++ b/feed.xml.tmpl
@@ -0,0 +1,20 @@
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <title>tjp.lol</title>
+ <subtitle>Notes, code, and longer thoughts from an elder-millennial software engineer in Salt Lake City.</subtitle>
+ <link href="https://tjp.lol/feed.xml" rel="self"/>
+ <link href="https://tjp.lol/"/>
+ {{$posts := sortPages (index .Pages "weblog") "post_date"}}{{with slicePages $posts 0 1}}<updated>{{range .}}{{rfc3339 (index .Meta "post_date")}}{{end}}</updated>{{else}}<updated>2026-07-11T00:00:00Z</updated>{{end}}
+ <id>https://tjp.lol/</id>
+ <author>
+ <name>tjp</name>
+ <email>t@tjp.lol</email>
+ </author>
+ {{range $posts}}<entry>
+ <title>{{.Title}}</title>
+ <link href="{{.CanonicalURL}}"/>
+ <id>{{.CanonicalURL}}</id>
+ <published>{{rfc3339 (index .Meta "post_date")}}</published>
+ <updated>{{if index .Meta "updated"}}{{rfc3339 (index .Meta "updated")}}{{else}}{{rfc3339 (index .Meta "post_date")}}{{end}}</updated>
+ <content type="html">{{xml (joinSections .Sections)}}</content>
+ </entry>{{end}}
+</feed>
diff --git a/index.html b/index.html
index 6acaad8..fe7b5ee 100644
--- a/index.html
+++ b/index.html
@@ -16,7 +16,7 @@
<p class="tag">Elder-millennial software engineer in Salt Lake City, Utah. Writing about code, tools, and the small web.</p>
<nav class="main">
<a href="weblog/index.html">weblog</a>
- <!-- <a href="notes/index.html">notes</a> -->
+
<a href="projects/index.html">projects</a>
<a href="about/index.html">about</a>
<a href="feed.xml">feed</a>
@@ -26,7 +26,7 @@
<main>
<section class="intro">
<p>Welcome in. This site is my writing desk: weblog posts moving over from my <span class="mono">gemini://</span> capsule, plus a public cut of the notes I keep in Obsidian and sync across my machines with Syncthing.</p>
- <p>Plain HTML and CSS. No JavaScript, no build step, no tracking &mdash; it reads exactly the same with scripts turned off. That's the point.</p>
+ <p>Plain HTML and CSS. No JavaScript, no tracking &mdash; it reads exactly the same with scripts turned off. That's the point.</p>
</section>
<section>
@@ -44,28 +44,16 @@
<li><span class="k">gemini</span><a href="about/index.html">gemini://…/~tjp</a></li>
</ul>
</section>
- <!-- <section>
- <h2 class="sec">from the notes</h2>
- <ul class="links">
- <li><a href="notes/plain-html.html">The case for plain HTML</a></li>
- <li><a href="notes/index.html">Home-lab &amp; self-hosting</a></li>
- <li><a href="notes/index.html">Emacs config, annotated</a></li>
- <li><a href="notes/index.html">Books I keep re-reading</a></li>
- </ul>
- </section> -->
+
</div>
</main>
<footer>
- <!-- <div class="ring">
- <span>webring:</span>
- <a href="#">&larr; prev</a>
- <a href="#">rand</a>
- <a href="#">next &rarr;</a>
- </div> -->
- <p>hand-written in slc &middot; &copy;2026 tjp &middot; content <a href="about/index.html">CC BY 4.0</a> unless noted &middot; <a href="feed.xml">feed</a></p>
+
+ <p>&copy;2026 tjp &middot; content <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a> unless noted &middot; <a href="feed.xml">feed</a></p>
</footer>
</div>
</body>
</html>
+
diff --git a/index.html.tmpl b/index.html.tmpl
new file mode 100644
index 0000000..83a9a4f
--- /dev/null
+++ b/index.html.tmpl
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>tjp.lol</title>
+<meta name="description" content="Personal site of tjp — software engineer in Salt Lake City. Weblog, notes, projects.">
+<link rel="stylesheet" href="style.css">
+<link rel="alternate" type="application/atom+xml" title="tjp.lol" href="feed.xml">
+</head>
+<body>
+<div class="wrap">
+
+{{template "header" .}}
+ <main>
+ <section class="intro">
+ <p>Welcome in. This site is my writing desk: weblog posts moving over from my <span class="mono">gemini://</span> capsule, plus a public cut of the notes I keep in Obsidian and sync across my machines with Syncthing.</p>
+ <p>Plain HTML and CSS. No JavaScript, no tracking &mdash; it reads exactly the same with scripts turned off. That's the point.</p>
+ </section>
+
+ <section>
+ <h2 class="sec">recent posts</h2>
+ {{$posts := sortPages (index .Pages "weblog") "post_date"}}{{with $posts}}<ul class="posts">
+ {{range slicePages . 0 5}}<li><time datetime="{{date (index .Meta "post_date") "2006-01-02"}}">{{date (index .Meta "post_date") "2006-01-02"}}</time><a href="{{rel "index.html" .OutputPath}}">{{.Title}}</a></li>{{end}}
+ </ul>
+ {{end}}<p style="margin-top:.9rem;font-size:.9rem;"><a href="weblog/index.html">all posts &rarr;</a></p>
+ </section>
+
+ <div class="cols">
+ <section>
+ <h2 class="sec">elsewhere</h2>
+ <ul class="links">
+ <li><span class="k">email</span><a href="mailto:t@tjp.lol">t@tjp.lol</a></li>
+ <li><span class="k">code</span><a href="https://github.com/teepark">github.com/teepark</a></li>
+ <li><span class="k">fedi</span><a href="https://tilde.zone/@tjpcc" rel="me">@tjpcc@tilde.zone</a></li>
+ <li><span class="k">gemini</span><a href="about/index.html">gemini://…/~tjp</a></li>
+ </ul>
+ </section>
+ <!-- <section>
+ <h2 class="sec">from the notes</h2>
+ </section> -->
+ </div>
+ </main>
+
+{{template "footer" .}}
diff --git a/notes/_plain-html.html b/notes/_plain-html.html
deleted file mode 100644
index d60275e..0000000
--- a/notes/_plain-html.html
+++ /dev/null
@@ -1,70 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>The case for plain HTML — notes — tjp.lol</title>
-<meta name="description" content="A note on writing plain, durable HTML.">
-<link rel="stylesheet" href="../style.css">
-</head>
-<body>
-<div class="wrap">
-
- <header class="site">
- <h1 class="brand"><a href="../index.html">~tjp<span class="cur"> $_</span></a></h1>
- <nav class="main">
- <a href="../weblog/index.html">weblog</a>
- <a href="../notes/index.html" aria-current="page">notes</a>
- <a href="../projects/index.html">projects</a>
- <a href="../about/index.html">about</a>
- <a href="../feed.xml">rss</a>
- </nav>
- </header>
-
- <main>
- <article class="post">
- <div class="post-head">
- <p class="meta"><a href="../notes/index.html" style="text-decoration:none;">&larr; notes</a> &nbsp;/&nbsp; tended <time datetime="2026-05-30">2026-05-30</time> &middot; planted <time datetime="2024-08-11">2024-08-11</time></p>
- <h1>The case for plain HTML</h1>
- </div>
-
- <div class="note-body">
- <p><em>A note, not an essay &mdash; expect it to change. This page shows how a wiki note reads: internal <a class="wiki" href="index.html">[[wiki links]]</a> render as underlined-dotted links, and a backlinks panel sits at the bottom.</em></p>
-
- <p>A hand-written HTML page is the most durable document format I know of that still looks like a document. It has no runtime, no dependencies, and no version that can go out of date. A file I wrote in 2004 opens today, pixel-for-pixel, in a browser that didn't exist then.</p>
-
- <p>This connects to <a class="wiki" href="index.html">Gemtext &amp; the small web</a> &mdash; both are bets that <em>constraint outlives capability</em>. See also <a class="wiki" href="index.html">How I take notes</a> for why I keep the source in plain text.</p>
-
- <h2>Things I no longer do</h2>
- <ul>
- <li>Reach for a framework before there's a second page.</li>
- <li>Add a build step to save five keystrokes.</li>
- <li>Load a web font when the reader already owns a good one.</li>
- </ul>
-
- <h2>The test</h2>
- <p>Turn JavaScript off. If the page is worse, I've added fragility I have to maintain forever. If it's identical, I'm done. This whole site passes that test &mdash; see the <a class="wiki" href="../about/index.html">colophon</a>.</p>
- </div>
-
- <div class="backlinks">
- <h2>Linked from</h2>
- <ul class="posts">
- <li><a href="index.html">Gemtext &amp; the small web</a></li>
- <li><a href="index.html">Home-lab &amp; self-hosting</a></li>
- <li><a href="../weblog/moving-off-gemini.html">Moving my weblog off Gemini (without leaving it)</a></li>
- </ul>
- </div>
- </article>
- </main>
-
- <footer>
- <div class="ring">
- <span>webring:</span>
- <a href="#">&larr; prev</a><a href="#">rand</a><a href="#">next &rarr;</a>
- </div>
- <p>hand-written in slc &middot; &copy;2026 tjp &middot; <a href="../feed.xml">feed</a></p>
- </footer>
-
-</div>
-</body>
-</html>
diff --git a/notes/_plain-html.md b/notes/_plain-html.md
new file mode 100644
index 0000000..6d6e078
--- /dev/null
+++ b/notes/_plain-html.md
@@ -0,0 +1,28 @@
+---
+description: A note on writing plain, durable HTML.
+planted: 2024-08-11
+tended: 2026-05-30
+---
+# The case for plain HTML
+
+*A note, not an essay — expect it to change. This page shows how a wiki note reads: internal wiki links render as underlined-dotted links, and a backlinks panel sits at the bottom.*
+
+A hand-written HTML page is the most durable document format I know of that still looks like a document. It has no runtime, no dependencies, and no version that can go out of date. A file I wrote in 2004 opens today, pixel-for-pixel, in a browser that didn't exist then.
+
+This connects to [Gemtext & the small web](index.html) — both are bets that *constraint outlives capability*. See also [How I take notes](index.html) for why I keep the source in plain text.
+
+## Things I no longer do
+
+- Reach for a framework before there's a second page.
+- Add a build step to save five keystrokes.
+- Load a web font when the reader already owns a good one.
+
+## The test
+
+Turn JavaScript off. If the page is worse, I've added fragility I have to maintain forever. If it's identical, I'm done. This whole site passes that test — see the [colophon](../about/index.html).
+
+## Linked from
+
+- [Gemtext & the small web](index.html)
+- [Home-lab & self-hosting](index.html)
+- [Moving my weblog off Gemini (without leaving it)](../weblog/moving-off-gemini.html)
diff --git a/notes/index.html b/notes/index.html
index 1a8db13..ea988cf 100644
--- a/notes/index.html
+++ b/notes/index.html
@@ -14,7 +14,7 @@
<h1 class="brand"><a href="../index.html">~tjp<span class="cur"> $_</span></a></h1>
<nav class="main">
<a href="../weblog/index.html">weblog</a>
- <!-- <a href="../notes/index.html" aria-current="page">notes</a> -->
+
<a href="../projects/index.html">projects</a>
<a href="../about/index.html">about</a>
<a href="../feed.xml">feed</a>
@@ -32,17 +32,16 @@
<section>
<h2 class="sec">all notes</h2>
+
</section>
</main>
<footer>
- <!-- <div class="ring">
- <span>webring:</span>
- <a href="#">&larr; prev</a><a href="#">rand</a><a href="#">next &rarr;</a>
- </div> -->
- <p>hand-written in slc &middot; &copy;2026 tjp &middot; <a href="../feed.xml">feed</a></p>
+
+ <p>&copy;2026 tjp &middot; content <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a> unless noted &middot; <a href="../feed.xml">feed</a></p>
</footer>
</div>
</body>
</html>
+
diff --git a/notes/index.html.tmpl b/notes/index.html.tmpl
new file mode 100644
index 0000000..468a466
--- /dev/null
+++ b/notes/index.html.tmpl
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Notes — tjp.lol</title>
+<meta name="description" content="A public cut of my Obsidian notes — a small digital garden.">
+<link rel="stylesheet" href="../style.css">
+</head>
+<body>
+<div class="wrap">
+
+{{template "header" .}}
+ <main>
+ <section class="intro">
+ <p>This is a digital garden &mdash; a public slice of the notes I keep in Obsidian. Unlike the <a href="../weblog/index.html">weblog</a>, these aren't essays. They're living pages: some are a paragraph, some are years old, most link to each other. Start anywhere and follow the threads.</p>
+ </section>
+
+ <section>
+ <h2 class="sec">start here</h2>
+ </section>
+
+ <section>
+ <h2 class="sec">all notes</h2>
+ {{$notes := sortPages (index .Pages "notes") "mod_time"}}{{with $notes}}<div class="note-index">
+ {{range .}}<a href="{{rel "notes/index.html" .OutputPath}}">{{.Title}}</a>{{end}}
+ </div>{{end}}
+ </section>
+ </main>
+
+{{template "footer" .}}
diff --git a/projects/index.html b/projects/index.html
index 0943b48..6bc0dc5 100644
--- a/projects/index.html
+++ b/projects/index.html
@@ -14,8 +14,8 @@
<h1 class="brand"><a href="../index.html">~tjp<span class="cur"> $_</span></a></h1>
<nav class="main">
<a href="../weblog/index.html">weblog</a>
- <!-- <a href="../notes/index.html">notes</a> -->
- <a href="../projects/index.html" aria-current="page">projects</a>
+
+ <a href="index.html" aria-current="page">projects</a>
<a href="../about/index.html">about</a>
<a href="../feed.xml">feed</a>
</nav>
@@ -36,13 +36,11 @@
</main>
<footer>
- <!-- <div class="ring">
- <span>webring:</span>
- <a href="#">&larr; prev</a><a href="#">rand</a><a href="#">next &rarr;</a>
- </div> -->
- <p>hand-written in slc &middot; &copy;2026 tjp &middot; <a href="../feed.xml">feed</a></p>
+
+ <p>&copy;2026 tjp &middot; content <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a> unless noted &middot; <a href="../feed.xml">feed</a></p>
</footer>
</div>
</body>
</html>
+
diff --git a/projects/index.html.tmpl b/projects/index.html.tmpl
new file mode 100644
index 0000000..a99a10b
--- /dev/null
+++ b/projects/index.html.tmpl
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Projects — tjp.lol</title>
+<meta name="description" content="Things tjp has built.">
+<link rel="stylesheet" href="../style.css">
+</head>
+<body>
+<div class="wrap">
+
+{{template "header" .}}
+ <main>
+ <section class="intro">
+ <p>Small things I've built and still maintain. Most live on <a href="https://github.com/teepark">GitHub</a>.</p>
+ </section>
+
+ <section>
+ <h2 class="sec">maintained</h2>
+ </section>
+
+ <section>
+ <h2 class="sec">archived / for fun</h2>
+ </section>
+ </main>
+
+{{template "footer" .}}
diff --git a/site.tmpl b/site.tmpl
new file mode 100644
index 0000000..6c60443
--- /dev/null
+++ b/site.tmpl
@@ -0,0 +1,90 @@
+{{define "head"}}<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>{{if eq .OutputPath "about/index.html"}}About &amp; colophon{{else}}{{.Title}}{{end}} — tjp.lol</title>
+{{with index .Meta "description"}}<meta name="description" content="{{.}}">
+{{end}}<link rel="stylesheet" href="{{rel .OutputPath "style.css"}}">
+{{if or (eq .OutputPath "weblog/index.html") (index .Meta "post_date")}}<link rel="alternate" type="application/atom+xml" title="tjp.lol" href="{{rel .OutputPath "feed.xml"}}">
+{{end}}</head>
+<body>
+<div class="wrap">
+{{end}}
+
+{{define "header"}} <header class="site">
+ <h1 class="brand"><a href="{{rel .OutputPath "index.html"}}">~tjp<span class="cur"> $_</span></a></h1>
+ {{if eq .OutputPath "index.html"}}<p class="tag">Elder-millennial software engineer in Salt Lake City, Utah. Writing about code, tools, and the small web.</p>
+ {{end}}<nav class="main">
+ <a href="{{rel .OutputPath "weblog/index.html"}}"{{if eq .OutputPath "weblog/index.html"}} aria-current="page"{{end}}>weblog</a>
+ <!-- <a href="{{rel .OutputPath "notes/index.html"}}"{{if eq .OutputPath "notes/index.html"}} aria-current="page"{{end}}>notes</a> -->
+ <a href="{{rel .OutputPath "projects/index.html"}}"{{if eq .OutputPath "projects/index.html"}} aria-current="page"{{end}}>projects</a>
+ <a href="{{rel .OutputPath "about/index.html"}}"{{if eq .OutputPath "about/index.html"}} aria-current="page"{{end}}>about</a>
+ <a href="{{rel .OutputPath "feed.xml"}}">feed</a>
+ </nav>
+ </header>
+{{end}}
+
+{{define "footer"}} <footer>
+ <!-- <div class="ring">
+ <span>webring:</span>
+ <a href="#">&larr; prev</a><a href="#">rand</a><a href="#">next &rarr;</a>
+ </div> -->
+ <p>&copy;2026 tjp &middot; content <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a> unless noted &middot; <a href="{{rel .OutputPath "feed.xml"}}">feed</a></p>
+ </footer>
+
+</div>
+</body>
+</html>
+{{end}}
+
+{{define "page"}}{{template "head" .}}{{template "header" .}}
+ <main>
+ <article class="post">
+ <div class="post-head">
+ <h1>{{.Title}}</h1>
+ </div>
+ <div class="note-body">
+ {{joinSections .Sections}}
+ </div>
+ </article>
+ </main>
+
+{{template "footer" .}}{{end}}
+
+{{define "weblog"}}{{template "head" .}}{{template "header" .}}
+ <main>
+ <article class="post">
+ <div class="post-head">
+ <p class="meta"><a href="{{rel .OutputPath "weblog/index.html"}}" style="text-decoration:none;">&larr; weblog</a> &nbsp;/&nbsp; <time datetime="{{date (index .Meta "post_date") "2006-01-02"}}">{{date (index .Meta "post_date") "2006-01-02"}}</time></p>
+ <h1>{{.Title}}</h1>
+ </div>
+
+ {{with index .Sections 0}}<div style="font-size:1.2em">{{.}}</div>{{end}}
+ {{joinSections (slice .Sections 1)}}
+ </article>
+
+ <nav class="post-nav">
+ <a href="{{rel .OutputPath "weblog/index.html"}}">&larr; Older</a>
+ <a href="{{rel .OutputPath "weblog/index.html"}}">All posts</a>
+ <a href="{{rel .OutputPath "weblog/index.html"}}">Newer &rarr;</a>
+ </nav>
+ </main>
+
+{{template "footer" .}}{{end}}
+
+{{define "note"}}{{template "head" .}}{{template "header" .}}
+ <main>
+ <article class="post">
+ <div class="post-head">
+ <p class="meta"><a href="{{rel .OutputPath "notes/index.html"}}" style="text-decoration:none;">&larr; notes</a></p>
+ <h1>{{.Title}}</h1>
+ </div>
+
+ <div class="note-body">
+ {{joinSections .Sections}}
+ </div>
+ </article>
+ </main>
+
+{{template "footer" .}}{{end}}
diff --git a/weblog/_moving-off-gemini.html b/weblog/_moving-off-gemini.html
deleted file mode 100644
index fdf6165..0000000
--- a/weblog/_moving-off-gemini.html
+++ /dev/null
@@ -1,88 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Moving my weblog off Gemini (without leaving it) — tjp.lol</title>
-<meta name="description" content="How I mirror my Gemini capsule to the web without abandoning geminispace.">
-<link rel="stylesheet" href="../style.css">
-<link rel="alternate" type="application/atom+xml" title="tjp.lol" href="../feed.xml">
-</head>
-<body>
-<div class="wrap">
-
- <header class="site">
- <h1 class="brand"><a href="../index.html">~tjp<span class="cur"> $_</span></a></h1>
- <nav class="main">
- <a href="../weblog/index.html" aria-current="page">weblog</a>
- <a href="../notes/index.html">notes</a>
- <a href="../projects/index.html">projects</a>
- <a href="../about/index.html">about</a>
- <a href="../feed.xml">rss</a>
- </nav>
- </header>
-
- <main>
- <article class="post">
- <div class="post-head">
- <p class="meta"><a href="../weblog/index.html" style="text-decoration:none;">&larr; weblog</a> &nbsp;/&nbsp; <time datetime="2026-06-30">2026-06-30</time></p>
- <h1>Moving my weblog off Gemini (without leaving it)</h1>
- </div>
-
- <p><em>This is placeholder text laid out so you can see how a full post reads. Replace it with the real <span class="mono">.gmi</span> content when you export the capsule &mdash; the structure below covers everything a converted Gemini post will need: headings, lists, quotes, code, and links.</em></p>
-
- <p>I've kept a capsule at <code>gemini://gemini.ctrl-c.club/~tjp/</code> for a few years now. Gemini is a lovely place to write: the format is so constrained that there's nothing to fiddle with, and the whole thing rewards plain, linear prose. But I wanted a web presence too &mdash; one that fits in with the personal sites I keep finding in webrings.</p>
-
- <h2>The plan</h2>
- <p>The goal was never to abandon geminispace. It was to have both, from one source of truth. Gemtext is close enough to Markdown that a small converter gets you most of the way there:</p>
-
- <ul>
- <li>Keep writing <code>.gmi</code> files in my capsule.</li>
- <li>Run a converter that emits HTML with the same structure.</li>
- <li>Wrap each page in the site template you're reading now.</li>
- </ul>
-
- <h2>The converter</h2>
- <p>Gemtext has exactly one block-level construct per line, which makes parsing almost insultingly simple. Here's the core of it:</p>
-
- <pre><code>while IFS= read -r line; do
- case "$line" in
- "### "*) echo "&lt;h3&gt;${line#### }&lt;/h3&gt;" ;;
- "## "*) echo "&lt;h2&gt;${line### }&lt;/h2&gt;" ;;
- "# "*) echo "&lt;h1&gt;${line## }&lt;/h1&gt;" ;;
- "=> "*) emit_link "$line" ;;
- "* "*) emit_list_item "$line" ;;
- "&gt; "*) echo "&lt;blockquote&gt;${line#&gt; }&lt;/blockquote&gt;" ;;
- "") echo "" ;;
- *) echo "&lt;p&gt;$line&lt;/p&gt;" ;;
- esac
-done &lt; "$1"</code></pre>
-
- <p>That's genuinely most of it. The fiddly parts are link lines (<code>=&gt;</code> can be a bare URL or have link text) and collapsing runs of <code>* </code> lines into a single <code>&lt;ul&gt;</code>.</p>
-
- <blockquote>The best format is the one that outlives the tool you wrote it in. Gemtext will still be readable when this converter is long gone.</blockquote>
-
- <h2>What I kept</h2>
- <p>No client-side anything. The web version renders identically with JavaScript disabled, because there is no JavaScript to disable. Fonts are whatever your system already has. The whole page is a few kilobytes.</p>
-
- <p>If you want the source, it lives with the rest of my <a href="../projects/index.html">projects</a>. And the capsule is still there &mdash; see the <a href="../about/index.html">colophon</a> for how it all fits together.</p>
- </article>
-
- <nav class="post-nav">
- <a href="../weblog/index.html">&larr; Older</a>
- <a href="../weblog/index.html">All posts</a>
- <a href="../weblog/index.html">Newer &rarr;</a>
- </nav>
- </main>
-
- <footer>
- <div class="ring">
- <span>webring:</span>
- <a href="#">&larr; prev</a><a href="#">rand</a><a href="#">next &rarr;</a>
- </div>
- <p>hand-written in slc &middot; &copy;2026 tjp &middot; content <a href="../about/index.html">CC BY 4.0</a> &middot; <a href="../feed.xml">feed</a></p>
- </footer>
-
-</div>
-</body>
-</html>
diff --git a/weblog/_moving-off-gemini.md b/weblog/_moving-off-gemini.md
new file mode 100644
index 0000000..e0cafe0
--- /dev/null
+++ b/weblog/_moving-off-gemini.md
@@ -0,0 +1,46 @@
+---
+post_date: 2026-06-30
+description: How I mirror my Gemini capsule to the web without abandoning geminispace.
+---
+# Moving my weblog off Gemini (without leaving it)
+
+*This is placeholder text laid out so you can see how a full post reads. Replace it with the real `.gmi` content when you export the capsule — the structure below covers everything a converted Gemini post will need: headings, lists, quotes, code, and links.*
+
+I've kept a capsule at `gemini://gemini.ctrl-c.club/~tjp/` for a few years now. Gemini is a lovely place to write: the format is so constrained that there's nothing to fiddle with, and the whole thing rewards plain, linear prose. But I wanted a web presence too — one that fits in with the personal sites I keep finding in webrings.
+
+## The plan
+
+The goal was never to abandon geminispace. It was to have both, from one source of truth. Gemtext is close enough to Markdown that a small converter gets you most of the way there:
+
+- Keep writing `.gmi` files in my capsule.
+- Run a converter that emits HTML with the same structure.
+- Wrap each page in the site template you're reading now.
+
+## The converter
+
+Gemtext has exactly one block-level construct per line, which makes parsing almost insultingly simple. Here's the core of it:
+
+```sh
+while IFS= read -r line; do
+ case "$line" in
+ "### "*) echo "<h3>${line#### }</h3>" ;;
+ "## "*) echo "<h2>${line### }</h2>" ;;
+ "# "*) echo "<h1>${line## }</h1>" ;;
+ "=> "*) emit_link "$line" ;;
+ "* "*) emit_list_item "$line" ;;
+ "> "*) echo "<blockquote>${line#> }</blockquote>" ;;
+ "") echo "" ;;
+ *) echo "<p>$line</p>" ;;
+ esac
+done < "$1"
+```
+
+That's genuinely most of it. The fiddly parts are link lines (`=>` can be a bare URL or have link text) and collapsing runs of `* ` lines into a single `<ul>`.
+
+> The best format is the one that outlives the tool you wrote it in. Gemtext will still be readable when this converter is long gone.
+
+## What I kept
+
+No client-side anything. The web version renders identically with JavaScript disabled, because there is no JavaScript to disable. Fonts are whatever your system already has. The whole page is a few kilobytes.
+
+If you want the source, it lives with the rest of my [projects](../projects/index.html). And the capsule is still there — see the [colophon](../about/index.html) for how it all fits together.
diff --git a/weblog/index.html b/weblog/index.html
index 8510f60..f67da6b 100644
--- a/weblog/index.html
+++ b/weblog/index.html
@@ -14,8 +14,8 @@
<header class="site">
<h1 class="brand"><a href="../index.html">~tjp<span class="cur"> $_</span></a></h1>
<nav class="main">
- <a href="../weblog/index.html" aria-current="page">weblog</a>
- <!-- <a href="../notes/index.html">notes</a> -->
+ <a href="index.html" aria-current="page">weblog</a>
+
<a href="../projects/index.html">projects</a>
<a href="../about/index.html">about</a>
<a href="../feed.xml">feed</a>
@@ -27,16 +27,15 @@
<p>Longer-form writing. Most of these are migrating over from my Gemini capsule, so dates reflect the original posting. <a href="../feed.xml">Subscribe via RSS</a>.</p>
</section>
+
</main>
<footer>
- <!-- <div class="ring">
- <span>webring:</span>
- <a href="#">&larr; prev</a><a href="#">rand</a><a href="#">next &rarr;</a>
- </div> -->
- <p>hand-written in slc &middot; &copy;2026 tjp &middot; <a href="../feed.xml">feed</a></p>
+
+ <p>&copy;2026 tjp &middot; content <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a> unless noted &middot; <a href="../feed.xml">feed</a></p>
</footer>
</div>
</body>
</html>
+
diff --git a/weblog/index.html.tmpl b/weblog/index.html.tmpl
new file mode 100644
index 0000000..9d97024
--- /dev/null
+++ b/weblog/index.html.tmpl
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Weblog — tjp.lol</title>
+<meta name="description" content="Weblog posts by tjp.">
+<link rel="stylesheet" href="../style.css">
+<link rel="alternate" type="application/atom+xml" title="tjp.lol" href="../feed.xml">
+</head>
+<body>
+<div class="wrap">
+
+{{template "header" .}}
+ <main>
+ <section class="intro">
+ <p>Longer-form writing. Most of these are migrating over from my Gemini capsule, so dates reflect the original posting. <a href="../feed.xml">Subscribe via RSS</a>.</p>
+ </section>
+
+ {{$posts := sortPages (index .Pages "weblog") "post_date"}}{{with $posts}}<section>
+ <ul class="posts">
+ {{range .}}<li><time datetime="{{date (index .Meta "post_date") "2006-01-02"}}">{{date (index .Meta "post_date") "2006-01-02"}}</time><a href="{{rel "weblog/index.html" .OutputPath}}">{{.Title}}</a></li>{{end}}
+ </ul>
+ </section>{{end}}
+ </main>
+
+{{template "footer" .}}