From 5943b3063676d68381545e36a9903e1df278e923 Mon Sep 17 00:00:00 2001 From: t Date: Sat, 11 Jul 2026 11:42:23 -0600 Subject: initial commit: claude-designed site and project doc to staticly generate it going forward --- .thumbnail | Bin 0 -> 6344 bytes _docs/PROJECT_PLAN.md | 245 ++++++++++++++++++++++++++++++++++++++++++ about/index.html | 69 ++++++++++++ feed.xml | 37 +++++++ index.html | 86 +++++++++++++++ notes/index.html | 67 ++++++++++++ notes/plain-html.html | 70 ++++++++++++ projects/index.html | 66 ++++++++++++ style.css | 111 +++++++++++++++++++ weblog/index.html | 61 +++++++++++ weblog/moving-off-gemini.html | 91 ++++++++++++++++ 11 files changed, 903 insertions(+) create mode 100644 .thumbnail create mode 100644 _docs/PROJECT_PLAN.md create mode 100644 about/index.html create mode 100644 feed.xml create mode 100644 index.html create mode 100644 notes/index.html create mode 100644 notes/plain-html.html create mode 100644 projects/index.html create mode 100644 style.css create mode 100644 weblog/index.html create mode 100644 weblog/moving-off-gemini.html diff --git a/.thumbnail b/.thumbnail new file mode 100644 index 0000000..336e6d1 Binary files /dev/null and b/.thumbnail differ diff --git a/_docs/PROJECT_PLAN.md b/_docs/PROJECT_PLAN.md new file mode 100644 index 0000000..5a8abec --- /dev/null +++ b/_docs/PROJECT_PLAN.md @@ -0,0 +1,245 @@ +# tjp.lol project plan + +## Goals + +- Preserve the existing visual design and `style.css` unchanged. +- Replace duplicated HTML with a small Go static-site generator. +- Make Markdown the source format for public content. +- Support public Obsidian notes without nonstandard Obsidian syntax. +- Publish an Atom feed at `feed.xml`. +- Remove fictional public content without replacing it with AI-written prose. +- Keep all published post prose human-written; disclose AI coding/refactoring assistance in the colophon. + +## Content and placeholder cleanup + +- Prefix retained fictional example files with `_`. + - They remain repository references only. + - The generator does not scan, render, list, or otherwise expose underscore-prefixed files. + - An underscore-prefixed directory excludes its complete subtree, enabling `_private/` in the vault. +- Remove links to fictional weblog posts, notes, projects, tags, and feed entries. +- Remove tags everywhere; there is no tag system. +- Comment out: + - Notes navigation in the shared header. + - Homepage “from the notes.” + - Webring markup in the shared footer. +- Keep Notes hidden until real vault content is publishing successfully. +- Replace the fake feed with a valid, initially empty Atom feed rather than invented entries. +- During migration, enumerate every existing prose location for manual rewrite: + - page body copy; + - titles and descriptions; + - feed title/subtitle text; + - project descriptions; + - link labels where applicable; + - colophon text; + - placeholder entries and summaries. +- Do not generate replacement prose. + +## Licence and colophon + +- Site content is licensed **CC BY 4.0 unless noted**. +- Put a consistent content-licence notice in the shared footer, linking directly to Creative Commons. +- Use the colophon to explain the default licence and exceptions. +- No separate prose `LICENSE` file or registration is required. +- Generator code may receive a separate OSS licence later. +- Replace outdated colophon claims about hand-written HTML and having no build step. +- Retain the no-JavaScript claim if it remains true. +- The human-written colophon should state that AI may be used for coding and batch refactoring, but not to generate published post prose. + +## Generator + +### Implementation + +- A Go command-line binary. +- Use a maintained CommonMark parser with familiar GFM extensions. +- Disable raw HTML in Markdown. +- Use Go template syntax: + - `html/template` for HTML outputs; + - XML-safe rendering for Atom output. +- Follow the `sw-convert` model: load a named set of reusable templates/partials, rather than serving literal header/footer files. + +### Source and output rules + +- Run recursively from one defined site root. +- Markdown conversion is in place: + + ```text + path/page.md → path/page.html + ``` + +- Page-output templates use explicit destination extensions: + + ```text + index.html.tmpl → index.html + feed.xml.tmpl → feed.xml + ``` + +- Reject collisions, such as both `index.md` and `index.html.tmpl` targeting `index.html`. +- Static assets such as `style.css` are untouched. +- Markdown sources may remain downloadable. +- Template sources should not be publicly served. + +### Template structure + +- Markdown pages use path-based site layouts rather than requiring layout frontmatter: + - weblog pages receive the existing post structure/classes; + - notes receive the existing note structure/classes; + - ordinary pages receive a general page layout. +- Complex pages such as the homepage, weblog index, notes index, and feed can be written directly as templates. +- Templates receive helpers for relative paths, sorting, filtering, slicing, and joining rendered sections. +- Header/footer templates calculate correct relative links such as `../style.css`, preserving portable relative URLs. + +## Markdown parsing and page model + +### Links + +Rewrite only site-local Markdown link destinations: + +```text +[example](other.md) → [example](other.html) +[example](other.md#part) → [example](other.html#part) +``` + +Leave external URLs, mail links, fragments, asset links, and code blocks unchanged. + +### Titles and sections + +- The first H1 supplies the title for ordinary Markdown pages. +- The title is used for document metadata, page headings, listings, and Atom entry titles. +- The extracted H1 is not duplicated in rendered body content. +- Title fallback order: + + 1. first H1; + 2. optional frontmatter title where unavoidable; + 3. filename-derived fallback with a build warning. + +- Parsed Markdown body is exposed as rendered top-level sections: + + ```go + Sections []template.HTML + ``` + +- This is the primary content abstraction; no separate `Lede` field is needed. +- Weblog convention: the first section after the opening H1 should be a short paragraph. The weblog layout renders that first section in a wrapper with inline `font-size:1.2em`, then renders the remainder normally. +- Notes and ordinary pages do not receive special lede treatment. + +### Metadata + +- YAML frontmatter is supported and exposed as `.Meta`. +- Frontmatter is optional except where genuine metadata is needed. +- Weblog posts use `post_date` for chronological listings and Atom dates. +- An optional updated date may override the original post date. +- Notes can sort by source file modification time. + +## Page inventory and indexes + +Before rendering anything: + +1. Discover all eligible Markdown and page-template sources. +2. Parse frontmatter and Markdown. +3. Derive page paths, titles, metadata, source modification times, and rendered sections. +4. Build the complete page inventory. +5. Render all HTML pages and `feed.xml`. + +The template data includes: + +```go +Pages map[string][]*Page +``` + +Each `*Page` is inserted into every ancestor directory collection. For example: + +```text +notes/recipes/cocktails/old_fashioned.md +``` + +is available through: + +```text +Pages["notes"] +Pages["notes/recipes"] +Pages["notes/recipes/cocktails"] +``` + +All entries point to the same `Page` object, which carries path information, metadata, title, modification time, and rendered sections. + +Templates can therefore build lists without special generator features: + +- weblog index: weblog pages sorted by `Meta.post_date`; +- homepage: recent posts; +- notes index: notes sorted by modification time; +- future manually authored “good starting points” sections. + +Only real pages enter these collections. Underscore-prefixed files, directories, and non-page outputs such as `feed.xml` do not. + +## Feed + +- Retain Atom 1.0 at `feed.xml`. +- Label the navigation link “feed.” +- Include weblog posts only. +- Sort by post date. +- Emit full rendered post content in each entry. +- Use absolute canonical URLs under `https://tjp.lol/`. +- Emit a valid zero-entry feed until real posts exist. + +## Syncthing and notes + +- Generated HTML remains beside Markdown on the server. +- Prevent generated HTML from syncing through the vault with Syncthing ignore rules. + +Each device has a local, non-synced `.stignore`: + +```text +#include .stignore.shared +``` + +The synchronized `.stignore.shared` contains: + +```text +*.html +``` + +This prevents HTML synchronization in either direction at every depth while allowing Markdown synchronization. + +Notes rollout: + +1. Comment out Notes links and homepage note links. +2. Connect the synced vault to the site source tree and build note HTML. +3. Validate generated note pages and privacy behavior. +4. Restore Notes navigation only when public notes are ready. + +## Caddy follow-up + +Not part of the generator implementation, but record a server task: + +- Deny requests for any path containing an underscore-prefixed component. +- Deny template-source requests such as `*.tmpl`. +- This ensures `_private/` Markdown and internal template files cannot be downloaded even though Markdown is otherwise publicly available. + +## Reliability and validation + +- Do not overwrite non-generated HTML. +- Track generated outputs so deleted/renamed Markdown does not leave accidental stale public pages; cleanup must only remove tracked generated files. +- Write output safely so a failed build preserves existing pages. +- Cron execution should use a lock, log failures, and run after Syncthing has had time to settle. +- Add tests for: + - Markdown/GFM conversion and raw-HTML rejection; + - local-link rewriting; + - underscore exclusions; + - output collisions; + - title/section extraction; + - recursive page inventory; + - relative-path generation; + - Atom validity and full-content entries. +- Validate internal links and feed XML before deployment. + +## Delivery sequence + +1. Archive fictional examples under underscore-prefixed names and remove public references. +2. Establish source conventions, templates, and generator tests. +3. Implement parsing, page inventory, Markdown rendering, relative links, and output writing. +4. Migrate current HTML structure into Markdown and `.html.tmpl` sources. +5. Audit all inherited prose for manual replacement. +6. Generate the Atom feed and validate the public site. +7. Configure Syncthing ignores and Caddy protection. +8. Connect and publish the selected notes vault content. +9. Restore Notes links when the published notes are real and ready. diff --git a/about/index.html b/about/index.html new file mode 100644 index 0000000..9b58299 --- /dev/null +++ b/about/index.html @@ -0,0 +1,69 @@ + + + + + +About & colophon — tjp.lol + + + + +
+ +
+

