git » homepage.git » commit cbe8667

Refine TechWorks and public identity

author Alan Dipert
2026-08-10 17:32:33 UTC
committer Alan Dipert
2026-08-10 17:32:33 UTC
parent 8a715b3951978d7bd9331d95a323194588571db7

Refine TechWorks and public identity

AGENTS.md +3 -1
md/ConsultingPractice.md +0 -15
md/Home.md +13 -5
md/OpenSource.md +94 -71
md/PersonalBackground.md +21 -2
md/RecoveredTechnicalWriting.md +3 -13
md/TechWorks.md +42 -20
md/TechWorksArchive.md +47 -81
md/Writings.md +5 -5
tests/test_sitegen.py +37 -0
tools/sitegen.py +133 -18
tpl/head.html +1 -0

diff --git a/AGENTS.md b/AGENTS.md
index 6edebcd..31f0ce8 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -24,7 +24,9 @@ Welcome! This document describes how automated helpers should operate when worki
 
 When drafting or editing Markdown articles under `md/`:
 
-- Begin with a level-one heading that matches the filename (e.g., `# HomepageDesign`).
+- Begin with a level-one human-readable page title. Legacy CamelCase filenames
+  keep their stable routes while the heading may use spaces (for example,
+  `OpenSource.md` may begin with `# Open Source`).
 - Use short, conversational paragraphs aimed at technical readers; prefer present tense and active voice.
 - Link to related wiki pages using relative Markdown links (`./PageName.md`) so the generator can rewrite them.
 - When publishing or adding a new article, add a dated entry near the top of the `md/Home.md` Updates table with a relative Markdown link to the article.
