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/.gitignore b/.gitignore
index 860981e..a44363d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,4 +26,9 @@ pnpm-debug.log*
# CSpell cache
.cspellcache
-/test-results
+
+# Playwright
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
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 5860a5e..9b9cdd5 100644
--- a/justfile
+++ b/justfile
@@ -33,6 +33,11 @@ clean:
install:
npm install
+# Setup: full project setup including dependencies and Playwright browsers
+setup:
+ npm install
+ npx playwright install
+
# Spellcheck: checks spelling in source files
spellcheck:
npm run spellcheck
@@ -53,10 +58,54 @@ 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
+# QA: runs all Playwright QA tests (full suite for CI)
+qa:
+ npm run qa
+
+# QA-quick: runs quick sample of tests for local development
+qa-quick:
+ npm run qa:quick
+
+# QA-full: runs complete test suite including all sitemap pages
+qa-full:
+ npm run qa:full
+
+# QA-comprehensive: runs only the comprehensive sitemap tests
+qa-comprehensive:
+ npm run qa:comprehensive
+
+# QA-core: runs only the core tests (not comprehensive)
+qa-core:
+ npm run qa:core
+
+# QA-headed: runs Playwright tests with visible browser
+qa-headed:
+ npm run qa:headed
+
+# QA-ui: opens Playwright UI for interactive testing
+qa-ui:
+ npm run qa:ui
+
+# QA-debug: runs Playwright tests in debug mode
+qa-debug:
+ npm run qa:debug
+
+# QA-report: shows Playwright test report
+qa-report:
+ npm run qa:report
+
+# QA-codegen: opens Playwright code generator
+qa-codegen:
+ npm run qa:codegen
+
# Learn-spelling: adds new words to cspell dictionary (comma-separated)
learn-spelling words:
- node scripts/learn-spelling.js {{words}}
\ No newline at end of file
+ node scripts/learn-spelling.js {{words}}
diff --git a/package-lock.json b/package-lock.json
index 1bcb4b3..d618d83 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -43,9 +43,12 @@
"@cspell/dict-npm": "^5.2.14",
"@cspell/dict-typescript": "^3.2.3",
"@playwright/test": "^1.54.2",
+ "@types/xml2js": "^0.4.14",
"cspell": "^9.2.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
- "prettier": "^3.6.2"
+ "markdownlint-cli2": "^0.19.1",
+ "prettier": "^3.6.2",
+ "xml2js": "^0.6.2"
}
},
"node_modules/@alloc/quick-lru": {
@@ -123,7 +126,8 @@
"version": "2.12.2",
"resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.12.2.tgz",
"integrity": "sha512-w2zfvhjNCkNMmMMOn5b0J8+OmUaBL1o40ipMvqcG6NRpdC+lKxmTi48DT8Xw0SzJ3AfmeFLB45zXZXtmbsjcgw==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@astrojs/internal-helpers": {
"version": "0.7.2",
@@ -351,6 +355,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz",
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -805,7 +810,8 @@
"resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.0.18.tgz",
"integrity": "sha512-EF77RqROHL+4LhMGW5NTeKqfUd/e4OOv6EDFQ/UQQiFyWuqkEKyEz0NDILxOFxWUEVdjT2GQ2cC7t12B6pESwg==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@cspell/dict-dart": {
"version": "2.3.1",
@@ -945,14 +951,16 @@
"resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.12.tgz",
"integrity": "sha512-JFffQ1dDVEyJq6tCDWv0r/RqkdSnV43P2F/3jJ9rwLgdsOIXwQbXrz6QDlvQLVvNSnORH9KjDtenFTGDyzfCaA==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@cspell/dict-html-symbol-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.4.tgz",
"integrity": "sha512-afea+0rGPDeOV9gdO06UW183Qg6wRhWVkgCFwiO3bDupAoyXRuvupbb5nUyqSTsLXIKL8u8uXQlJ9pkz07oVXw==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@cspell/dict-java": {
"version": "5.0.12",
@@ -1150,7 +1158,8 @@
"resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.2.3.tgz",
"integrity": "sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@cspell/dict-vue": {
"version": "3.0.5",
@@ -3005,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",
@@ -3127,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",
@@ -3171,6 +3200,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
"integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -3180,6 +3210,7 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -3209,6 +3240,16 @@
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
+ "node_modules/@types/xml2js": {
+ "version": "0.4.14",
+ "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz",
+ "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@@ -3253,6 +3294,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz",
"integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.40.0",
"@typescript-eslint/types": "8.40.0",
@@ -3570,6 +3612,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3866,6 +3909,7 @@
"resolved": "https://registry.npmjs.org/astro/-/astro-5.13.2.tgz",
"integrity": "sha512-yjcXY0Ua3EwjpVd3GoUXa65HQ6qgmURBptA+M9GzE0oYvgfuyM7bIbH8IR/TWIbdefVUJR5b7nZ0oVnMytmyfQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@astrojs/compiler": "^2.12.2",
"@astrojs/internal-helpers": "0.7.2",
@@ -4385,6 +4429,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001735",
"electron-to-chromium": "^1.5.204",
@@ -5920,6 +5965,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz",
"integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -6997,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",
@@ -8136,6 +8203,7 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"license": "MIT",
+ "peer": true,
"bin": {
"jiti": "bin/jiti.js"
}
@@ -8147,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"
@@ -8222,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",
@@ -8297,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",
@@ -8403,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",
@@ -8413,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",
@@ -8742,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",
@@ -8820,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",
@@ -8941,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",
@@ -10192,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",
@@ -10318,6 +10569,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -10470,6 +10722,7 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -10546,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",
@@ -10593,6 +10856,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -10602,6 +10866,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -11101,6 +11366,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.4.tgz",
"integrity": "sha512-YbxoxvoqNg9zAmw4+vzh1FkGAiZRK+LhnSrbSrSXMdZYsRPDWoshcSd/pldKRO6lWzv/e9TiJAVQyirYIeSIPQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -11496,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",
@@ -11907,6 +12186,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -12288,6 +12568,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -12305,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",
@@ -12377,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",
@@ -12752,6 +13053,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -13385,6 +13687,30 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/xml2js": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
+ "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
"node_modules/xxhash-wasm": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz",
@@ -13411,6 +13737,7 @@
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"license": "ISC",
+ "peer": true,
"bin": {
"yaml": "bin.mjs"
},
@@ -13654,6 +13981,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/package.json b/package.json
index e44d20d..3bcffd1 100644
--- a/package.json
+++ b/package.json
@@ -11,13 +11,24 @@
"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!'"
+ "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",
+ "qa:quick": "SAMPLE_MODE=true playwright test --ignore-snapshots",
+ "qa:full": "playwright test --ignore-snapshots",
+ "qa:comprehensive": "playwright test comprehensive.spec.ts --ignore-snapshots",
+ "qa:core": "playwright test --ignore tests/comprehensive.spec.ts --ignore-snapshots",
+ "qa:headed": "playwright test --headed --ignore-snapshots",
+ "qa:ui": "playwright test --ui",
+ "qa:debug": "playwright test --debug",
+ "qa:report": "playwright show-report",
+ "qa:codegen": "playwright codegen http://localhost:4321"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
@@ -55,8 +66,11 @@
"@cspell/dict-npm": "^5.2.14",
"@cspell/dict-typescript": "^3.2.3",
"@playwright/test": "^1.54.2",
+ "@types/xml2js": "^0.4.14",
"cspell": "^9.2.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
- "prettier": "^3.6.2"
+ "markdownlint-cli2": "^0.19.1",
+ "prettier": "^3.6.2",
+ "xml2js": "^0.6.2"
}
}
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 0000000..c2ad546
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,81 @@
+import { defineConfig, devices } from "@playwright/test";
+
+/**
+ * Playwright configuration for QA testing
+ * See https://playwright.dev/docs/test-configuration
+ */
+export default defineConfig({
+ testDir: "./tests",
+
+ // Run tests in files in parallel
+ fullyParallel: true,
+
+ // Fail the build on CI if you accidentally left test.only in the source code
+ forbidOnly: !!process.env.CI,
+
+ // Retry on CI only
+ retries: process.env.CI ? 2 : 0,
+
+ // Opt out of parallel tests on CI
+ workers: process.env.CI ? 1 : undefined,
+
+ // Reporter to use
+ reporter: process.env.CI ? "github" : "list",
+
+ // Shared settings for all the projects below
+ use: {
+ // Base URL to use in actions like `await page.goto('/')`
+ baseURL: process.env.BASE_URL || "http://localhost:4321",
+
+ // Collect trace when retrying the failed test
+ trace: "on-first-retry",
+
+ // Screenshot on failure
+ screenshot: "only-on-failure",
+
+ // Launch options for better compatibility with sandboxed environments
+ launchOptions: {
+ args: [
+ "--disable-dev-shm-usage",
+ // Only disable sandbox in CI environments where necessary
+ ...(process.env.CI ? ["--no-sandbox", "--disable-setuid-sandbox"] : []),
+ ],
+ },
+ },
+
+ // Configure projects for major browsers
+ projects: [
+ {
+ name: "chromium",
+ use: { ...devices["Desktop Chrome"] },
+ },
+
+ {
+ name: "firefox",
+ use: { ...devices["Desktop Firefox"] },
+ },
+
+ {
+ name: "webkit",
+ use: { ...devices["Desktop Safari"] },
+ },
+
+ // Mobile viewports
+ {
+ name: "Mobile Chrome",
+ use: { ...devices["Pixel 5"] },
+ },
+ {
+ name: "Mobile Safari",
+ use: { ...devices["iPhone 12"] },
+ },
+ ],
+
+ // Run your local dev server before starting the tests
+ webServer: {
+ command: "npm run build && npm run preview",
+ url: "http://localhost:4321",
+ reuseExistingServer: !process.env.CI,
+ timeout: 120 * 1000,
+ },
+});
diff --git a/src/components/ContentCard.astro b/src/components/ContentCard.astro
index 9820efa..37b3bf9 100644
--- a/src/components/ContentCard.astro
+++ b/src/components/ContentCard.astro
@@ -1,6 +1,8 @@
---
import { renderInlineMarkdown } from "@lib/markdown";
+type HeadingLevel = 2 | 3 | 4 | 5 | 6;
+
type Props = {
titlePrefix?: string;
title: string;
@@ -8,9 +10,11 @@ type Props = {
link: string;
ariaLabel?: string;
maxLines?: number | "none";
+ headingLevel?: HeadingLevel;
}
-const { titlePrefix, title, subtitle, link, ariaLabel, maxLines = 2 } = Astro.props;
+const { titlePrefix, title, subtitle, link, ariaLabel, maxLines = 2, headingLevel = 3 } = Astro.props;
+const HeadingTag = `h${headingLevel}` as keyof HTMLElementTagNameMap;
const renderedTitlePrefix = titlePrefix ? renderInlineMarkdown(titlePrefix) : undefined;
const renderedTitle = renderInlineMarkdown(title);
const renderedSubtitle = renderInlineMarkdown(subtitle);
@@ -38,12 +42,12 @@ const accessibleLabel = ariaLabel || `${titlePrefix ? titlePrefix + ': ' : ''}${
aria-label={accessibleLabel}
class="relative group flex flex-nowrap py-3 px-4 pr-10 rounded-lg border border-black/15 dark:border-white/20 hover:bg-black/10 dark:hover:bg-white/10 hover:text-black dark:hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 dark:focus-visible:ring-blue-400 motion-safe:transition-colors motion-safe:duration-300 motion-safe:ease-in-out">
-
+
{renderedTitlePrefix && (
)}
-
+