~tjp $_

+ +
+ +
+
+
+

About

+
+
+

I'm tjp — 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.

+

This site is where I keep my writing and a public cut of my notes. If you want to reach me, t@tjp.lol is the best way.

+ +

Elsewhere

+ +
+ + +
+
+ + + +
+ + diff --git a/feed.xml b/feed.xml new file mode 100644 index 0000000..7dbd364 --- /dev/null +++ b/feed.xml @@ -0,0 +1,37 @@ + + + tjp.lol + Notes, code, and longer thoughts from an elder-millennial software engineer in Salt Lake City. + + + 2026-06-30T00:00:00Z + https://tjp.lol/ + + tjp + t@tjp.lol + + + + Moving my weblog off Gemini (without leaving it) + + https://tjp.lol/weblog/moving-off-gemini.html + 2026-06-30T00:00:00Z + How I mirror my Gemini capsule to the web with a tiny gemtext-to-HTML converter, without abandoning geminispace. + + + + A static site generator in 100 lines of bash + + https://tjp.lol/weblog/static-site-generator-bash + 2026-05-18T00:00:00Z + Placeholder entry — replace with the real post. + + + + Syncthing is the personal cloud I actually wanted + + https://tjp.lol/weblog/syncthing + 2026-04-02T00:00:00Z + Placeholder entry — replace with the real post. + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..7bfab0a --- /dev/null +++ b/index.html @@ -0,0 +1,86 @@ + + + + + +tjp.lol + + + + + +
+ +
+

