| author | Alan Dipert
<alan@tailrecursion.com> 2026-08-10 17:32:33 UTC |
| committer | Alan Dipert
<alan@tailrecursion.com> 2026-08-10 17:32:33 UTC |
| parent | 8a715b3951978d7bd9331d95a323194588571db7 |
| 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  -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 & 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>