diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e9a66f8..8954325 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -42,10 +42,16 @@ jobs:
- name: Install Dependencies
run: npm ci
+
+ - name: Install Playwright Browsers
+ run: npx playwright install --with-deps
- name: Run Linting
run: npm run lint
-
+
+ - name: Run Markdown Linting
+ run: npm run lint:markdown
+
- name: Run Spell Check (Source)
run: npm run spellcheck
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
new file mode 100644
index 0000000..61cbfe7
--- /dev/null
+++ b/.markdownlint.yaml
@@ -0,0 +1,56 @@
+# markdownlint configuration
+# See https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md
+
+# Heading hierarchy rules
+MD001: true # heading-increment - headings should only increment by one level at a time
+MD025: true # single-title/single-h1 - documents should have a single top-level heading
+
+# Heading style rules
+MD003: # heading-style - heading style should be consistent
+ style: atx # Use ATX style (##) not Setext style (underlines)
+MD018: true # no-missing-space-atx - no space after hash on atx style heading
+MD019: true # no-multiple-space-atx - multiple spaces after hash on atx style heading
+MD023: true # heading-start-left - headings must start at the beginning of the line
+MD024: # no-duplicate-heading - multiple headings with the same content
+ siblings_only: true # Allow duplicate headings in different sections
+MD026: # no-trailing-punctuation - trailing punctuation in heading
+ punctuation: ".,;:!" # Don't allow these, but allow ? for questions
+
+# First line rules
+MD041: false # first-line-heading/first-line-h1 - disabled because we use frontmatter
+
+# Line length
+MD013: false # line-length - disabled for prose content
+
+# Code blocks
+MD046: # code-block-style - code block style should be consistent
+ style: fenced # Use fenced code blocks (```) not indented
+
+# Lists
+MD004: # ul-style - unordered list style should be consistent
+ style: dash # Use dashes for unordered lists
+MD029: # ol-prefix - ordered list item prefix
+ style: ordered # Use sequential numbers (1, 2, 3) not all 1s
+MD030: # list-marker-space - spaces after list markers
+ ul_single: 1
+ ul_multi: 1
+ ol_single: 1
+ ol_multi: 1
+
+# Blank lines
+MD012: # no-multiple-blanks - multiple consecutive blank lines
+ maximum: 2 # Allow up to 2 blank lines for visual separation
+MD022: # blanks-around-headings - headings should be surrounded by blank lines
+ lines_above: 1
+ lines_below: 1
+
+# Inline HTML
+MD033: false # no-inline-html - disabled to allow inline HTML in MDX
+
+# Emphasis
+MD036: false # no-emphasis-as-heading - disabled to allow emphasized text that looks like headings
+
+# Whitespace and formatting
+MD009: false # no-trailing-spaces - disabled as trailing spaces don't affect rendering
+MD007: false # ul-indent - disabled to allow flexible list indentation
+MD060: false # table-column-style - disabled as table alignment is cosmetic
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..4beccb0
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,99 @@
+# Guide to `plx.github.io`
+
+This file provides guidance to coding agents like Claude Code and Codex.
+
+## Repository Overview
+
+This is an Astro-based static site deployed to GitHub Pages. The site uses Tailwind CSS for styling and is built using npm/Node.js, then deployed via GitHub Actions; as such, any changes pushed to `main` will be automatically deployed—convenient, but be careful!
+
+## Key Commands
+
+The repository includes a justfile to gather all project commands in a single place; if you're unsure "how do I X?", look there first.
+
+It also manages the preview server using a tool called `trop` (https://github.com/plx/trop).
+
+Some key commands are:
+
+- just install: installs dependencies (npm ci)
+- just preview: launches dev server with hot reload (port automatically allocated by trop)
+- just shutdown: kills dev server if running (port automatically allocated by trop)
+- just build: builds the site for production (to dist/)
+- just spellcheck: checks spelling in source files
+- just spellcheck-html: checks spelling in built HTML output
+- just lint: runs ESLint on all files
+- just lint-fix: auto-fixes ESLint issues where possible
+- just validate: runs all validation checks (lint + spellcheck + build + links)
+
+## Key Technical Decisions
+
+- **Framework**: Astro with React integration
+- **Styling**: Tailwind CSS with Typography plugin
+- **Content**: MDX support for enhanced markdown
+- **Build**: Static site generation to `dist/` folder
+- **Deployment**: GitHub Actions workflow deploys to GitHub Pages
+- **Site URL**: https://plx.github.io
+
+Additionally, we aim to have *reasonable* accessibility support throughout the site.
+
+## Content Structure
+
+The site's content is organized into three main collections:
+
+- Blog posts (longer-form articles): `src/content/blog/`
+- Briefs (short notes): `src/content/briefs/`
+- Projects: `src/content/projects/`
+
+Here are brief remarks about each.
+
+### Blog Posts
+
+Structured as folders containing *at least* an `index.md` file, placed in `src/content/blog/`; for example, `my-new-post` looks like:
+
+```
+src/content/blog/my-new-post/
+src/content/blog/my-new-post/index.md
+```
+
+Posts should include front matter with relevant metadata.
+
+### Briefs (Short Notes)
+
+Organized into categories represented as folders within `src/content/briefs/`, and stored *directly* as markdown files (no additional nesting / generic `index.md`).
+For example, the following contains two briefs—one in the `swift-warts` category and one in the `claude-code` category:
+
+```
+src/content/briefs/swift-warts/my-swift-brief.md
+src/content/briefs/claude-code/my-claude-brief.md
+```
+
+Categories are auto-discovered from folder names. To add a new category, simply create a new folder.
+Categories may also customize their display name, description, and sort priority by establishing a `category.yaml` file in the category folder; this is useful because the category name is used in multiple places throughout the site, and benefits from having distinct, contextually-appropriate representations.
+
+### Projects (Descriptions of Projects)
+
+Structured analogously to "Blog Posts`, but placed in `src/content/projects/`, instead.
+
+## Directory Structure
+
+- `src/`: Source code
+ - `components/`: Astro components
+ - `content/`: Content collections (blog, briefs, projects)
+ - `blog/`: where blog posts live
+ - `briefs/`: where briefs live
+ - `projects/`: where project pages live
+ - `layouts/`: Page layouts
+ - `pages/`: Routes and pages
+ - `styles/`: Global styles
+ - `lib/`: Utilities
+- `public/`: Static assets (fonts, images, etc.)
+- `dist/`: Build output (generated, not in repo)
+- `.github/workflows/`: GitHub Actions workflows
+
+## Testing and QA
+
+The repository has Playwright browser automation available via MCP for testing and QA purposes. This enables:
+
+- Visual testing and screenshot capture
+- Navigation testing
+- Content verification
+- Browser automation tasks
diff --git a/CLAUDE.md b/CLAUDE.md
index bd4ff16..43c994c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,98 +1 @@
-# CLAUDE.md
-
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
-
-## Repository Overview
-
-This is an Astro-based static site deployed to GitHub Pages. The site uses Tailwind CSS for styling and is built using npm/Node.js, then deployed via GitHub Actions; as such, any changes pushed to `main` will be automatically deployed—convenient, but be careful!
-
-## Key Commands
-
-The repository includes a justfile to gather all project commands in a single place; if you're unsure "how do I X?", look there first.
-It also manages the preview server using a tool called `trop` (https://github.com/plx/trop).
-
-Some key commands are:
-
-- just install: installs dependencies (npm ci)
-- just preview: launches dev server with hot reload (port automatically allocated by trop)
-- just shutdown: kills dev server if running (port automatically allocated by trop)
-- just build: builds the site for production (to dist/)
-- just spellcheck: checks spelling in source files
-- just spellcheck-html: checks spelling in built HTML output
-- just lint: runs ESLint on all files
-- just lint-fix: auto-fixes ESLint issues where possible
-- just validate: runs all validation checks (lint + spellcheck + build + links)
-
-## Key Technical Decisions
-
-- **Framework**: Astro with React integration
-- **Styling**: Tailwind CSS with Typography plugin
-- **Content**: MDX support for enhanced markdown
-- **Build**: Static site generation to `dist/` folder
-- **Deployment**: GitHub Actions workflow deploys to GitHub Pages
-- **Site URL**: https://plx.github.io
-
-Additionally, we aim to have *reasonable* accessibility support throughout the site.
-
-## Content Structure
-
-The site's content is organized into three main collections:
-
-- Blog posts (longer-form articles): `src/content/blog/`
-- Briefs (short notes): `src/content/briefs/`
-- Projects: `src/content/projects/`
-
-Here are brief remarks about each.
-
-### Blog Posts
-
-Structured as folders containing *at least* an `index.md` file, placed in `src/content/blog/`; for example, `my-new-post` looks like:
-
-```
-src/content/blog/my-new-post/
-src/content/blog/my-new-post/index.md
-```
-
-Posts should include front matter with relevant metadata.
-
-### Briefs (Short Notes)
-
-Organized into categories represented as folders within `src/content/briefs/`, and stored *directly* as markdown files (no additional nesting / generic `index.md`).
-For example, the following contains two briefs—one in the `swift-warts` category and one in the `claude-code` category:
-
-```
-src/content/briefs/swift-warts/my-swift-brief.md
-src/content/briefs/claude-code/my-claude-brief.md
-```
-
-Categories are auto-discovered from folder names. To add a new category, simply create a new folder.
-Categories may also customize their display name, description, and sort priority by establishing a `category.yaml` file in the category folder; this is useful because the category name is used in multiple places throughout the site, and benefits from having distinct, contextually-appropriate representations.
-
-### Projects (Descriptions of Projects)
-
-Structured analogously to "Blog Posts`, but placed in `src/content/projects/`, instead.
-
-## Directory Structure
-
-- `src/`: Source code
- - `components/`: Astro components
- - `content/`: Content collections (blog, briefs, projects)
- - `blog/`: where blog posts live
- - `briefs/`: where briefs live
- - `projects/`: where project pages live
- - `layouts/`: Page layouts
- - `pages/`: Routes and pages
- - `styles/`: Global styles
- - `lib/`: Utilities
-- `public/`: Static assets (fonts, images, etc.)
-- `dist/`: Build output (generated, not in repo)
-- `.github/workflows/`: GitHub Actions workflows
-
-## Testing and QA
-
-The repository has Playwright browser automation available via MCP for testing and QA purposes. This enables:
-
-- Visual testing and screenshot capture
-- Navigation testing
-- Content verification
-- Browser automation tasks
+@AGENTS.md
diff --git a/justfile b/justfile
index 030ab91..4c6b8f9 100644
--- a/justfile
+++ b/justfile
@@ -104,6 +104,10 @@ lint:
lint-fix:
npm run lint:fix
+# Lint-markdown: runs markdownlint on content files
+lint-markdown:
+ npm run lint:markdown
+
# Validate: runs all validation checks (lint + spellcheck + build + links)
validate:
npm run validate:all
diff --git a/package-lock.json b/package-lock.json
index 81548af..d618d83 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -46,6 +46,7 @@
"@types/xml2js": "^0.4.14",
"cspell": "^9.2.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
+ "markdownlint-cli2": "^0.19.1",
"prettier": "^3.6.2",
"xml2js": "^0.6.2"
}
@@ -3013,6 +3014,19 @@
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"license": "MIT"
},
+ "node_modules/@sindresorhus/merge-streams": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
+ "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
@@ -3135,6 +3149,13 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"license": "MIT"
},
+ "node_modules/@types/katex": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz",
+ "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/mdast": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
@@ -7022,6 +7043,27 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/globby": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-15.0.0.tgz",
+ "integrity": "sha512-oB4vkQGqlMl682wL1IlWd02tXCbquGWM4voPEI85QmNKCaw8zGTm1f1rubFgkg3Eli2PtKlFgrnmUqasbQWlkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/merge-streams": "^4.0.0",
+ "fast-glob": "^3.3.3",
+ "ignore": "^7.0.5",
+ "path-type": "^6.0.0",
+ "slash": "^5.1.0",
+ "unicorn-magic": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -8173,9 +8215,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -8248,6 +8290,33 @@
"node": ">=4.0"
}
},
+ "node_modules/katex": {
+ "version": "0.16.25",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.25.tgz",
+ "integrity": "sha512-woHRUZ/iF23GBP1dkDQMh1QBad9dmr8/PAwNA54VrSOVYgI12MAcE14TqnDdQOdzyEonGzMepYnqBMYdsoAr8Q==",
+ "dev": true,
+ "funding": [
+ "https://opencollective.com/katex",
+ "https://github.com/sponsors/katex"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^8.3.0"
+ },
+ "bin": {
+ "katex": "cli.js"
+ }
+ },
+ "node_modules/katex/node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -8323,6 +8392,16 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT"
},
+ "node_modules/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "uc.micro": "^2.0.0"
+ }
+ },
"node_modules/local-pkg": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz",
@@ -8429,6 +8508,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/markdown-it": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
+ "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "^4.4.0",
+ "linkify-it": "^5.0.0",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.mjs"
+ }
+ },
"node_modules/markdown-table": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
@@ -8439,6 +8536,74 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/markdownlint": {
+ "version": "0.39.0",
+ "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.39.0.tgz",
+ "integrity": "sha512-Xt/oY7bAiHwukL1iru2np5LIkhwD19Y7frlsiDILK62v3jucXCD6JXlZlwMG12HZOR+roHIVuJZrfCkOhp6k3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "micromark": "4.0.2",
+ "micromark-core-commonmark": "2.0.3",
+ "micromark-extension-directive": "4.0.0",
+ "micromark-extension-gfm-autolink-literal": "2.1.0",
+ "micromark-extension-gfm-footnote": "2.1.0",
+ "micromark-extension-gfm-table": "2.1.1",
+ "micromark-extension-math": "3.1.0",
+ "micromark-util-types": "2.0.2"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/DavidAnson"
+ }
+ },
+ "node_modules/markdownlint-cli2": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.19.1.tgz",
+ "integrity": "sha512-p3JTemJJbkiMjXEMiFwgm0v6ym5g8K+b2oDny+6xdl300tUKySxvilJQLSea48C6OaYNmO30kH9KxpiAg5bWJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "globby": "15.0.0",
+ "js-yaml": "4.1.1",
+ "jsonc-parser": "3.3.1",
+ "markdown-it": "14.1.0",
+ "markdownlint": "0.39.0",
+ "markdownlint-cli2-formatter-default": "0.0.6",
+ "micromatch": "4.0.8"
+ },
+ "bin": {
+ "markdownlint-cli2": "markdownlint-cli2-bin.mjs"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/DavidAnson"
+ }
+ },
+ "node_modules/markdownlint-cli2-formatter-default": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.6.tgz",
+ "integrity": "sha512-VVDGKsq9sgzu378swJ0fcHfSicUnMxnL8gnLm/Q4J/xsNJ4e5bA6lvAz7PCzIl0/No0lHyaWdqVD2jotxOSFMQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/DavidAnson"
+ },
+ "peerDependencies": {
+ "markdownlint-cli2": ">=0.0.4"
+ }
+ },
+ "node_modules/markdownlint-cli2/node_modules/jsonc-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
+ "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -8768,6 +8933,13 @@
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
"license": "CC0-1.0"
},
+ "node_modules/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -8846,6 +9018,26 @@
"micromark-util-types": "^2.0.0"
}
},
+ "node_modules/micromark-extension-directive": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz",
+ "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "parse-entities": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/micromark-extension-gfm": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
@@ -8967,6 +9159,26 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/micromark-extension-math": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz",
+ "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/katex": "^0.16.0",
+ "devlop": "^1.0.0",
+ "katex": "^0.16.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/micromark-extension-mdx-expression": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz",
@@ -10218,6 +10430,19 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/path-type": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz",
+ "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
@@ -10574,6 +10799,16 @@
"node": ">=6"
}
},
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/quansync": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
@@ -11527,6 +11762,19 @@
"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==",
"license": "MIT"
},
+ "node_modules/slash": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
+ "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/smol-toml": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.4.2.tgz",
@@ -12338,6 +12586,13 @@
"semver": "^7.3.8"
}
},
+ "node_modules/uc.micro": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/ufo": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
@@ -12410,6 +12665,19 @@
"tiny-inflate": "^1.0.0"
}
},
+ "node_modules/unicorn-magic": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
+ "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/unified": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
diff --git a/package.json b/package.json
index 501e21f..3bcffd1 100644
--- a/package.json
+++ b/package.json
@@ -11,11 +11,12 @@
"astro": "astro",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
+ "lint:markdown": "markdownlint-cli2 \"src/content/**/*.{md,mdx}\"",
"spellcheck": "cspell \"src/**/*.{md,mdx,ts,tsx,js,jsx,astro}\" --no-progress",
"spellcheck:html": "cspell \"dist/**/*.html\" --no-progress",
"spellcheck:all": "npm run spellcheck && npm run build && npm run spellcheck:html",
"validate:links": "node scripts/validate-links.js",
- "validate:all": "npm run lint && npm run spellcheck && npm run build && npm run spellcheck:html && npm run validate:links",
+ "validate:all": "npm run lint && npm run lint:markdown && npm run spellcheck && npm run build && npm run spellcheck:html && npm run validate:links",
"test:ci": "npm run lint && npm run spellcheck && npm run build && npm run spellcheck:html && npm run validate:links",
"test:ci:verbose": "echo '🔍 Running CI validation locally...' && npm run lint && echo '✓ Linting passed' && npm run spellcheck && echo '✓ Source spell check passed' && npm run build && echo '✓ Build succeeded' && npm run spellcheck:html && echo '✓ HTML spell check passed' && npm run validate:links && echo '✓ Link validation passed' && echo '✅ All CI checks passed!'",
"qa": "playwright test --ignore-snapshots",
@@ -68,6 +69,7 @@
"@types/xml2js": "^0.4.14",
"cspell": "^9.2.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
+ "markdownlint-cli2": "^0.19.1",
"prettier": "^3.6.2",
"xml2js": "^0.6.2"
}
diff --git a/src/components/Header.astro b/src/components/Header.astro
index ff4ddb2..811b7a9 100644
--- a/src/components/Header.astro
+++ b/src/components/Header.astro
@@ -28,6 +28,12 @@ import { SITE } from "@consts";
projects
+
+ {`/`}
+
+
+ about
+
diff --git a/src/components/Link.astro b/src/components/Link.astro
index 2014ca2..52a0875 100644
--- a/src/components/Link.astro
+++ b/src/components/Link.astro
@@ -10,9 +10,10 @@ type Props = {
const { href, external, underline = true, ...rest } = Astro.props;
---
-
diff --git a/src/content/briefs/swift-warts/first-trailing-closure-cannot-have-a-label.md b/src/content/briefs/swift-warts/first-trailing-closure-cannot-have-a-label.md
index 2f9168c..e6907aa 100644
--- a/src/content/briefs/swift-warts/first-trailing-closure-cannot-have-a-label.md
+++ b/src/content/briefs/swift-warts/first-trailing-closure-cannot-have-a-label.md
@@ -9,7 +9,7 @@ Swift's syntax for trailing closures and multiple trailing closures are fantasti
Especially in such an otherwise-expressive language, this restriction is a bit jarring, and has real impacts on API design.
-### Method Pairs
+## Method Pairs
As a simple example, I think it's helpful to include method pairs like these:
@@ -50,7 +50,7 @@ var activeComponents: Set {
Given the need to use labels to disambiguate between the two methods, we wind up with `@autoclosure` being the best fit for this API (instead of just using closures).
-### Fused Functional Chains
+## Fused Functional Chains
As another example, for performance reason I often create "fused" versions of common functional chains: a fused "map, filter", a fused "filter, map", and so on.
@@ -97,10 +97,10 @@ let premiumContactInfo = orders.mapFilterMap
// feels clunky no matter how you finesse the formatting
let premiumContactInfo = orders.mapFilterMap
{ $0.customer }
- filter: { $0.isPremium }
+ filter: { $0.isPremium }
map: { $0.contactInfo }
```
-### Is There Hope?
+## Is There Hope?
Sadly, no: this capability has already been discussed-and-decided against, [as discussed a bit here](https://forums.swift.org/t/can-first-trailing-closure-be-named/69793/8).
diff --git a/src/content/briefs/testing/decision-execution-pattern.md b/src/content/briefs/testing/decision-execution-pattern.md
index bdbe6a2..46c8a4d 100644
--- a/src/content/briefs/testing/decision-execution-pattern.md
+++ b/src/content/briefs/testing/decision-execution-pattern.md
@@ -17,12 +17,4 @@ TODO: provide a *motivated*, *concrete* example.
*Postscript:* another way to interpret this pattern is as an informal, private, delegate-like design pattern:
-
-
-
-
-
-
-
-
The code in the "decision" phase should *generally* be structured as a pure function that receives all relevant information via parameters and returns a a function that returns a data item, e.g.:
diff --git a/src/content/projects/agentic-navigation-guide/index.md b/src/content/projects/agentic-navigation-guide/index.md
index 7f00498..8d6e3dd 100644
--- a/src/content/projects/agentic-navigation-guide/index.md
+++ b/src/content/projects/agentic-navigation-guide/index.md
@@ -66,19 +66,19 @@ For the *initial* implementation, I used a specification-driven workflow:
2. In *plan mode*, I had Opus generate a high-level roadmap with distinct *phases* (and iterated a bit until it was satisfactory)
3. I asked Claude to implement "phase 1" (and just "phase 1")
4. I had Claude write a `ContinuingMission.md` file that:
- - described the work done so far
- - described the work remaining
- - described the immediate "next steps" for the next session
-4. I then entered a loop like this:
- - start a fresh session
- - have Claude copy the `ContinuingMission.md` file into a `missions/` folder in the repo (and rename with a timestamp, to make it unique)
- - have Claude read the `ContinuingMission.md` file and take on the next task
- - review the results, offer feedback, and keep Claude iterating until he finished the task
- - have Claude *rewrite* `ContinuingMission.md` to once again:
- - describe the work done so far
- - describe the work remaining
- - describe the immediate "next steps" for the next session
-5. I kept repeating that loop until the initial pass on the project was complete
+ - described the work done so far
+ - described the work remaining
+ - described the immediate "next steps" for the next session
+5. I then entered a loop like this:
+ - start a fresh session
+ - have Claude copy the `ContinuingMission.md` file into a `missions/` folder in the repo (and rename with a timestamp, to make it unique)
+ - have Claude read the `ContinuingMission.md` file and take on the next task
+ - review the results, offer feedback, and keep Claude iterating until he finished the task
+ - have Claude *rewrite* `ContinuingMission.md` to once again:
+ - describe the work done so far
+ - describe the work remaining
+ - describe the immediate "next steps" for the next session
+6. I kept repeating that loop until the initial pass on the project was complete
Since this was my first pure vibe-coding experiment, I iteratively improved my workflow as I went:
diff --git a/src/content/projects/hdxl-xctest-retrofit/index.md b/src/content/projects/hdxl-xctest-retrofit/index.md
index f91c648..ae62e8d 100644
--- a/src/content/projects/hdxl-xctest-retrofit/index.md
+++ b/src/content/projects/hdxl-xctest-retrofit/index.md
@@ -11,7 +11,7 @@ repoURL: "https://github.com/plx/hdxl-xctest-retrofit/"
1. migrate from `XCTestCase` subclasses to `@Suite` structs
2. apply `@Test` annotation to test functions[^2]
-2. prepend `#` to `XCTAssert*` calls
+3. prepend `#` to `XCTAssert*` calls
[^1]: The primary gaps are around expectations, expected failures, and attachments—IMHO those don't map cleanly to Swift Testing's APIs, so they're currently unsupported.
diff --git a/src/layouts/PageLayout.astro b/src/layouts/PageLayout.astro
index bb8017c..a46eecc 100644
--- a/src/layouts/PageLayout.astro
+++ b/src/layouts/PageLayout.astro
@@ -14,12 +14,14 @@ type Props = {
const { title, description, ogData } = Astro.props;
const plainTitle = stripMarkdown(title);
+// Don't add site name suffix if the title is already the site name (home page)
+const pageTitle = plainTitle === SITE.NAME ? plainTitle : `${plainTitle} | ${SITE.NAME}`;
---
-
+
diff --git a/src/pages/about.astro b/src/pages/about.astro
new file mode 100644
index 0000000..1c9f351
--- /dev/null
+++ b/src/pages/about.astro
@@ -0,0 +1,69 @@
+---
+import PageLayout from "@layouts/PageLayout.astro";
+import Container from "@components/Container.astro";
+import Link from "@components/Link.astro";
+import { SITE, SOCIALS } from "@consts";
+import { getHomeOGData } from "@lib/opengraph";
+
+const ogData = getHomeOGData(
+ Astro.url.toString(),
+ Astro.site?.toString() || ""
+);
+---
+
+
+
+
+
+ About
+
+
+
+
+
About Dispatches
+
+
+ Dispatches is where I publish technical writing on topics of personal interest.
+ During this initial phase, much of the content will be "refurbished-and-expanded": older
+ personal notes or explanations, updated and expanded where necessary.
+
+
+ Expect a focus on Swift, lower-level iOS work, and—as of late—agentic coding assistants.
+
+
+
+
+
+
About Me
+
+
+ I'm a software engineer with extensive experience in iOS development, Swift programming,
+ and system-level engineering. I've spent years working on complex iOS applications and
+ have developed a deep appreciation for elegant APIs, robust type systems, and accessible design.
+
+
+ More recently, I've been exploring the intersection of human and AI-assisted development,
+ particularly through tools like Claude Code and other agentic coding assistants.
+
+
See all posts
@@ -70,9 +71,9 @@ const ogData = getHomeOGData(
-
+
Recent briefs
-
+
See all briefs
@@ -88,9 +89,9 @@ const ogData = getHomeOGData(
-
+
Recent projects
-
+
See all projects
@@ -105,9 +106,9 @@ const ogData = getHomeOGData(
-
+
Let's Connect
-
+
Here's how to get in touch:
diff --git a/src/pages/projects/index.astro b/src/pages/projects/index.astro
index 4291e14..47852f7 100644
--- a/src/pages/projects/index.astro
+++ b/src/pages/projects/index.astro
@@ -24,9 +24,9 @@ const ogData = getListOGData(