~tjp $_

+

Elder-millennial software engineer in Salt Lake City, Utah. Writing about code, tools, and the small web.

+ +
+ +
+
+

Welcome in. This site is my writing desk: weblog posts moving over from my gemini:// capsule, plus a public cut of the notes I keep in Obsidian and sync across my machines with Syncthing.

+

Plain HTML and CSS. No JavaScript, no build step, no tracking — it reads exactly the same with scripts turned off. That's the point.

+
+ +
+

recent posts

+ +

all posts →

+
+ +
+

tags

+ +
+ + +
+ + + +
+ + diff --git a/notes/index.html b/notes/index.html new file mode 100644 index 0000000..7c5265f --- /dev/null +++ b/notes/index.html @@ -0,0 +1,67 @@ + + + + + +Notes — tjp.lol + + + + +
+ +
+

~tjp $_

+ +
+ +
+
+

This is a digital garden — a public slice of the notes I keep in Obsidian. Unlike the weblog, 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.

+
+ +
+

start here

+ +
+ +
+

all notes

+ +
+
+ + + +
+ + diff --git a/notes/plain-html.html b/notes/plain-html.html new file mode 100644 index 0000000..d60275e --- /dev/null +++ b/notes/plain-html.html @@ -0,0 +1,70 @@ + + + + + +The case for plain HTML — notes — tjp.lol + + + + +
+ +
+

~tjp $_

+ +
+ +
+
+
+

← notes  /  tended · planted

+

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 — both are bets that constraint outlives capability. See also How I take notes 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.

