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. +

+
+
+ +
+

Connect

+
+

Feel free to reach out:

+
    + {SOCIALS.map(SOCIAL => ( +
  • + + {SOCIAL.NAME} + +
  • + ))} +
+
+
+
+
+
+
diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index 56337d0..08aef6e 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -39,9 +39,9 @@ const ogData = getListOGData(
-
+

Blog -

+
{years.map(year => (
diff --git a/src/pages/briefs/index.astro b/src/pages/briefs/index.astro index 320d1b5..8b577aa 100644 --- a/src/pages/briefs/index.astro +++ b/src/pages/briefs/index.astro @@ -79,9 +79,9 @@ const ogData = getListOGData(
-
+

Briefs -

+
    { brief_categories.map(categoryKey => { diff --git a/src/pages/index.astro b/src/pages/index.astro index e4b3e5d..77e2ada 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -39,6 +39,7 @@ const ogData = getHomeOGData( +

    Dispatches

    @@ -52,9 +53,9 @@ const ogData = getHomeOGData(
    -
    +

    Latest posts -

    + 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(

    -
    +

    Projects -

    +
      { projects.map((project) => ( diff --git a/tests/navigation.spec.ts b/tests/navigation.spec.ts index 75157ae..4a395cf 100644 --- a/tests/navigation.spec.ts +++ b/tests/navigation.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from "@playwright/test"; test.describe("Navigation", () => { test("home page loads successfully", async ({ page }) => { await page.goto("/"); - await expect(page).toHaveTitle(/plx\.github\.io/i); + await expect(page).toHaveTitle(/Dispatches/i); }); test("can navigate to blog", async ({ page }) => {