diff --git a/md/ConsultingPractice.md b/md/ConsultingPractice.md
deleted file mode 100644
index 7468f8c..0000000
--- a/md/ConsultingPractice.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# ConsultingPractice
-**As of November 2020 I'm booked and unavailable to start new projects, but please don't hesitate to e-mail me anyway at [alan@tailrecursion.com](mailto:alan@tailrecursion.com). I'll be happy to make your acquaintance and get back to you when I become available.**
-
-As of June 2020 I am an independent software consultant specializing in [Clojure](https://clojure.org/), [ClojureScript](https://clojurescript.org/), JavaScript, and web application design and architecture. I have over a decade of experience and have successfully built and operated applications in a wide variety of challenging technical and business environments. My practice is defined by the following mission statement:
-
-
-1. **Deliver** reliable software solutions regularly and rapidly to my clients.
-2. **Collaborate** whenever possible to avoid pitfalls and leverage existing expertise.
-3. **Communicate** continuously to ensure solutions meet all expectations and requirements.
-4. **Resolve**, immediately, any operational difficulties associated with delivered software.
-5. **Improve** techniques and methodologies continuously as lessons are learned.
-
-
-If you are interested in contracting my services and would like to discuss an engagement, please don't hesitate to send me an e-mail at [alan@tailrecursion.com](mailto:alan@tailrecursion.com). I look forward to hearing from you!
-
diff --git a/md/Home.md b/md/Home.md
index a24fe03..6109b77 100644
--- a/md/Home.md
+++ b/md/Home.md
@@ -1,16 +1,23 @@
 # Home
 ![In front of a wall of giant doughnuts](./Home/donuts_alan_float_right_392px.jpg)
 
-Hello, and welcome to my personal [homepage](https://en.wikipedia.org/wiki/Home_page) and [wiki](https://en.wikipedia.org/wiki/Wiki).
+I’m Alan Dipert, a programmer and builder. I like small languages, visible
+computation, and tools that help people think without hiding how they work.
+
+This is where I keep [current projects](./TechWorks.md), technical history,
+and [writing](./Writings.md). It also holds things I care about away from the
+keyboard: Christian faith, family, books, and the pleasure of staying curious.
+[A little more about me](./PersonalBackground.md).
 
 ## Explore
 
 <blockquote class="techworks-panel">
 
 ### [TechWorks](./TechWorks.md)
-Talks, papers, interviews, and workshops from my work in programming languages, web development, and software tools.
+What I am building now, followed by talks, papers, interviews, workshops, and
+older software worth remembering.
 
-[Open source](./OpenSource.md) · [Recovered technical writing](./RecoveredTechnicalWriting.md)
+[Open source](./OpenSource.md) · [Recovered technical writing](./RecoveredTechnicalWriting.md) · [Writings](./Writings.md)
 
 </blockquote>
 
@@ -31,8 +38,9 @@ Talks, papers, interviews, and workshops from my work in programming languages,
 
 | Date       | Note                                                                                                                                       |
 |:-----------|:-------------------------------------------------------------------------------------------------------------------------------------------|
-| 2026-08-09 | Added [OpenSource](./OpenSource.md), an evidence-backed catalog of software I created, maintained, revived, and contributed to.             |
-| 2026-08-09 | Added [RecoveredTechnicalWriting](./RecoveredTechnicalWriting.md), a curated guide to technical essays recovered from my old blog.          |
+| 2026-08-10 | Refined [TechWorks](./TechWorks.md), [Open Source](./OpenSource.md), and [About Alan](./PersonalBackground.md) around current work and evidence-backed history. |
+| 2026-08-09 | Added [Open Source](./OpenSource.md), an evidence-backed catalog of software I created, maintained, revived, and contributed to.             |
+| 2026-08-09 | Added [Recovered Technical Writing](./RecoveredTechnicalWriting.md), a curated guide to technical essays recovered from my old blog.          |
 | 2026-07-15 | Added [Klong](./Klong.md), on returning to array programming by putting the real Klong interpreter in a browser notebook.                  |
 | 2026-05-15 | Added [ProgrammablePhones](./ProgrammablePhones.md), on AI coding agents making personal mobile software practical.                         |
 | 2026-02-24 | Added [GridCalc](./GridCalc.md), an RPN spreadsheet for iOS with a persistent, visible program grid.                                       |
diff --git a/md/OpenSource.md b/md/OpenSource.md
index 4acfb79..29a7005 100644
--- a/md/OpenSource.md
+++ b/md/OpenSource.md
@@ -1,32 +1,43 @@
-# OpenSource
+# Open Source
 
 I have written, maintained, revived, and contributed to open-source software
-since the late 1990s. This page separates those roles so a small patch to a
-large project does not look like authorship, and preservation work does not
-look like writing the original code.
-
-The counts below are a snapshot captured August 9, 2026. They count commits
-linked to my GitHub identity on each canonical repository's current history.
-Older email addresses, rebases, and squashed pull requests can make the real
-total larger. Forks and mirrors are deduplicated before they are counted.
-
-## Software I helped create
-
-| Project | My role | What it is |
-|:--------|:--------|:-----------|
-| [fswatch](https://github.com/emcrisostomo/fswatch) | Original author | I wrote the first macOS/FSEvents implementation in 2009. Enrico M. Crisostomo and I merged it with his portable `fsw` in 2014, when he took over maintenance. The [official history](https://emcrisostomo.github.io/fswatch/doc/1.17.1/fswatch.html/Introduction.html) records the handoff. |
-| [Boot](https://github.com/boot-clj/boot) | Co-creator with Micha Niskin | Programmable build tooling for Clojure. The canonical repository currently contains 158 commits linked to me. |
-| [Hoplon](https://github.com/hoplon/hoplon) | Co-author and major contributor | A Clojure and ClojureScript web programming environment. The canonical repository currently contains 31 commits linked to me. |
-| [Javelin](https://github.com/hoplon/javelin) | Co-author and major contributor | Spreadsheet-like dataflow programming for ClojureScript. The canonical repository currently contains 53 commits linked to me. |
-| [Gherkin](https://github.com/alandipert/gherkin) | Author | A functional programming language and interpreter written in GNU Bash 4. |
-| [storage-atom](https://github.com/alandipert/storage-atom) | Author | ClojureScript atoms backed by browser local and session storage. |
-| [Intension](https://github.com/alandipert/intension) | Author | Datalog queries over nested Clojure maps and vectors. |
-| [Enduro](https://github.com/alandipert/enduro) | Author | Durable Clojure atoms backed by append-only storage. |
-
-The full archive also preserves smaller experiments, libraries, talks, class
-projects, and utilities. They include clj-swingrepl, html2r, HyperTurbo,
-BerryCam, fclojure, jsonptunnel, reconfig, CampfireClient, Wombat, and
-Flapjax demonstrations.
+since the late 1990s. The role matters. Starting a project, maintaining it,
+recovering its history, and sending one useful patch are all worthwhile, but
+they are different kinds of work.
+
+## Software I started or helped start
+
+- **[fswatch](https://github.com/emcrisostomo/fswatch) — Original author.** I
+  wrote the first macOS/FSEvents implementation in 2009. Enrico M. Crisostomo
+  and I merged it with his portable `fsw` in 2014, when he took over
+  maintenance. The [official history](https://emcrisostomo.github.io/fswatch/doc/1.17.1/fswatch.html/Introduction.html)
+  records the handoff.
+- **[Step and Scalatra](#step-and-scalatra) — Creator of Step; early Scalatra
+  contributor.** Step was the small Scala web framework I started. It became
+  Scalatra as other developers joined and the project found its lasting name.
+- **[Boot](https://github.com/boot-clj/boot) — Co-creator with Micha Niskin.**
+  Programmable build tooling for Clojure.
+- **[Hoplon](https://github.com/hoplon/hoplon) — Co-author and major
+  contributor.** A Clojure and ClojureScript environment for programming web
+  interfaces.
+- **[Javelin](https://github.com/hoplon/javelin) — Co-author and major
+  contributor.** Spreadsheet-like dataflow programming for ClojureScript.
+- **[Gherkin](https://github.com/alandipert/gherkin) — Author.** A functional
+  programming language and interpreter written in GNU Bash 4.
+- **[storage-atom](https://github.com/alandipert/storage-atom) — Author.**
+  ClojureScript atoms backed by browser local and session storage.
+- **[Intension](https://github.com/alandipert/intension) — Author.** Datalog
+  queries over nested Clojure maps and vectors.
+- **[Enduro](https://github.com/alandipert/enduro) — Author.** Durable Clojure
+  atoms backed by append-only storage.
+
+### Step and Scalatra
+
+I started [Step](https://github.com/alandipert/step) in 2010 as a compact web
+framework for Scala. The project was renamed Scalatra within its first week.
+I remained part of the early development, and the surviving history credits
+me as the person who started it. Today’s [Scalatra](https://scalatra.org/) is
+the work of the maintainers and contributors who carried it forward.
 
 ## Older software recovered or revived
 
@@ -34,57 +45,69 @@ Flapjax demonstrations.
 
 I wrote ProblemScan v1.0 in Visual Basic in February 2000 and submitted it to
 Planet Source Code. It scans a selected file for strings associated with
-NetBus, Sub7, and Back Orifice servers. The original encoded submission,
-decoded ZIP, and reconstructed Git history have been recovered from the first
-Planet Source Code CD image. It is early work, preserved as history rather
-than current security advice.
+NetBus, Sub7, and Back Orifice servers. It is early work, kept as history
+rather than current security advice.
 
 ### NCSA Mosaic
 
 In 2010 I recovered the public NCSA Mosaic source and republished its history
-on GitHub. I did not write Mosaic. [GitHub covered the repository](https://github.blog/news-insights/ncsa-mosaic-on-github/)
-when it appeared, and [UNESCO later used it as a Software Heritage example](https://www.unesco.org/en/articles/softwareheritageorg-unesco-and-inria-open-archive-digital-age)
-beside the Apollo 11 guidance software, Quake III, and AngularJS.
+on GitHub. I did not write Mosaic. [GitHub covered the restored repository](https://github.blog/news-insights/ncsa-mosaic-on-github/),
+and contemporary reports appeared in [Wired](https://www.wired.com/2010/03/browse-the-web-as-it-looked-in-1993/)
+and [bit-tech](https://bit-tech.net/news/tech/software/hackers-resurrect-ncsa-mosaic/1/).
 
 ## Contributions to established projects
 
-| Project | Contribution | Evidence snapshot |
-|:--------|:-------------|:------------------|
-| [Clojure](https://github.com/clojure/clojure) | Language and build fixes, including `into-array`, unchecked math, n-ary bit functions, and math inlining | 4 canonical commits |
-| [Scalatra](https://github.com/scalatra/scalatra) | Early framework development during the Step/Scalatra period | 7 canonical commits; early release metadata also lists me as a developer |
-| [Leiningen](https://github.com/technomancy/leiningen) | Build-tool contributions | 9 canonical commits |
-| [Scittle](https://github.com/babashka/scittle) | Documentation contribution | 1 canonical commit |
-| [ring-undertow-adapter](https://github.com/luminus-framework/ring-undertow-adapter) | Adapter contributions | 2 canonical commits |
-| [j](https://github.com/kevinkrouse/j) | Common Lisp implementation contribution | 1 canonical commit |
+- **[Clojure](https://github.com/clojure/clojure):** language and build fixes,
+  including work on `into-array`, unchecked math, n-ary bit functions, and
+  math inlining.
+- **[Leiningen](https://github.com/technomancy/leiningen):** build-tool fixes
+  and improvements.
+- **[Scittle](https://github.com/babashka/scittle):** documentation.
+- **[ring-undertow-adapter](https://github.com/luminus-framework/ring-undertow-adapter):**
+  adapter improvements.
+- **[j](https://github.com/kevinkrouse/j):** a contribution to the Common Lisp
+  implementation.
+- **[JD4X](https://jdx.sourceforge.net/):** a small contribution to the Java
+  desktop project; its surviving homepage lists me among its credited
+  contributors.
 
 ## Professional open source
 
-I made these contributions as part of my paid work. They still belong in the
-footprint, with that context stated plainly.
-
-| Project | Contribution | Evidence snapshot |
-|:--------|:-------------|:------------------|
-| [Shiny](https://github.com/rstudio/shiny) | Testing infrastructure, mock sessions, `testServer`, and related application work | 162 canonical commits |
-| [shinyloadtest](https://github.com/rstudio/shinyloadtest) | Load-test recording, analysis, and tooling | 257 canonical commits |
-| [shinycannon](https://github.com/rstudio/shinycannon) | Shiny load-generation tooling | 226 canonical commits |
-| [httpuv](https://github.com/rstudio/httpuv) | HTTP and WebSocket server contributions | 7 canonical commits |
-| [shinydashboard](https://github.com/rstudio/shinydashboard) | Dashboard contributions | 2 canonical commits |
-
-## Size of the footprint
-
-The first complete GitHub inventory contains 141 public repositories under
-`alandipert`: 99 original repositories and 42 forks. It also contains 386
-public gists. The public API also exposes 58 repositories owned by projects
-or collaborators with which the account is affiliated. Those numbers describe
-the preservation workload, not a claim that every fork or affiliation is an
-independent contribution.
-
-The repository-only archive keeps complete Git bundles for that inventory and
-for canonical upstream projects where my work landed. A contribution ledger
-records roles and evidence; the repository metadata and Git histories retain
-licenses, dates, and commit identities; the preservation manifest adds source
-URLs and SHA-256 checksums. The public list will grow when another contribution
-can be tied to canonical history. A fork by itself is not enough evidence.
-
-Related talks, papers, and interviews are collected in [TechWorks](./TechWorks.md),
-and recovered early essays are in [RecoveredTechnicalWriting](./RecoveredTechnicalWriting.md).
+I made this work while employed by RStudio. It belongs in the footprint, with
+that context stated plainly.
+
+- **[Shiny](https://github.com/rstudio/shiny):** testing infrastructure, mock
+  sessions, `testServer`, and related application work.
+- **[shinyloadtest](https://github.com/rstudio/shinyloadtest):** load-test
+  recording, analysis, and tooling.
+- **[shinycannon](https://github.com/rstudio/shinycannon):** Shiny
+  load-generation tooling.
+- **[httpuv](https://github.com/rstudio/httpuv):** HTTP and WebSocket server
+  work.
+- **[shinydashboard](https://github.com/rstudio/shinydashboard):** dashboard
+  improvements.
+
+## Small pieces that traveled
+
+In 2011 I posted [Kahn’s topological sort in Clojure](https://gist.github.com/alandipert/1263783).
+People reported using it directly. It was also copied into a
+[Malli data-generation experiment](https://gist.github.com/jeroenvandijk/5e0785f25f7fdfeac7bc7a0be72cb62a)
+with the attribution and license intact, and it inspired a credited
+[F# port](https://gist.github.com/bcachet/8e1155f75ea36f1b417f84ea88dae312).
+It is a satisfying example of a small, self-contained piece of code traveling
+farther than its original context.
+
+A smaller ClojureScript experiment made
+[regular expressions callable](https://gist.github.com/alandipert/2346460).
+One teacher wrote that he kept pointing learners to it because the compact
+example exposed something deep about ClojureScript’s protocols. That is a
+different kind of footprint, but a useful one: a snippet can travel as an idea
+even when it never becomes a library.
+
+There are other small experiments and utilities on my
+[GitHub profile](https://github.com/alandipert/). I add something here when I
+can describe its role and downstream use from direct evidence.
+
+Related talks, papers, and interviews are collected in
+[TechWorks](./TechWorks.md), and selected early essays are in
+[Recovered Technical Writing](./RecoveredTechnicalWriting.md).
diff --git a/md/PersonalBackground.md b/md/PersonalBackground.md
index a5450cf..1582486 100644
--- a/md/PersonalBackground.md
+++ b/md/PersonalBackground.md
@@ -1,3 +1,22 @@
-# PersonalBackground
-TODO
+# About Alan
 
+I’m a programmer and builder. I have spent much of my career working with
+programming languages, web systems, build tools, and open-source software. I
+am happiest when the machinery stays small enough to understand and the
+result gives someone a more direct way to think.
+
+That thread runs from [Step and Scalatra](./OpenSource.md#step-and-scalatra),
+Boot, Hoplon, and Javelin through newer projects such as
+[GridCalc](./GridCalc.md) and [Klong for the Web](./Klong.md). It also explains
+why I have taken time to recover old work such as ProblemScan, NCSA Mosaic,
+and the technical writing from my former sites.
+
+Christian faith is central to how I understand the world. I write about it in
+[Who Is God?](./WhoIsGod.md) and related notes. My family’s history belongs
+here too. The [eulogy I gave for my dad](./RandallRDipertEulogy.md) and the
+reading course he assembled in [The Well-Read Undergrad](./WellReadUndergrad.md)
+are parts of our inheritance that I want to keep.
+
+I still enjoy learning languages, reading, amateur radio, drones, lawn care,
+and conversations with people who are absorbed in something. If you would
+like one of those conversations, see [Coffee Time](./CoffeeTime.md).
diff --git a/md/RecoveredTechnicalWriting.md b/md/RecoveredTechnicalWriting.md
index 4d69b2b..b051802 100644
--- a/md/RecoveredTechnicalWriting.md
+++ b/md/RecoveredTechnicalWriting.md
@@ -1,9 +1,8 @@
-# RecoveredTechnicalWriting
+# Recovered Technical Writing
 
 My old blog at `alan.dipert.org` disappeared from the live web, but the
-Internet Archive retained most of it. I recovered 79 distinct posts from
-2008–2013 and saved them as a repository-only WARC. These are the pieces I
-still want readers to encounter.
+Internet Archive retained much of it. These are the pieces I still want
+readers to encounter.
 
 ## Discover Lisp in Your Web Browser with Javascript
 
@@ -65,12 +64,3 @@ details have aged, but the central comparison between iteration and reducing
 a sequence remains useful.
 
 [Read the archived post](https://web.archive.org/web/20130602154353/http://alan.dipert.org/post/307586762/polyglot-folding-ruby-clojure-scala)
-
-## What remains in the archive
-
-The repository also preserves the rest of the blog, the older
-`neologic.net/ad` site, and period RSS snapshots. I have left rants, personal
-posts, predictions, obsolete snippets, reblogs, and short link posts out of
-the public collection. One old Perl post describes weak XOR-based secret
-handling; it remains historical material and should not be used as security
-guidance.
diff --git a/md/TechWorks.md b/md/TechWorks.md
index c3ffe71..d954cff 100644
--- a/md/TechWorks.md
+++ b/md/TechWorks.md
@@ -1,29 +1,51 @@
 # TechWorks
 
-I have worked in public across programming languages, build tools, web
-frameworks, developer education, and software preservation. This page keeps
-the talks, papers, interviews, teaching, and technical writing together.
+I build programming tools, small languages, and systems that make computation
+easier to see. This page starts with the things I am making now, then gathers
+the talks, papers, interviews, teaching, and older software that show how I
+got here.
 
-## Selected work
+## Building now
 
-- [JACL: A Common Lisp for Developing Single-Page Web Applications](./TechWorks/jacl-demo-els-2020.pdf), a 2020 European Lisp Symposium paper and presentation
-- [Make Shiny fast by doing as little work as possible](https://rstudio.com/resources/rstudioconf-2018/make-shiny-fast-by-doing-as-little-work-as-possible/), from rstudio::conf 2018
-- [Boot Can Build It](https://www.youtube.com/watch?v=TcnzB2tB-8Q), presented with Micha Niskin at Clojure/West 2015
-- [Web Programming with Hoplon](https://www.youtube.com/watch?v=wVXjExRiFy0), presented with Micha Niskin at Clojure/West 2014
-- [FRP in ClojureScript with Javelin](http://www.infoq.com/presentations/ClojureScript-Javelin), from Clojure/West 2013
-- [Programming with Values in Clojure](https://www.infoq.com/presentations/Programming-with-Values-in-Clojure/), from Clojure/West 2012
+- [GridCalc](./GridCalc.md) is an RPN spreadsheet for iOS. The grid is both the
+  calculation and its visible history.
+- [Klong for the Web](./Klong.md) puts Nils M Holm’s real Klong interpreter in
+  an offline-capable browser notebook.
+- [Actor Agents](./ActorAgents.md) and [Programmable Phones](./ProgrammablePhones.md)
+  record current experiments with capable, constrained software and personal
+  tools built with AI assistance.
 
-My software work includes originating [fswatch](./OpenSource.md#software-i-helped-create),
-co-creating Boot, helping build Hoplon and Javelin, contributing to Scalatra,
-Clojure, Leiningen, and the Shiny ecosystem, and
-[recovering NCSA Mosaic's source history](./OpenSource.md#ncsa-mosaic). The
-[OpenSource](./OpenSource.md) catalog separates those roles and identifies
-work done for employers.
+## Earlier work
 
-Copies of papers, slides, recordings, transcripts, and source snapshots are
-available in the [TechWorks Archive](./TechWorksArchive.md). Selected early
-blog posts are collected in
-[RecoveredTechnicalWriting](./RecoveredTechnicalWriting.md).
+### Languages and interfaces
+
+I created the Scala framework Step, which became Scalatra. Later I co-created
+Boot with Micha Niskin and helped build Hoplon and Javelin. Those projects
+treated builds and web interfaces as programs instead of piles of
+configuration. Gherkin, JACL, and talks such as
+[Programming with Values in Clojure](https://www.infoq.com/presentations/Programming-with-Values-in-Clojure/)
+came from the same interest in small languages and direct models.
+
+### Software in production
+
+At RStudio I contributed to Shiny, shinyloadtest, shinycannon, httpuv, and
+shinydashboard. My public work from that period includes
+[Make Shiny fast by doing as little work as possible](https://rstudio.com/resources/rstudioconf-2018/make-shiny-fast-by-doing-as-little-work-as-possible/),
+load-testing tools, integration testing, and connecting React components to
+Shiny applications.
+
+### Teaching and preservation
+
+I have taught Clojure in workshops and courses, spoken about Common Lisp,
+Scala, Raspberry Pi, Git, testing, and functional programming, and written the
+foreword to *Clojure for the Brave and True*. I also originated
+[fswatch](./OpenSource.md#software-i-started-or-helped-start), recovered NCSA
+Mosaic’s public source history, and restored early work such as ProblemScan.
+
+The role-labeled software history is in [Open Source](./OpenSource.md).
+[Copies and Downloads](./TechWorksArchive.md) keeps papers, slides,
+recordings, transcripts, and source snapshots together by work. Selected
+early essays are in [Recovered Technical Writing](./RecoveredTechnicalWriting.md).
 
 ## Complete chronology
 
diff --git a/md/TechWorksArchive.md b/md/TechWorksArchive.md
index 0a9d5f0..2847a95 100644
--- a/md/TechWorksArchive.md
+++ b/md/TechWorksArchive.md
@@ -1,88 +1,54 @@
-# TechWorks Archive
+# Copies and Downloads
 
-Downloads and recordings from [TechWorks](./TechWorks.md). Some original event pages and media hosts have disappeared over the years, so copies of the work itself are collected here.
+These are readable and playable copies of work listed in
+[TechWorks](./TechWorks.md). Each row keeps the formats for one work together.
+Original event and project links remain in the main chronology.
 
-## Articles
+## Talks, papers, and workshops
 
-| Work | Read |
-|:-----|:-----|
-| Foreword to Clojure for the Brave and True | [Local copy](./TechWorks/ClojureBraveTrueForeword.html) |
-| OCRUG interview | [Local copy](./TechWorks/OCRUGInterview.html) |
-| My workstation OS: NetBSD | [Local copy](./TechWorks/MyWorkstationOSNetBSD.html) |
-| Shiny Server (Pro) 1.5.6 | [Local copy](./TechWorks/ShinyServer156.html) |
-| Shiny Server (Pro) 1.5.4 | [Local copy](./TechWorks/ShinyServer154.html) |
-
-## Papers and slides
-
-| Work | Download |
-|:-----|:---------|
-| JACL: A Common Lisp for Developing Single-Page Web Applications | [Paper](./TechWorks/jacl-demo-els-2020.pdf) |
+| Work | Available copies |
+|:-----|:-----------------|
+| Common Lisp for the Curious Clojurian | [Video](./TechWorks/media/video/44Q9ew9JH_U.mp4) · [captions](./TechWorks/media/video/44Q9ew9JH_U.en.vtt) |
+| JACL: A Common Lisp for Developing Single-Page Web Applications | [Paper](./TechWorks/jacl-demo-els-2020.pdf) · [video](./TechWorks/media/video/HGuTqsVh59w.mp4) · [captions](./TechWorks/media/video/HGuTqsVh59w.en.vtt) |
 | Integration Testing in Shiny Apps and Modules | [Poster](./TechWorks/2020-01-28-RStudio-Conf-Integration-Testing-ePoster.pdf) |
 | Software Engineering with Shiny | [Program](./TechWorks/BBSW_2019_program.pdf) |
 | Old School FP: A Common Lisp Experience Report | [Slides](./TechWorks/2018-12-17-CommonLispOrangeCombinator.pdf) |
 | Load testing Shiny | [Webinar page](./TechWorks/2018-09-06-load-testing-shiny-archive.html) |
-| Boot Can Build It | [Slides](./TechWorks/2015-04-20_ClojureWestBoot.pdf) |
-| FRP in ClojureScript with Javelin | [Slides](./TechWorks/Dipert-FRP_in_ClojureScript_with_Javelin.pdf) |
-| An Introduction to Raspberry Pi | [Slides](./TechWorks/2013-01-10_RaspberryPi.pdf) |
-| Programming with Values in Clojure | [Slides](./TechWorks/Dipert-ProgrammingWithValues.pdf) |
-| Computing with Clojure | [Slides](./TechWorks/decks/oscon2012-clojure/index.html) |
-| Your Own Compiler in 20 Minutes | [Slides](./TechWorks/decks/barcamp-2012/index.html) |
-| Managing Code and Quality with Git and Testing | [Slides](./TechWorks/decks/gitrubytesting/index.html) |
-| Scala, a Scalable Language | [Slides](./TechWorks/2009-09-03-Scala-a-Scalable-Language.pdf) |
-
-## Recordings
-
-These are the best available copies from the original hosts. Captions are included where available.
-
-| Recording | Watch | Captions |
-|:----------|:------|:---------|
-| Common Lisp for the Curious Clojurian | [Video](./TechWorks/media/video/44Q9ew9JH_U.mp4) | [English](./TechWorks/media/video/44Q9ew9JH_U.en.vtt) |
-| JACL | [Video](./TechWorks/media/video/HGuTqsVh59w.mp4) | [English](./TechWorks/media/video/HGuTqsVh59w.en.vtt) |
-| Boot Can Build It | [Video](./TechWorks/media/video/TcnzB2tB-8Q.mp4) | [English](./TechWorks/media/video/TcnzB2tB-8Q.en.vtt) |
-| Gherkin lightning talk | [Video](./TechWorks/media/video/bmHTFo2Rf2w.mp4) | [English](./TechWorks/media/video/bmHTFo2Rf2w.en.vtt) |
-| Uberlisp lightning talk | [Video](./TechWorks/media/video/tSw3x0rVh88.mp4) | [English](./TechWorks/media/video/tSw3x0rVh88.en.vtt) |
-| Web Programming with Hoplon | [Video](./TechWorks/media/video/wVXjExRiFy0.mp4) | [English](./TechWorks/media/video/wVXjExRiFy0.en.vtt) |
-| Flapjax lightning talk | [Video](./TechWorks/media/video/xaxF5RDdVRE.mp4) | [English](./TechWorks/media/video/xaxF5RDdVRE.en.vtt) |
-| An Introduction to Raspberry Pi | [Video](https://www.youtube.com/watch?v=fDjpAHbB-Mg) | [English](https://www.youtube.com/watch?v=fDjpAHbB-Mg) |
-| FRP in ClojureScript with Javelin | [Video](./TechWorks/media/video/infoq-frp-javelin.mp4), [audio](./TechWorks/media/audio/infoq-frp-javelin.mp3), [slides](./TechWorks/Dipert-FRP_in_ClojureScript_with_Javelin.pdf) | Not published |
-| Programming with Values in Clojure | [Video](./TechWorks/media/video/infoq-programming-with-values.mp4), [audio](./TechWorks/media/audio/infoq-programming-with-values.mp3), [slides](./TechWorks/Dipert-ProgrammingWithValues.pdf) | Not published |
-
-## Podcasts
-
-| Episode | Listen | Transcript |
-|:--------|:-------|:-----------|
-| ClojureStream S4 E19: Hoplon | [Audio](./TechWorks/media/audio/clojurestream-s4-e19.mp3) | Not published |
-| Cognicast 112: Solving problems and Boot, part II | [Audio](./TechWorks/media/audio/cognicast-112.mp3) | [Read](./TechWorks/Cognicast112.html#transcript) |
-| Cognicast 111: Solving problems and Boot | [Audio](./TechWorks/media/audio/cognicast-111.mp3) | [Read](./TechWorks/Cognicast111.html#transcript) |
-| Cognicast 52: Hoplon | [Audio](./TechWorks/media/audio/cognicast-052.mp3) | Not published |
-
-## Sites
-
-| Site | Browse |
-|:-----|:-------|
-| Just Lisp Things | [Articles](./TechWorks/authored/jlt/index.html) |
-| Wondr: Thoughts on R programming | [Articles](./TechWorks/authored/wondr/index.html) |
-
-## Source code
-
-The broader, role-labeled project and contribution inventory is in
-[OpenSource](./OpenSource.md). Complete Git histories and provenance remain
-repository-only.
-
-| Project | Repository | Download |
-|:--------|:-----------|:---------|
-| BarCamp JSONScript | [GitHub](https://github.com/alandipert/barcamp2012-jsonscript) | [Snapshot](./TechWorks/barcamp2012-jsonscript-7e4dcf7.tar.gz) |
-| BerryCam | [GitHub](https://github.com/alandipert/berrycam) | [Snapshot](./TechWorks/berrycam-d24a0c8.tar.gz) |
-| ClojureBridge curriculum | [GitHub](https://github.com/ClojureBridge/curriculum) | [Snapshot](./TechWorks/clojurebridge-curriculum-c8605f6.tar.gz) |
-| Fast Shiny | [GitHub](https://github.com/alandipert/fast-shiny) | [Snapshot](./TechWorks/fast-shiny-da0ea10.tar.gz) |
-| Flapjax demo | [GitHub](https://github.com/alandipert/flapjax-demo) | [Snapshot](./TechWorks/flapjax-demo-b719782.tar.gz) |
-| Git and Ruby testing presentation | [GitHub](https://github.com/alandipert/gitrubytesting-presentation) | [Snapshot](./TechWorks/gitrubytesting-presentation-84744ec.tar.gz) |
-| Gherkin | [GitHub](https://github.com/alandipert/gherkin) | [Snapshot](./TechWorks/gherkin-76dc034.tar.gz) |
-| OCRUG 2018 presentation | [GitHub](https://github.com/alandipert/ocrug-2018-11-27) | [Snapshot](./TechWorks/ocrug-2018-11-27-71dd0b1.tar.gz) |
-| OSCON Clojure workshop | [GitHub](https://github.com/alandipert/oscon2012-clojure) | [Snapshot](./TechWorks/oscon2012-clojure-5ac233f.tar.gz) |
-| Intermediate Shiny workshop | [GitHub](https://github.com/jcheng5/shiny-training-rstudioconf-2018) | [Snapshot](./TechWorks/shiny-training-rstudioconf-2018-2966e76.tar.gz) |
-| Scala presentation | [GitHub](https://github.com/alandipert/scala-presentation) | [Snapshot](./TechWorks/scala-presentation-82e2d00.tar.gz) |
-| TriClojure Hoplon deck | [GitHub](https://github.com/alandipert/triclojure-hoplon-deck) | [Snapshot](./TechWorks/triclojure-hoplon-deck-7afa060.tar.gz) |
-| Wombat | [GitHub](https://github.com/alandipert/wombat) | [Snapshot](./TechWorks/wombat-dbd34f4.tar.gz) |
-
-Original event pages and host links remain on [TechWorks](./TechWorks.md).
+| Boot Can Build It | [Video](./TechWorks/media/video/TcnzB2tB-8Q.mp4) · [captions](./TechWorks/media/video/TcnzB2tB-8Q.en.vtt) · [slides](./TechWorks/2015-04-20_ClojureWestBoot.pdf) |
+| Web Programming with Hoplon | [Video](./TechWorks/media/video/wVXjExRiFy0.mp4) · [captions](./TechWorks/media/video/wVXjExRiFy0.en.vtt) |
+| FRP in ClojureScript with Javelin | [Video](./TechWorks/media/video/infoq-frp-javelin.mp4) · [audio](./TechWorks/media/audio/infoq-frp-javelin.mp3) · [slides](./TechWorks/Dipert-FRP_in_ClojureScript_with_Javelin.pdf) |
+| An Introduction to Raspberry Pi | [Video](https://www.youtube.com/watch?v=fDjpAHbB-Mg) · [slides](./TechWorks/2013-01-10_RaspberryPi.pdf) · [BerryCam source](./TechWorks/berrycam-d24a0c8.tar.gz) |
+| Programming with Values in Clojure | [Video](./TechWorks/media/video/infoq-programming-with-values.mp4) · [audio](./TechWorks/media/audio/infoq-programming-with-values.mp3) · [slides](./TechWorks/Dipert-ProgrammingWithValues.pdf) |
+| Computing with Clojure | [Slides](./TechWorks/decks/oscon2012-clojure/index.html) · [workshop source](./TechWorks/oscon2012-clojure-5ac233f.tar.gz) |
+| Your Own Compiler in 20 Minutes | [Slides](./TechWorks/decks/barcamp-2012/index.html) · [source](./TechWorks/barcamp2012-jsonscript-7e4dcf7.tar.gz) |
+| Gherkin, a Lisp 1 in Bash 4 | [Video](./TechWorks/media/video/bmHTFo2Rf2w.mp4) · [captions](./TechWorks/media/video/bmHTFo2Rf2w.en.vtt) · [source](./TechWorks/gherkin-76dc034.tar.gz) |
+| Uberlisp, a Lisp for Arduino | [Video](./TechWorks/media/video/tSw3x0rVh88.mp4) · [captions](./TechWorks/media/video/tSw3x0rVh88.en.vtt) · [Wombat source](./TechWorks/wombat-dbd34f4.tar.gz) |
+| FRP in ClojureScript with Flapjax | [Video](./TechWorks/media/video/xaxF5RDdVRE.mp4) · [captions](./TechWorks/media/video/xaxF5RDdVRE.en.vtt) · [source](./TechWorks/flapjax-demo-b719782.tar.gz) |
+| Managing Code and Quality with Git and Testing | [Slides](./TechWorks/decks/gitrubytesting/index.html) · [source](./TechWorks/gitrubytesting-presentation-84744ec.tar.gz) |
+| Scala, a Scalable Language | [Slides](./TechWorks/2009-09-03-Scala-a-Scalable-Language.pdf) · [source](./TechWorks/scala-presentation-82e2d00.tar.gz) |
+
+## Interviews, podcasts, and articles
+
+| Work | Available copies |
+|:-----|:-----------------|
+| ClojureStream S4 E19: Hoplon | [Audio](./TechWorks/media/audio/clojurestream-s4-e19.mp3) |
+| Cognicast 112: Solving problems and Boot, part II | [Audio](./TechWorks/media/audio/cognicast-112.mp3) · [transcript](./TechWorks/Cognicast112.html#transcript) |
+| Cognicast 111: Solving problems and Boot | [Audio](./TechWorks/media/audio/cognicast-111.mp3) · [transcript](./TechWorks/Cognicast111.html#transcript) |
+| Cognicast 52: Hoplon | [Audio](./TechWorks/media/audio/cognicast-052.mp3) |
+| Foreword to *Clojure for the Brave and True* | [Read](./TechWorks/ClojureBraveTrueForeword.html) |
+| OCRUG interview | [Read](./TechWorks/OCRUGInterview.html) |
+| My workstation OS: NetBSD | [Read](./TechWorks/MyWorkstationOSNetBSD.html) |
+| Shiny Server (Pro) 1.5.6 | [Read](./TechWorks/ShinyServer156.html) |
+| Shiny Server (Pro) 1.5.4 | [Read](./TechWorks/ShinyServer154.html) |
+
+## Sites and additional source
+
+| Work | Available copies |
+|:-----|:-----------------|
+| Just Lisp Things | [Browse](./TechWorks/authored/jlt/index.html) |
+| Wondr: Thoughts on R programming | [Browse](./TechWorks/authored/wondr/index.html) |
+| Fast Shiny | [Source](./TechWorks/fast-shiny-da0ea10.tar.gz) |
+| Intermediate Shiny workshop | [Source](./TechWorks/shiny-training-rstudioconf-2018-2966e76.tar.gz) |
+| OCRUG React and Shiny presentation | [Source](./TechWorks/ocrug-2018-11-27-71dd0b1.tar.gz) |
+| TriClojure Hoplon deck | [Source](./TechWorks/triclojure-hoplon-deck-7afa060.tar.gz) |
+| ClojureBridge curriculum | [Source](./TechWorks/clojurebridge-curriculum-c8605f6.tar.gz) |
diff --git a/md/Writings.md b/md/Writings.md
index 1af30c5..fb2e614 100644
--- a/md/Writings.md
+++ b/md/Writings.md
@@ -2,18 +2,18 @@
 
 I keep long-form pieces here when I want to explore a topic beyond a paragraph or status post.
 
-Programming
+## Programming
 - [Klong](./Klong.md) recounts my return to array programming and the browser notebook I built to keep the real interpreter close at hand.
-- [RecoveredTechnicalWriting](./RecoveredTechnicalWriting.md) collects the Lisp, structural-editing, Datomic, Mosaic, cloud-computing, and polyglot posts worth surfacing from my old blog.
-- [OpenSource](./OpenSource.md) records the software I created and the established projects I contributed to, with roles and evidence kept separate.
+- [Recovered Technical Writing](./RecoveredTechnicalWriting.md) collects Lisp, structural-editing, Datomic, Mosaic, cloud-computing, and polyglot posts from my old blog.
+- [Open Source](./OpenSource.md) records the software I created and the established projects I contributed to, with roles kept distinct.
 
-AI
+## AI
 - [AIAndRisk](./AIAndRisk.md) weighs how AI shifts upside toward owners who can offload risk, while salaried engineers must keep understanding in their own heads.
 - [Coherence](./Coherence.md) explains how I use an AI coding agent like an employee by aligning intent, artifacts, and execution.
 - [BeNotAfraid](./BeNotAfraid.md) offers a reminder not to fear AI and to keep our gaze on the Creator who loves us.
 - [ActorAgents](./ActorAgents.md) describes how the actor model keeps agentic systems small, explicit, and safe through capability passing and limits.
 
-Faith
+## Faith
 - [WhoIsGod](./WhoIsGod.md) is the entry point for the [Names](./NamesOfGod.md), [Covenant Names](./CovenantNamesOfGod.md), and [Covenant](./CovenantsOfGod.md) tables so readers can find every reference from a single hub.
 - [OurEmptyPromises](./OurEmptyPromises.md) reflects on the limits of human promises and why certainty only comes from God.
 - [RandallRDipertEulogy](./RandallRDipertEulogy.md) shares the eulogy I gave for my dad and the lessons I carry forward.
diff --git a/tests/test_sitegen.py b/tests/test_sitegen.py
index 183cfe8..b946ae9 100644
--- a/tests/test_sitegen.py
+++ b/tests/test_sitegen.py
@@ -35,6 +35,43 @@ class SitegenTests(unittest.TestCase):
         text = '<a href="https://example.com/a.md">web</a> <a href="#hello">section</a>'
         self.assertEqual(sitegen.rewrite_html_links(text, source), text)
 
+    def test_mark_identity_links_adds_rel_me_only_to_known_profiles(self) -> None:
+        text = (
+            '<a href="https://github.com/alandipert/">me</a> '
+            '<a href="https://github.com/example/">other</a>'
+        )
+        self.assertEqual(
+            sitegen.mark_identity_links(text),
+            '<a href="https://github.com/alandipert/" rel="me">me</a> '
+            '<a href="https://github.com/example/">other</a>',
+        )
+
+    def test_document_title_and_description_use_readable_content(self) -> None:
+        document = ET.Element(f"{sitegen.NS}document")
+        heading = ET.SubElement(document, f"{sitegen.NS}heading", level="1")
+        ET.SubElement(heading, f"{sitegen.NS}text").text = "Open Source"
+        paragraph = ET.SubElement(document, f"{sitegen.NS}paragraph")
+        ET.SubElement(paragraph, f"{sitegen.NS}text").text = (
+            "A sufficiently detailed description of this page and its purpose."
+        )
+        self.assertEqual(sitegen.document_title(document, "Fallback"), "Open Source")
+        self.assertEqual(
+            sitegen.document_description(document, "Fallback"),
+            "A sufficiently detailed description of this page and its purpose.",
+        )
+
+    def test_metadata_contains_canonical_social_and_person_data(self) -> None:
+        rendered = sitegen.metadata_html(
+            "Home", "Alan & his work", "https://example.com/Home.html", home=True
+        )
+        self.assertIn('name="description" content="Alan &amp; his work"', rendered)
+        self.assertIn('rel="canonical" href="https://example.com/Home.html"', rendered)
+        self.assertIn('property="og:title" content="Home"', rendered)
+        script = rendered.split('<script type="application/ld+json">', 1)[1].split(
+            "</script>", 1
+        )[0]
+        self.assertEqual(__import__("json").loads(script)["@type"], "Person")
+
     def test_backlinks_are_sorted_and_deduplicated(self) -> None:
         with tempfile.TemporaryDirectory() as directory:
             src = Path(directory)
diff --git a/tools/sitegen.py b/tools/sitegen.py
index 1b32ab0..3992b24 100644
--- a/tools/sitegen.py
+++ b/tools/sitegen.py
@@ -6,6 +6,7 @@ from __future__ import annotations
 import argparse
 import hashlib
 import html
+import json
 import os
 import re
 import shlex
@@ -36,6 +37,12 @@ HTML_HREF_PATTERN = re.compile(r'(<a\b[^>]*\bhref=")([^"]+)(")')
 ALLOWED_PATH_PATTERN = re.compile(r"^[A-Za-z0-9._\-/]+$")
 TAG_PATTERN = re.compile(r"<[^>]+>")
 LATEST_WORKS_MARKER = "<!-- latest-works:6 -->"
+IDENTITY_URLS = (
+    "https://github.com/alandipert/",
+    "https://www.linkedin.com/in/alandipert/",
+    "https://news.ycombinator.com/user?id=wooby",
+    "https://twitter.com/alandipert",
+)
 
 
 def parse_args() -> argparse.Namespace:
@@ -129,6 +136,13 @@ def rewrite_html_links(text: str, source: Path) -> str:
     return HTML_HREF_PATTERN.sub(replace, text)
 
 
+def mark_identity_links(text: str) -> str:
+    for url in IDENTITY_URLS:
+        quoted = html.escape(url, quote=True)
+        text = text.replace(f'href="{quoted}"', f'href="{quoted}" rel="me"')
+    return text
+
+
 def iter_links(node: ET.Element):
     if node.tag == f"{NS}link":
         destination = node.attrib.get("destination", "")
@@ -188,21 +202,77 @@ def page_class(title: str) -> str:
     return f"page-{slug or 'untitled'}"
 
 
-def template(text: str, *, root: str, title: str, page_css: str, info: str) -> str:
+def document_title(document: ET.Element, fallback: str) -> str:
+    for node in document.iter():
+        if local_name(node.tag) == "heading" and node.attrib.get("level") == "1":
+            title = "".join(node.itertext()).strip()
+            if title:
+                return title
+    return fallback
+
+
+def document_description(document: ET.Element, fallback: str) -> str:
+    for node in document.iter():
+        if local_name(node.tag) != "paragraph":
+            continue
+        value = " ".join("".join(node.itertext()).split())
+        if len(value) < 40:
+            continue
+        if len(value) > 180:
+            value = value[:177].rsplit(" ", 1)[0] + "…"
+        return value
+    return fallback
+
+
+def metadata_html(title: str, description: str, canonical: str, *, home: bool) -> str:
+    values = [
+        f'  <meta name="description" content="{html.escape(description, quote=True)}">',
+        f'  <link rel="canonical" href="{html.escape(canonical, quote=True)}">',
+        '  <meta property="og:type" content="website">',
+        f'  <meta property="og:title" content="{html.escape(title, quote=True)}">',
+        f'  <meta property="og:description" content="{html.escape(description, quote=True)}">',
+        f'  <meta property="og:url" content="{html.escape(canonical, quote=True)}">',
+    ]
+    if home:
+        person = {
+            "@context": "https://schema.org",
+            "@type": "Person",
+            "name": "Alan Dipert",
+            "url": canonical,
+            "sameAs": [
+                "https://github.com/alandipert/",
+                "https://www.linkedin.com/in/alandipert/",
+                "https://news.ycombinator.com/user?id=wooby",
+                "https://twitter.com/alandipert",
+            ],
+            "knowsAbout": [
+                "Programming languages", "Developer tools", "Open-source software",
+                "Software preservation",
+            ],
+        }
+        encoded = json.dumps(person, ensure_ascii=False, separators=(",", ":"))
+        values.append(f'  <script type="application/ld+json">{encoded}</script>')
+    return "\n".join(values)
+
+
+def template(
+    text: str, *, root: str, title: str, page_css: str, info: str, meta: str = ""
+) -> str:
     return (
         text.replace("@ROOT@", root)
         .replace("@TITLE@", html.escape(title))
         .replace("@PAGE_CLASS@", page_css)
         .replace("@BUILDINFO@", info)
+        .replace("@META@", meta)
     )
 
 
-def backlinks_html(sources: list[str], root: str) -> str:
+def backlinks_html(sources: list[str], root: str, titles: dict[str, str]) -> str:
     if not sources:
         return ""
     items = "\n    ".join(
         f'<li><a href="{html.escape(root + source[:-3] + ".html", quote=True)}">'
-        f"{html.escape(source[:-3])}</a></li>"
+        f"{html.escape(titles.get(source, source[:-3]))}</a></li>"
         for source in sources
     )
     return (
@@ -224,23 +294,33 @@ def write_page(
     backlinks: dict[str, list[str]],
     updates: list[Update],
     src: Path,
+    document: ET.Element,
+    titles: dict[str, str],
+    base_url: str,
 ) -> None:
     output_relative = relative.with_suffix(".html")
     root = root_prefix(output_relative)
-    title = source.stem
+    title = titles[relative.as_posix()]
+    browser_title = "Alan Dipert — Programmer and builder" if relative == Path("Home.md") else f"{title} — Alan Dipert"
+    description = document_description(
+        document, "Writing and projects by programmer and builder Alan Dipert."
+    )
+    canonical = urljoin(base_url, output_relative.as_posix())
+    meta = metadata_html(title, description, canonical, home=relative == Path("Home.md"))
     info = buildinfo.build_info(source.as_posix())
     body = rewrite_html_links(
         run_cmark(output="html", source=source),
         source,
     )
     if relative == Path("Home.md"):
-        body = enhance_homepage(body, updates, source, src)
+        body = enhance_homepage(body, updates, source, src, titles)
+        body = mark_identity_links(body)
     rendered = "".join(
         (
-            template(head, root=root, title=title, page_css=page_class(title), info=info),
+            template(head, root=root, title=browser_title, page_css=page_class(title), info=info, meta=meta),
             body,
-            backlinks_html(backlinks.get(relative.as_posix(), []), root),
-            template(foot, root=root, title=title, page_css=page_class(title), info=info),
+            backlinks_html(backlinks.get(relative.as_posix(), []), root, titles),
+            template(foot, root=root, title=browser_title, page_css=page_class(title), info=info),
         )
     )
     destination = out / output_relative
@@ -252,6 +332,7 @@ class IndexNode:
     def __init__(self, name: str) -> None:
         self.name = name
         self.href: str | None = None
+        self.title: str | None = None
         self.children: OrderedDict[str, IndexNode] = OrderedDict()
 
     def child(self, name: str) -> "IndexNode":
@@ -260,13 +341,14 @@ class IndexNode:
         return self.children[name]
 
 
-def index_tree(paths: list[Path]) -> IndexNode:
+def index_tree(paths: list[Path], titles: dict[str, str]) -> IndexNode:
     root = IndexNode("")
     for path in paths:
         node = root
         for part in path.with_suffix("").parts:
             node = node.child(part)
         node.href = path.as_posix()
+        node.title = titles.get(path.as_posix())
     return root
 
 
@@ -274,7 +356,7 @@ def render_index_children(node: IndexNode, depth: int = 0) -> list[str]:
     indent = "  " * depth
     lines: list[str] = []
     for child in node.children.values():
-        display = html.escape(child.name)
+        display = html.escape(child.title or child.name)
         line = (
             f'{indent}<li><a href="./{html.escape(child.href[:-3])}.html">{display}</a>'
             if child.href
@@ -289,10 +371,20 @@ def render_index_children(node: IndexNode, depth: int = 0) -> list[str]:
     return lines
 
 
-def write_index(out: Path, src: Path, paths: list[Path], head: str, foot: str) -> None:
+def write_index(
+    out: Path, src: Path, paths: list[Path], titles: dict[str, str],
+    head: str, foot: str, base_url: str,
+) -> None:
     info = buildinfo.build_info(src.as_posix())
-    values = {"root": "", "title": "Index", "page_css": "page-index", "info": info}
-    index_lines = ['<ul class="site-index">', *render_index_children(index_tree(paths)), "</ul>"]
+    values = {
+        "root": "", "title": "Index — Alan Dipert", "page_css": "page-index",
+        "info": info,
+        "meta": metadata_html(
+            "Index", "An index of writing and projects by Alan Dipert.",
+            urljoin(base_url, "Index.html"), home=False,
+        ),
+    }
+    index_lines = ['<ul class="site-index">', *render_index_children(index_tree(paths, titles)), "</ul>"]
     rendered = "".join(
         (
             template(head, **values),
@@ -426,13 +518,24 @@ def breakable_title(title: str) -> str:
     return "<wbr>".join(html.escape(part) for part in parts)
 
 
-def render_latest_works(updates: list[Update], home: Path, src: Path) -> str:
+def render_latest_works(
+    updates: list[Update], home: Path, src: Path,
+    titles: dict[str, str] | None = None,
+) -> str:
     works = latest_local_works(updates, home, src)
     if not works:
         raise RuntimeError("No local works found for the homepage Explore section")
     items: list[str] = []
     for update in works:
         title = update.work_title or Path(update.source_destination).stem
+        destination = update.source_destination.split("#", 1)[0]
+        if titles and destination:
+            target = (home.parent / destination).resolve()
+            try:
+                key = target.relative_to(src.resolve()).as_posix()
+                title = titles.get(key, title)
+            except ValueError:
+                pass
         href = rewrite_destination(update.source_destination, home)
         visible_date = f"{update.date.day} {update.date.strftime('%b')} {update.date.year}"
         plain = html.unescape(TAG_PATTERN.sub("", update.body))
@@ -460,13 +563,18 @@ def render_latest_works(updates: list[Update], home: Path, src: Path) -> str:
     )
 
 
-def enhance_homepage(body: str, updates: list[Update], home: Path, src: Path) -> str:
+def enhance_homepage(
+    body: str, updates: list[Update], home: Path, src: Path,
+    titles: dict[str, str] | None = None,
+) -> str:
     if body.count(LATEST_WORKS_MARKER) != 1:
         raise RuntimeError(f"Home.md must contain exactly one {LATEST_WORKS_MARKER!r} marker")
     updates_heading = "<h2>Updates</h2>"
     if body.count(updates_heading) != 1:
         raise RuntimeError("Home.md must contain exactly one level-two Updates heading")
-    body = body.replace(LATEST_WORKS_MARKER, render_latest_works(updates, home, src), 1)
+    body = body.replace(
+        LATEST_WORKS_MARKER, render_latest_works(updates, home, src, titles), 1
+    )
     return body.replace(updates_heading, '<h2 id="updates">Updates</h2>', 1)
 
 
@@ -544,6 +652,10 @@ def build(src: Path, out: Path, head_path: Path, foot_path: Path, base_url: str)
     head = head_path.read_text(encoding="utf-8")
     foot = foot_path.read_text(encoding="utf-8")
     documents = {path: parse_markdown(path) for path in paths}
+    titles = {
+        path.relative_to(src).as_posix(): document_title(document, path.stem)
+        for path, document in documents.items()
+    }
     backlinks = backlink_map(src, documents)
     relatives = [path.relative_to(src) for path in paths]
     home = src / "Home.md"
@@ -552,8 +664,11 @@ def build(src: Path, out: Path, head_path: Path, foot_path: Path, base_url: str)
     normalized_base = base_url.rstrip("/") + "/"
     updates = updates_from_home(documents[home], normalized_base)
     for source, relative in zip(paths, relatives):
-        write_page(source, relative, out, head, foot, backlinks, updates, src)
-    write_index(out, src, relatives, head, foot)
+        write_page(
+            source, relative, out, head, foot, backlinks, updates, src,
+            documents[source], titles, normalized_base,
+        )
+    write_index(out, src, relatives, titles, head, foot, normalized_base)
     write_rss(out, updates, normalized_base)
     validate_internal_references(out)
 
diff --git a/tpl/head.html b/tpl/head.html
index 03d2048..7784ac4 100644
--- a/tpl/head.html
+++ b/tpl/head.html
@@ -4,6 +4,7 @@
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width,initial-scale=1">
   <title>@TITLE@</title>
+@META@
   <link rel="stylesheet" href="@ROOT@style.css">
   <link rel="alternate" type="application/rss+xml" title="The Homepage — Updates" href="@ROOT@updates.xml">
   <script async src="https://www.googletagmanager.com/gtag/js?id=G-XCMVL5K44X"></script>