+
+ + +
+
+ + + +
+ + diff --git a/projects/index.html b/projects/index.html new file mode 100644 index 0000000..4e004e6 --- /dev/null +++ b/projects/index.html @@ -0,0 +1,66 @@ + + + + + +Projects — tjp.lol + + + + +
+ +
+

~tjp $_

+ +
+ +
+
+

Small things I've built and still maintain. Most live on GitHub or tildegit. Placeholder entries for now — swap in the real ones whenever.

+
+ +
+

maintained

+ +
+ +
+

archived / for fun

+ +
+
+ + + +
+ + diff --git a/style.css b/style.css new file mode 100644 index 0000000..03c224c --- /dev/null +++ b/style.css @@ -0,0 +1,111 @@ +/* tjp.lol — shared styles. Plain CSS, no JS anywhere on this site. */ + +:root{ + --bg:#f4f4f1; --fg:#232322; --muted:#77776f; --rule:#d8d8d1; + --accent:#2f6f4f; --accent-hover:#1f513a; --card:#ecece7; + --measure:46rem; + --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace; + --sans:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; +} +@media (prefers-color-scheme:dark){ + :root{ + --bg:#131413; --fg:#d7d9d2; --muted:#82857b; --rule:#2a2c28; + --accent:#6fbf93; --accent-hover:#8fd6ac; --card:#1b1d1a; + } +} + +*{box-sizing:border-box;} +html{font-size:17px;} +body{ + margin:0; background:var(--bg); color:var(--fg); + font-family:var(--sans); line-height:1.62; -webkit-text-size-adjust:100%; +} +code,kbd,samp,pre,.mono,nav.main a,time,.k,h1.brand,h2.sec{font-family:var(--mono);} + +a{color:var(--accent); text-decoration:underline; text-underline-offset:2px; text-decoration-thickness:1px;} +a:hover{color:var(--accent-hover);} +a:visited{color:var(--accent);} + +.wrap{max-width:var(--measure); margin:0 auto; padding:0 1.4rem;} +img{max-width:100%; height:auto;} +hr{border:0; border-top:1px dashed var(--rule); margin:2rem 0;} + +/* ---- masthead ---- */ +header.site{padding:2.4rem 0 1.2rem;} +h1.brand{font-size:1.5rem; margin:0; font-weight:700; letter-spacing:-.02em;} +h1.brand a{color:var(--fg); text-decoration:none;} +h1.brand .cur{color:var(--accent); font-weight:400;} +header.site .tag{color:var(--muted); margin:.45rem 0 0; font-size:.98rem;} +nav.main{margin-top:1.1rem; padding-top:1rem; border-top:1px dashed var(--rule); + display:flex; flex-wrap:wrap; gap:.4rem 1rem; font-size:.92rem;} +nav.main a{color:var(--fg); text-decoration:none;} +nav.main a::before{content:"["; color:var(--muted); margin-right:.15rem;} +nav.main a::after{content:"]"; color:var(--muted); margin-left:.15rem;} +nav.main a:hover{color:var(--accent);} +nav.main a[aria-current="page"]{color:var(--accent);} + +main{padding:1.4rem 0 1rem;} +section{margin:2.2rem 0;} +h2.sec{font-size:.8rem; letter-spacing:.02em; color:var(--muted); font-weight:400; margin:0 0 .9rem;} +h2.sec::before{content:"## "; color:var(--accent);} + +.intro p{margin:0 0 .9rem; font-size:1.06rem;} + +/* ---- post/note lists ---- */ +ul.posts{list-style:none; margin:0; padding:0; font-size:.98rem;} +ul.posts li{display:flex; gap:1rem; align-items:baseline; padding:.34rem 0;} +ul.posts time{color:var(--muted); font-size:.82rem; white-space:nowrap; min-width:6rem; font-variant-numeric:tabular-nums;} +ul.posts a{text-decoration:none; color:var(--fg);} +ul.posts a:hover{color:var(--accent); text-decoration:underline;} +@media (max-width:30rem){ul.posts li{flex-direction:column; gap:.05rem;}} + +.tags{display:flex; flex-wrap:wrap; gap:.4rem; font-family:var(--mono); font-size:.85rem;} +.tags a{text-decoration:none; color:var(--muted);} +.tags a::before{content:"#";} +.tags a:hover{color:var(--accent);} + +.cols{display:grid; grid-template-columns:1fr 1fr; gap:1.6rem 2rem;} +@media (max-width:34rem){.cols{grid-template-columns:1fr;}} +.links{list-style:none; margin:0; padding:0; font-size:.95rem;} +.links li{padding:.24rem 0; display:flex; gap:.8rem;} +.links .k{color:var(--muted); font-size:.8rem; min-width:4.6rem;} +.links a{text-decoration:none;} .links a:hover{text-decoration:underline;} + +/* ---- long-form article ---- */ +article.post{font-size:1.08rem; line-height:1.7;} +article.post .post-head{margin:0 0 2rem;} +article.post h1{font-size:1.85rem; line-height:1.2; letter-spacing:-.02em; margin:.2rem 0 .5rem; font-family:var(--sans);} +article.post .meta{color:var(--muted); font-size:.85rem; font-family:var(--mono);} +article.post h2{font-size:1.3rem; margin:2.2rem 0 .7rem; letter-spacing:-.01em;} +article.post h3{font-size:1.08rem; margin:1.8rem 0 .5rem;} +article.post p{margin:0 0 1.1rem;} +article.post ul,article.post ol{margin:0 0 1.1rem; padding-left:1.3rem;} +article.post li{margin:.3rem 0;} +article.post blockquote{margin:1.4rem 0; padding:.2rem 0 .2rem 1.1rem; border-left:3px solid var(--accent); + color:var(--muted); font-style:italic;} +article.post code{background:var(--card); padding:.1rem .35rem; border-radius:3px; font-size:.9em;} +article.post pre{background:var(--card); border:1px solid var(--rule); border-radius:6px; + padding:1rem 1.1rem; overflow-x:auto; font-size:.86rem; line-height:1.55;} +article.post pre code{background:none; padding:0;} +article.post a{text-underline-offset:3px;} +.post-nav{display:flex; justify-content:space-between; gap:1rem; margin-top:2.4rem; + padding-top:1.2rem; border-top:1px dashed var(--rule); font-size:.9rem;} +.post-nav a{text-decoration:none;} + +/* ---- notes / wiki ---- */ +.note-body{font-size:1.05rem; line-height:1.7;} +.note-body a.wiki{text-decoration:none; border-bottom:1px solid var(--rule);} +.note-body a.wiki:hover{border-bottom-color:var(--accent);} +.backlinks{margin-top:2.4rem; padding-top:1.2rem; border-top:1px dashed var(--rule);} +.backlinks h2{font-size:.8rem; font-family:var(--mono); color:var(--muted); font-weight:400; margin:0 0 .6rem;} +.note-index{columns:2; column-gap:2rem; font-size:.98rem;} +@media (max-width:34rem){.note-index{columns:1;}} +.note-index a{display:block; padding:.2rem 0; text-decoration:none;} +.note-index a:hover{text-decoration:underline;} + +/* ---- footer ---- */ +footer{border-top:1px dashed var(--rule); margin-top:2.4rem; padding:1.4rem 0 3rem; + color:var(--muted); font-size:.85rem;} +.ring{display:flex; gap:.9rem; align-items:center; flex-wrap:wrap; margin-bottom:.7rem; font-family:var(--mono);} +.ring a{text-decoration:none;} +footer a{color:var(--muted);} footer a:hover{color:var(--accent);} diff --git a/weblog/index.html b/weblog/index.html new file mode 100644 index 0000000..4484805 --- /dev/null +++ b/weblog/index.html @@ -0,0 +1,61 @@ + + + + + +Weblog — tjp.lol + + + + + +
+ +
+

~tjp $_

+ +
+ +
+
+

Longer-form writing. Most of these are migrating over from my Gemini capsule, so dates reflect the original posting. Subscribe via RSS.

+
+ +
+

2026

+ +
+ +
+

2025

+ +
+
+ + + +
+ + diff --git a/weblog/moving-off-gemini.html b/weblog/moving-off-gemini.html new file mode 100644 index 0000000..f84a797 --- /dev/null +++ b/weblog/moving-off-gemini.html @@ -0,0 +1,91 @@ + + + + + +Moving my weblog off Gemini (without leaving it) — tjp.lol + + + + + +
+ +
+

~tjp $_

+ +
+ +
+
+
+

← weblog  / 

+

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:

+ +
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. And the capsule is still there — see the colophon for how it all fits together.

+
+ + +
+ + + +
+ + -- cgit v1.3