diff --git a/.claude-toolkit/skills/more-secure-dependabot-config/SKILL.md b/.claude-toolkit/skills/more-secure-dependabot-config/SKILL.md new file mode 100644 index 00000000..a86e78ff --- /dev/null +++ b/.claude-toolkit/skills/more-secure-dependabot-config/SKILL.md @@ -0,0 +1,117 @@ +--- +name: dependabot-config +description: > + Generate or update Dependabot configuration files for projects. Use this skill + whenever the user asks to add, create, update, configure, or fix Dependabot for + a project — including phrases like "set up Dependabot", "add Dependabot config", + "update my dependabot.yml", "enable Dependabot updates", or "configure automated + dependency updates". Always apply this skill even if the user only mentions one + ecosystem (e.g. "add Dependabot for npm") — the canonical config covers all + required ecosystems. +--- + +# Dependabot Configuration Skill + +Produces the canonical `.github/dependabot.yml` configuration for any project. +Always emit the complete canonical config, never a partial one, unless the user +explicitly overrides a specific field after reviewing it. + +--- + +## Canonical Configuration + +The authoritative configuration to emit is: + +```yaml +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + cooldown: + default-days: 7 + semver-major-days: 7 + semver-minor-days: 3 + semver-patch-days: 2 + include: + - "*" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + cooldown: + default-days: 7 + include: + - "*" +``` + +--- + +## Cooldown Rationale + +Document this to the user when relevant: + +| Field | Value | Reasoning | +| ------------------- | ----- | ------------------------------------------------------ | +| `default-days` | 7 | Catch-all safety buffer for uncategorised updates | +| `semver-major-days` | 7 | Breaking changes warrant the longest review window | +| `semver-minor-days` | 3 | New features; moderate confidence, shorter delay | +| `semver-patch-days` | 2 | Bug/security fixes; high confidence, fast-track | +| `include: ["*"]` | — | Apply cooldown rules to all packages without exception | + +--- + +## Workflow + +### 1. Identify context + +- Determine whether a `.github/dependabot.yml` already exists in the project. + - **Exists**: read the file, explain any differences from the canonical config, + then overwrite it with the canonical config using a file tool. + - **Does not exist**: create `.github/dependabot.yml` with the canonical config + using a file tool. + +### 2. Check for non-npm ecosystems + +If the project uses additional package ecosystems (e.g. `pip`, `cargo`, `bundler`, +`docker`, `composer`), add an additional `updates` block per ecosystem using the +same `schedule` and `cooldown` values. Keep `npm` and `github-actions` blocks +unchanged. Use the references file for additional ecosystem identifiers if needed. + +### 3. Handle directory variations + +If the user's project has packages in subdirectories (e.g. a monorepo), ask which +directories need coverage and emit one block per directory per ecosystem, keeping +all other fields from the canonical config intact. + +### 4. Write the file + +Write the config directly to `.github/dependabot.yml` in the project root using a +file tool — do not present it as a fenced code block for the user to copy manually. +Ensure the `.github/` directory exists before writing. + +After writing, confirm the file path to the user and follow with a concise +explanation of what was configured and why — particularly the cooldown strategy — +without repeating every field verbatim. + +--- + +## Constraints + +- **Never omit the `cooldown` block** from any ecosystem entry. +- **Never change the canonical values** unless the user explicitly requests it and + provides a reason (e.g. a monorepo with a stricter release cadence). +- **Always include `github-actions`** as an ecosystem, even if the user only asked + about npm or another runtime ecosystem. +- **YAML formatting**: two-space indentation, string values quoted, list items with + `- ` prefix. Validate indentation before emitting — malformed YAML is a silent + failure in Dependabot. + +--- + +## Reference + +See `references/ecosystems.md` for the full list of Dependabot-supported +`package-ecosystem` identifiers and their directory conventions. diff --git a/.claude-toolkit/skills/more-secure-dependabot-config/references/ecosystem.md b/.claude-toolkit/skills/more-secure-dependabot-config/references/ecosystem.md new file mode 100644 index 00000000..3adabf76 --- /dev/null +++ b/.claude-toolkit/skills/more-secure-dependabot-config/references/ecosystem.md @@ -0,0 +1,35 @@ +# Dependabot Package Ecosystem Identifiers + +Reference: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem + +| Identifier | Ecosystem | Manifest file(s) | +| ---------------- | -------------------------- | ------------------------------------ | +| `npm` | npm / Yarn / pnpm | `package.json` | +| `pip` | pip / Poetry / pip-compile | `requirements.txt`, `pyproject.toml` | +| `cargo` | Rust / Cargo | `Cargo.toml` | +| `bundler` | Ruby / Bundler | `Gemfile` | +| `composer` | PHP / Composer | `composer.json` | +| `docker` | Docker | `Dockerfile` | +| `gradle` | Java / Gradle | `build.gradle`, `build.gradle.kts` | +| `maven` | Java / Maven | `pom.xml` | +| `gomod` | Go modules | `go.mod` | +| `nuget` | .NET / NuGet | `*.csproj`, `packages.config` | +| `github-actions` | GitHub Actions workflows | `.github/workflows/*.yml` | +| `terraform` | Terraform | `*.tf` | +| `hex` | Elixir / Hex | `mix.exs` | +| `elm` | Elm | `elm.json` | +| `pub` | Dart / Flutter | `pubspec.yaml` | +| `swift` | Swift Package Manager | `Package.swift` | + +## Directory conventions + +- Single-package project: `directory: "/"` +- Monorepo with packages in subdirectories: one block per directory, e.g. + `directory: "/packages/ui"`, `directory: "/packages/api"` +- Docker images referenced in a subdirectory: `directory: "/docker"` + +## Notes + +- `github-actions` scans `.github/workflows/` regardless of the `directory` value; + `"/"` is the correct and conventional value. +- Multiple ecosystems in the same directory each require their own `updates` block. diff --git a/.claude-toolkit/skills/npm-publishing-best-practices/SKILL.md b/.claude-toolkit/skills/npm-publishing-best-practices/SKILL.md new file mode 100644 index 00000000..9778561c --- /dev/null +++ b/.claude-toolkit/skills/npm-publishing-best-practices/SKILL.md @@ -0,0 +1,316 @@ +--- +name: npm-package-publishing +description: > + Apply best practices when publishing npm packages, including secure CI/CD workflows, trusted + publishing via OIDC, GitHub repository hardening, and supply-chain attack prevention. Use this + skill whenever the user asks about publishing an npm package, setting up a publish workflow, + configuring GitHub Actions for release automation, managing npm tokens or secrets, setting up + changesets, or auditing an existing publishing pipeline for security. Also trigger when the user + mentions publint, OIDC trusted publishing, release automation, or package versioning workflows. +--- + +# npm Package Publishing — Best Practices + +Based on the [e18e publishing guide](https://e18e.dev/docs/publishing.html). Reference it for +the canonical source; this skill distils the actionable steps. + +> **Package manager note.** All examples in this skill use `npm` to match the e18e source +> material, but nothing here is npm-specific. Always use whichever package manager the project +> already uses — `pnpm`, `yarn`, `bun`, etc. Adapt commands accordingly: +> +> | npm | pnpm | yarn | +> | --------------------------------- | ------------------------------------------------- | ------------------------------------------------- | +> | `npm ci --ignore-scripts` | `pnpm install --frozen-lockfile --ignore-scripts` | `yarn install --frozen-lockfile --ignore-scripts` | +> | `npm i -g npm` | `pnpm add -g pnpm` | `yarn set version stable` | +> | `ignore-scripts=true` in `.npmrc` | `ignore-scripts=true` in `.npmrc` | `enableScripts: false` in `.yarnrc.yml` | +> +> Detect the project's package manager by checking for a lockfile (`pnpm-lock.yaml`, +> `yarn.lock`, `bun.lockb`) or a `packageManager` field in `package.json` before +> generating any commands or workflow steps. + +--- + +## 1 · Prerequisites + +### 1.1 · Enforce 2FA everywhere + +| Account | Location | Recommended method | +| ------- | -------------------------------------- | ---------------------------------------------------------------------------- | +| npm | Account → Security | Security key (YubiKey, Touch ID, Windows Hello); fallback: authenticator app | +| GitHub | Settings → Password and authentication | Same priority order | + +Use a password manager with generated passwords for both accounts. + +### 1.2 · Harden GitHub Actions settings + +`Settings → Actions → General`: + +- ✅ Require actions to be pinned to a **full-length commit SHA** +- ✅ Require approval for first-time contributors +- ✅ Set default workflow permissions to **"Read repository contents and packages"** + +> If the repository belongs to an organisation, apply these settings at org level for consistency +> across all repositories. + +### 1.3 · Configure branch protection + +`Settings → Branches` → create a ruleset for `main`: + +- ✅ Require a pull request before merging +- ✅ Require at least 1 approval +- ✅ Dismiss stale approvals when new commits are pushed +- ✅ Require approval of the most recent reviewable push + +### 1.4 · Remove legacy npm tokens + +`Settings → Secrets & Variables → Actions`: remove any stored npm tokens. OIDC trusted publishing +replaces them entirely — no long-lived secrets needed in the repository. + +--- + +## 2 · Trusted Publishing (OIDC) + +Trusted publishing means GitHub Actions authenticates directly with npm via OIDC — no npm token +ever touches the repository. + +### 2.1 · Configure on npmjs.com + +1. Open your package page → **Settings** tab → **Trusted Publishing** section. +2. Add a trusted publisher: + - **Organisation / user**: your GitHub org or username + - **Repository**: the repository name + - **Workflow filename**: e.g. `publish.yml` +3. Check **"Require two-factor authentication and disallow tokens"** — this forces manual publishes + to use 2FA as well. + +> For bulk configuration across many packages, use +> [`open-packages-on-npm`](https://github.com/antfu/open-packages-on-npm) to open each package +> in a new tab, then the +> [npm-trusted-publisher userscript](https://github.com/sxzz/userscripts/blob/main/src/npm-trusted-publisher.md) +> to configure them rapidly. + +### 2.2 · npm CLI version requirement + +The publish step **must** use npm CLI ≥ 11.5.1 for automatic OIDC trusted publishing. +Node.js 24 bundles npm 11.5.1; with older CI images, add a step before publishing: + +```yaml +- run: npm i -g npm +``` + +--- + +## 3 · Standard Publish Workflow + +Use the [e18e setup-publish template](https://github.com/e18e/setup-publish/blob/main/templates/default.yml) +as your base, or scaffold it with: + +```bash +npx @e18e/setup-publish +``` + +### 3.1 · Job structure + +The workflow **must** separate build from publish. This ensures publish permissions (the OIDC +token) are never exposed to build-time code. + +``` +test → build → publish +``` + +### 3.2 · Non-negotiable workflow constraints + +| Constraint | Why | +| ------------------------------------------------------ | ----------------------------------------------------------- | +| All actions pinned to full-length commit SHA | Prevents supply-chain attacks via action updates | +| `npm ci --ignore-scripts` (or `--ignore-scripts` flag) | Prevents malicious lifecycle scripts running during install | +| Build and publish in **separate jobs** | Isolates publish permissions from arbitrary build code | + +### 3.3 · Suppress lifecycle scripts project-wide + +Add to `.npmrc` in the repository: + +``` +ignore-scripts=true +``` + +Also apply globally on developer machines: + +```bash +npm config set -g ignore-scripts true +``` + +### 3.4 · Creating a release + +```bash +git tag v1.0.0 +git push origin v1.0.0 +``` + +Then in GitHub UI: **Releases → Draft a new release** → choose the tag → **Generate release notes** +→ **Publish release**. This triggers the workflow. + +--- + +## 4 · Alternative Workflow Strategies + +### 4.1 · Changesets (recommended for teams) + +Use the [changesets template](https://github.com/e18e/setup-publish/blob/main/templates/changesets.yml). + +- Merged PRs automatically update a release pull request +- Changelog is generated by changesets and included in the release PR +- Releasing = merging the generated release PR — no manual tagging + +### 4.2 · changelogithub (changelog from commit messages) + +Use the [changelogithub template](https://github.com/e18e/setup-publish/blob/main/templates/changelogithub.yml). + +- Tags are still pushed manually +- GitHub release + changelog are created automatically on tag push +- Package is published on tag push + +--- + +## 5 · Ongoing Maintenance + +### 5.1 · Keep dependencies updated + +Set up **Dependabot** or **Renovate** to receive automated PRs for dependency updates, +addressing security vulnerabilities promptly. + +### 5.2 · Keep GitHub Actions updated + +All actions must be pinned to a commit SHA (not a tag). To migrate existing workflows and keep +them current: + +```bash +npx actions-up +``` + +Run this periodically, or let Dependabot/Renovate manage action updates once SHAs are in place. + +### 5.3 · Lint workflows for vulnerabilities + +[`zizmor`](https://github.com/zizmorcore/zizmor) detects template injection vulnerabilities and +excessive permission scopes in GitHub workflow files: + +```bash +zizmor .github/workflows/publish.yml +``` + +Integrate this into CI or run it before merging workflow changes. + +### 5.4 · Validate package.json and exports + +[`publint`](https://publint.dev) checks for common publishing issues: missing files, incorrect +`exports` fields, wrong `main`/`module` paths, and more. + +```bash +npx publint +``` + +Review the [full list of publint rules](https://publint.dev/rules) to understand what it checks. +Run this locally before tagging a release. + +### 5.5 · Visualise dependency changes + +[`multiocular`](https://github.com/multiocular-com/multiocular) shows exactly what code changed +between dependency versions, helping catch unexpected changes or potential security issues. + +--- + +## 6 · Further Security Hardening + +### 6.1 · Use a GitHub environment (important for shared repos) + +Without a GitHub environment, **any account with write access can trigger a publish** by creating +a branch and modifying the release workflow — bypassing code review entirely. + +`Settings → Environments`: + +1. Create an environment named `publish`. +2. **Do not** allow administrator bypass of protection rules. +3. Limit deployment to explicit branch names only (e.g. `main`, `v1`). Do not use wildcards. + Remove stale branches promptly. +4. Update the publish job in `publish.yml`: + +```yaml +jobs: + publish: + environment: publish +``` + +5. Update the npm trusted publisher settings on `npmjs.com` to include the environment name. + +> Optionally configure the `publish` environment to require **manual approval** before the job +> proceeds — providing a human gate even if malicious code reaches a release branch. + +### 6.2 · Consider hardware security keys _(optional)_ + +Physical security keys (YubiKey, etc.) are significantly more resistant to phishing and credential +theft than authenticator apps or SMS. They are worth recommending but should never be enforced — +not everyone has access to one, and a good authenticator app is a perfectly reasonable alternative. +Mention this as a suggestion, not a requirement. + +### 6.3 · Protect all long-lived branches and tags + +Apply branch protection rules not just to `main` but to all long-lived branches and to all tags. + +### 6.4 · Enable immutable releases + +Enable [GitHub immutable releases](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases) +to prevent modification or deletion of tags and releases after creation. + +--- + +## 7 · Sole Maintainer Considerations + +There is an [open feature request](https://github.com/orgs/community/discussions/174507) to +support 2FA for GitHub environment approvals. Until that lands, trusted publishing carries a risk +for solo maintainers: a leaked GitHub token could be used to publish through the trusted workflow +without a 2FA challenge. + +**Recommendation for sole maintainers**: consider publishing locally with `npm publish` and +2FA-protected npm access until environment-level 2FA approval is supported. Continue to follow +all other security recommendations in this document regardless. + +--- + +## Quick Reference Checklist + +Use this when setting up a new package or auditing an existing one. + +### Account security + +- [ ] npm 2FA enabled (security key preferred) +- [ ] GitHub 2FA enabled (security key preferred) +- [ ] Password manager in use + +### Repository settings + +- [ ] Actions pinned to full-length commit SHAs (enforced in settings) +- [ ] First-time contributor approval required +- [ ] Default workflow permissions set to read-only +- [ ] `main` branch protected (PR + review required) +- [ ] No npm tokens in repository secrets + +### Trusted publishing + +- [ ] OIDC trusted publisher configured on npmjs.com +- [ ] "Require 2FA, disallow tokens" enabled on npm +- [ ] Publish step uses Node.js ≥ 24.8.0 +- [ ] GitHub environment (`publish`) configured with branch restrictions + +### Workflow hygiene + +- [ ] Build and publish are separate jobs +- [ ] `npm ci --ignore-scripts` used in all install steps +- [ ] `ignore-scripts=true` in `.npmrc` +- [ ] `zizmor` passes on all workflow files + +### Package quality + +- [ ] `npx publint` passes with no errors +- [ ] Dependabot or Renovate configured +- [ ] `actions-up` run to migrate to SHA-pinned actions diff --git a/.claude/skills/more-secure-dependabot-config b/.claude/skills/more-secure-dependabot-config new file mode 120000 index 00000000..53fbdec7 --- /dev/null +++ b/.claude/skills/more-secure-dependabot-config @@ -0,0 +1 @@ +../../.claude-toolkit/skills/more-secure-dependabot-config \ No newline at end of file diff --git a/.claude/skills/npm-publishing-best-practices b/.claude/skills/npm-publishing-best-practices new file mode 120000 index 00000000..81841b50 --- /dev/null +++ b/.claude/skills/npm-publishing-best-practices @@ -0,0 +1 @@ +../../.claude-toolkit/skills/npm-publishing-best-practices \ No newline at end of file diff --git a/.claude/toolkit-manifest.json b/.claude/toolkit-manifest.json new file mode 100644 index 00000000..7b86bb38 --- /dev/null +++ b/.claude/toolkit-manifest.json @@ -0,0 +1,20 @@ +{ + "commands": {}, + "hooks": {}, + "skills": { + "more-secure-dependabot-config": { + "hash": "7eafb6e", + "installedAt": "2026-05-23", + "linkedTo": [ + ".claude/skills" + ] + }, + "npm-publishing-best-practices": { + "hash": "7f379cf", + "installedAt": "2026-05-23", + "linkedTo": [ + ".claude/skills" + ] + } + } +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d1f0d085..85dac81f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,19 @@ updates: directory: "/" schedule: interval: "daily" + cooldown: + default-days: 7 + semver-major-days: 7 + semver-minor-days: 3 + semver-patch-days: 2 + include: + - "*" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + cooldown: + default-days: 7 + include: + - "*" diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 804241a4..fb578006 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,35 +1,54 @@ name: Playwright Tests + on: push: branches: [main] pull_request: branches: [main] + +permissions: + contents: read + jobs: test: timeout-minutes: 15 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + # actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - name: Set up pnpm - uses: pnpm/action-setup@v4 + # pnpm/action-setup@v4 + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa - - uses: actions/setup-node@v4 + # actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.16.0 + cache: pnpm - name: Install dependencies - run: pnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile --ignore-scripts + + - name: Cache Playwright browsers + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} - name: Install Playwright Browsers - run: npx playwright install --with-deps + run: pnpm exec playwright install --with-deps chromium - name: Run Playwright tests - run: npx playwright test + run: pnpm run test:a11y - - uses: actions/upload-artifact@v4 + # actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 if: ${{ !cancelled() }} with: name: playwright-report - path: playwright-report/ + path: | + full-report.json + test-results/ + playwright-report/ retention-days: 7 diff --git a/.gitignore b/.gitignore index bca1dfa0..3de72f78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ # build output dist/ +public/og/ # generated types .astro/ # dependencies node_modules/ +.pnpm-store/ # logs npm-debug.log* @@ -22,5 +24,6 @@ pnpm-debug.log* # test artifacts playwright-report/ +reports/ test-results/ **/full-report.json diff --git a/AGENTS.md b/AGENTS.md index a3e8640b..6a3c5395 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,7 @@ This is an Astro-based personal blog and website with the following key architec ### Testing & Quality - **Playwright Tests**: Accessibility-focused testing with custom axe-core integration -- **Custom Reporter**: `tests/reporter/axe-aggregate-reporter.ts` for accessibility test results +- **Accessibility Reporter**: `@schalkneethling/axe-aggregate-reporter` aggregates Playwright axe results - **Test Configuration**: Only runs on Chromium, focuses on a11y compliance ### Key Patterns diff --git a/CLAUDE.md b/CLAUDE.md index c6cb542b..24603b1f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,7 +46,7 @@ This is an Astro-based personal blog and website with the following key architec ### Testing & Quality - **Playwright Tests**: Accessibility-focused testing with custom axe-core integration -- **Custom Reporter**: `tests/reporter/axe-aggregate-reporter.ts` for accessibility test results +- **Accessibility Reporter**: `@schalkneethling/axe-aggregate-reporter` aggregates Playwright axe results - **Test Configuration**: Only runs on Chromium, focuses on a11y compliance ### Key Patterns diff --git a/README.md b/README.md index ac921d9a..0de19f69 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,41 @@ All commands are run from the root of the project, from a terminal: | Command | Action | | :------------------------ | :----------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:4321` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | -| `npm run astro -- --help` | Get help using the Astro CLI | +| `pnpm install` | Installs dependencies | +| `pnpm dev` | Starts local dev server at `localhost:4321` | +| `pnpm run generate:og` | Generates per-post Open Graph card images | +| `pnpm run build` | Generates Open Graph cards and builds to `dist/` | +| `pnpm preview` | Preview your build locally, before deploying | +| `pnpm run typecheck` | Typecheck the project with `astro check` | +| `pnpm run test:a11y` | Run Playwright axe accessibility tests | +| `pnpm run a11y:viewer` | Open the generated axe aggregate report viewer | +| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` | +| `pnpm astro -- --help` | Get help using the Astro CLI | + +## Open Graph cards + +Blog post social cards are generated at build time with +[`@schalkneethling/opengraph-cards-maker`](https://github.com/schalkneethling/opengraph-cards-maker). + +- Generator script: [`scripts/generate-og-cards.mjs`](scripts/generate-og-cards.mjs) +- Card background artwork: [`src/assets/open-graph/post-card-template.png`](src/assets/open-graph/post-card-template.png) +- Generated source output: `public/og/posts/` +- Built output: `dist/og/posts/` +- Metadata wiring: + - [`src/pages/posts/[...slug].astro`](src/pages/posts/[...slug].astro) + - [`src/layouts/MarkdownPostLayout.astro`](src/layouts/MarkdownPostLayout.astro) + - [`src/layouts/BaseLayout.astro`](src/layouts/BaseLayout.astro) + +`public/og/` is ignored because the cards are reproducible build output. Run +`pnpm run generate:og` after changing post frontmatter, the generator script, or +the card template. The full `pnpm run build` command runs this automatically. + +If the card design needs tuning, the current blog-specific layout is the +`image-panel` layout in +[`@schalkneethling/opengraph-cards-maker`](https://github.com/schalkneethling/opengraph-cards-maker/blob/main/docs/data-model.md#layout-image-panel). +The generator uses `background.src` for the template image and +`contentAlign: "align-end"` so the post text sits on the right while the +background graphic remains on the left. ## 👀 Want to learn more? diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 00000000..42d65ff7 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,6 @@ +[build] + command = "pnpm run setup:playwright && pnpm run build" + publish = "dist" + +[build.environment] + PLAYWRIGHT_BROWSERS_PATH = "0" diff --git a/package.json b/package.json index bbc1e26a..18990697 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,21 @@ "name": "schalkneethling.com", "type": "module", "version": "0.0.1", + "engines": { + "node": ">=24" + }, "scripts": { "dev": "astro dev", "start": "astro dev", - "build": "astro build", + "setup:playwright": "pnpm exec playwright install chromium", + "build": "pnpm run generate:og && astro build", + "generate:og": "node scripts/generate-og-cards.mjs", "preview": "astro preview", "astro": "astro", + "typecheck": "astro check", + "test:a11y": "playwright test", + "a11y:viewer": "axe-aggregate-viewer ./full-report.json", + "a11y:viewer:standalone": "axe-aggregate-viewer ./full-report.json --standalone", "test:unit": "vitest run" }, "dependencies": { @@ -17,13 +26,14 @@ "@schalkneethling/masonry-gridlanes-wc": "^0.1.0", "astro": "^6.3.1", "astro-embed": "^0.13.0", - "sharp": "^0.34.5", "typescript": "^6.0.3", "varlock": "^1.1.0" }, "devDependencies": { "@axe-core/playwright": "^4.11.3", - "@playwright/test": "^1.59.1", + "@playwright/test": "^1.60.0", + "@schalkneethling/axe-aggregate-reporter": "0.6.0", + "@schalkneethling/opengraph-cards-maker": "^0.1.0", "@varlock/1password-plugin": "^1.1.0", "axe-core": "^4.11.4", "vitest": "^4.1.5" diff --git a/playwright.config.ts b/playwright.config.ts index f54ed46a..3a1cb343 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -14,6 +14,7 @@ import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ outputDir: "./test-results", testDir: "./tests", + testMatch: "**/*.spec.ts", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ @@ -23,11 +24,11 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: "./tests/reporter/axe-aggregate-reporter.ts", + reporter: [["list"], ["@schalkneethling/axe-aggregate-reporter/reporter"]], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://localhost:3000', + baseURL: "http://localhost:4321", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", @@ -72,9 +73,9 @@ export default defineConfig({ ], /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://localhost:3000', - // reuseExistingServer: !process.env.CI, - // }, + webServer: { + command: "pnpm run dev -- --host 127.0.0.1", + url: "http://localhost:4321", + reuseExistingServer: !process.env.CI, + }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37781c12..7a12a6e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 0.9.9(prettier@3.8.3)(typescript@6.0.3) '@astrojs/mdx': specifier: ^5.0.3 - version: 5.0.3(astro@6.3.1(@types/node@25.0.3)(rollup@4.60.3)(yaml@2.8.4)) + version: 5.0.3(astro@6.3.1(lightningcss@1.32.0)(rollup@4.60.3)(yaml@2.8.4)) '@astrojs/rss': specifier: ^4.0.18 version: 4.0.18 @@ -22,13 +22,10 @@ importers: version: 0.1.0 astro: specifier: ^6.3.1 - version: 6.3.1(@types/node@25.0.3)(rollup@4.60.3)(yaml@2.8.4) + version: 6.3.1(lightningcss@1.32.0)(rollup@4.60.3)(yaml@2.8.4) astro-embed: specifier: ^0.13.0 - version: 0.13.0(astro@6.3.1(@types/node@25.0.3)(rollup@4.60.3)(yaml@2.8.4)) - sharp: - specifier: ^0.34.5 - version: 0.34.5 + version: 0.13.0(astro@6.3.1(lightningcss@1.32.0)(rollup@4.60.3)(yaml@2.8.4)) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -38,10 +35,16 @@ importers: devDependencies: '@axe-core/playwright': specifier: ^4.11.3 - version: 4.11.3(playwright-core@1.59.1) + version: 4.11.3(playwright-core@1.60.0) '@playwright/test': - specifier: ^1.59.1 - version: 1.59.1 + specifier: ^1.60.0 + version: 1.60.0 + '@schalkneethling/axe-aggregate-reporter': + specifier: 0.6.0 + version: 0.6.0 + '@schalkneethling/opengraph-cards-maker': + specifier: ^0.1.0 + version: 0.1.0(esbuild@0.27.7) '@varlock/1password-plugin': specifier: ^1.1.0 version: 1.1.0(varlock@1.1.0) @@ -50,7 +53,7 @@ importers: version: 4.11.4 vitest: specifier: ^4.1.5 - version: 4.1.5(@types/node@25.0.3)(vite@7.3.3(@types/node@25.0.3)(yaml@2.8.4)) + version: 4.1.5(vite@8.0.14(esbuild@0.27.7)(yaml@2.8.4)) packages: @@ -228,9 +231,18 @@ packages: '@emmetio/stream-reader@2.2.0': resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.7.1': resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.27.7': resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} @@ -546,22 +558,129 @@ packages: '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@nodable/entities@2.1.0': resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@oxc-project/types@0.132.0': + resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} + '@parse5/tools@0.7.0': resolution: {integrity: sha512-JDvrGhc8kYBq7/SM4obJkpgwWo6pRjF/fo9CCaiJyVOkDf203Ciq2UF6TjzCFXKs7Q/zS2sS4deyBx0XzRvh9Q==} peerDependencies: parse5: 7.x || 8.x - '@playwright/test@1.59.1': - resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} + '@playwright/test@1.60.0': + resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} engines: {node: '>=18'} hasBin: true + '@rolldown/binding-android-arm64@1.0.2': + resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.2': + resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.2': + resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.2': + resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.2': + resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.2': + resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.2': + resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.2': + resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.2': + resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.2': + resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.2': + resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.2': + resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.2': + resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -709,9 +828,19 @@ packages: cpu: [x64] os: [win32] + '@schalkneethling/axe-aggregate-reporter@0.6.0': + resolution: {integrity: sha512-uo3RtNZoHe4I2U7vpCe+Y9oX+OXcViZT80kacDXOANPI8D8LZdI++YZHoID36baHpT38seziW6xJnYlhAeKrKw==} + engines: {node: '>=24.8.0'} + hasBin: true + '@schalkneethling/masonry-gridlanes-wc@0.1.0': resolution: {integrity: sha512-E5OZF8DORpsqZ7RWNxmnh0qIF/hgjsZOW5S6FkJuoem2J1onItVbpCWHboFlklu1RauNH3WMVUgRZCDkLqebvw==} + '@schalkneethling/opengraph-cards-maker@0.1.0': + resolution: {integrity: sha512-s4wAjabN1bpITaZz2JcTUPZ5rlOHU1Hn5ceq0MP93vR9WIigHvWs5XUzTDBtChLSPg5sEbIsGwILXaYbUuMTlg==} + engines: {node: '>=24'} + hasBin: true + '@shikijs/core@4.0.2': resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==} engines: {node: '>=20'} @@ -746,6 +875,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -779,9 +911,6 @@ packages: '@types/nlcst@2.0.3': resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} - '@types/node@25.0.3': - resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} - '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1335,6 +1464,80 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lite-youtube-embed@0.3.4: resolution: {integrity: sha512-aXgxpwK7AIW58GEbRzA8EYaY4LWvF3FKak6B9OtSJmuNyLhX2ouD4cMTxz/yR5HFInhknaYd2jLWOTRTvT8oAw==} @@ -1621,13 +1824,13 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - playwright-core@1.59.1: - resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + playwright-core@1.60.0: + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} engines: {node: '>=18'} hasBin: true - playwright@1.59.1: - resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} + playwright@1.60.0: + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} engines: {node: '>=18'} hasBin: true @@ -1635,6 +1838,10 @@ packages: resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + prettier@3.8.3: resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} engines: {node: '>=14'} @@ -1744,6 +1951,11 @@ packages: retext@9.0.0: resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + rolldown@1.0.2: + resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup@4.60.3: resolution: {integrity: sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -1878,9 +2090,6 @@ packages: uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - unicode-segmenter@0.14.5: resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} @@ -2039,6 +2248,49 @@ packages: yaml: optional: true + vite@8.0.14: + resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitefu@1.1.3: resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} peerDependencies: @@ -2264,7 +2516,7 @@ snapshots: dependencies: '@astro-community/astro-embed-utils': 0.2.0 - '@astro-community/astro-embed-integration@0.12.0(astro@6.3.1(@types/node@25.0.3)(rollup@4.60.3)(yaml@2.8.4))': + '@astro-community/astro-embed-integration@0.12.0(astro@6.3.1(lightningcss@1.32.0)(rollup@4.60.3)(yaml@2.8.4))': dependencies: '@astro-community/astro-embed-bluesky': 0.2.1 '@astro-community/astro-embed-gist': 0.1.0 @@ -2274,8 +2526,8 @@ snapshots: '@astro-community/astro-embed-vimeo': 0.3.12 '@astro-community/astro-embed-youtube': 0.5.10 '@types/unist': 3.0.3 - astro: 6.3.1(@types/node@25.0.3)(rollup@4.60.3)(yaml@2.8.4) - astro-auto-import: 0.5.1(astro@6.3.1(@types/node@25.0.3)(rollup@4.60.3)(yaml@2.8.4)) + astro: 6.3.1(lightningcss@1.32.0)(rollup@4.60.3)(yaml@2.8.4) + astro-auto-import: 0.5.1(astro@6.3.1(lightningcss@1.32.0)(rollup@4.60.3)(yaml@2.8.4)) unist-util-select: 5.1.0 '@astro-community/astro-embed-link-preview@0.3.1': @@ -2402,12 +2654,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/mdx@5.0.3(astro@6.3.1(@types/node@25.0.3)(rollup@4.60.3)(yaml@2.8.4))': + '@astrojs/mdx@5.0.3(astro@6.3.1(lightningcss@1.32.0)(rollup@4.60.3)(yaml@2.8.4))': dependencies: '@astrojs/markdown-remark': 7.1.0 '@mdx-js/mdx': 3.1.1 acorn: 8.16.0 - astro: 6.3.1(@types/node@25.0.3)(rollup@4.60.3)(yaml@2.8.4) + astro: 6.3.1(lightningcss@1.32.0)(rollup@4.60.3)(yaml@2.8.4) es-module-lexer: 2.0.0 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -2477,10 +2729,10 @@ snapshots: dependencies: unicode-segmenter: 0.14.5 - '@axe-core/playwright@4.11.3(playwright-core@1.59.1)': + '@axe-core/playwright@4.11.3(playwright-core@1.60.0)': dependencies: axe-core: 4.11.4 - playwright-core: 1.59.1 + playwright-core: 1.60.0 '@babel/helper-string-parser@7.27.1': {} @@ -2538,11 +2790,27 @@ snapshots: '@emmetio/stream-reader@2.2.0': {} + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.27.7': optional: true @@ -2621,7 +2889,8 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true - '@img/colour@1.0.0': {} + '@img/colour@1.0.0': + optional: true '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: @@ -2749,17 +3018,77 @@ snapshots: transitivePeerDependencies: - supports-color + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + '@nodable/entities@2.1.0': {} '@oslojs/encoding@1.1.0': {} + '@oxc-project/types@0.132.0': {} + '@parse5/tools@0.7.0(parse5@8.0.1)': dependencies: parse5: 8.0.1 - '@playwright/test@1.59.1': + '@playwright/test@1.60.0': dependencies: - playwright: 1.59.1 + playwright: 1.60.0 + + '@rolldown/binding-android-arm64@1.0.2': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.2': + optional: true + + '@rolldown/binding-darwin-x64@1.0.2': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.2': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.2': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.2': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.2': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.2': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.2': + optional: true + + '@rolldown/pluginutils@1.0.1': {} '@rollup/pluginutils@5.3.0(rollup@4.60.3)': dependencies: @@ -2844,10 +3173,30 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.3': optional: true + '@schalkneethling/axe-aggregate-reporter@0.6.0': {} + '@schalkneethling/masonry-gridlanes-wc@0.1.0': dependencies: '@chenglou/pretext': 0.0.4 + '@schalkneethling/opengraph-cards-maker@0.1.0(esbuild@0.27.7)': + dependencies: + playwright: 1.60.0 + vite: 8.0.14(esbuild@0.27.7)(yaml@2.8.4) + yaml: 2.8.4 + transitivePeerDependencies: + - '@types/node' + - '@vitejs/devtools' + - esbuild + - jiti + - less + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + '@shikijs/core@4.0.2': dependencies: '@shikijs/primitive': 4.0.2 @@ -2890,6 +3239,11 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -2925,11 +3279,6 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/node@25.0.3': - dependencies: - undici-types: 7.16.0 - optional: true - '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -2949,13 +3298,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.5(vite@7.3.3(@types/node@25.0.3)(yaml@2.8.4))': + '@vitest/mocker@4.1.5(vite@8.0.14(esbuild@0.27.7)(yaml@2.8.4))': dependencies: '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.3(@types/node@25.0.3)(yaml@2.8.4) + vite: 8.0.14(esbuild@0.27.7)(yaml@2.8.4) '@vitest/pretty-format@4.1.5': dependencies: @@ -3069,25 +3418,25 @@ snapshots: astring@1.9.0: {} - astro-auto-import@0.5.1(astro@6.3.1(@types/node@25.0.3)(rollup@4.60.3)(yaml@2.8.4)): + astro-auto-import@0.5.1(astro@6.3.1(lightningcss@1.32.0)(rollup@4.60.3)(yaml@2.8.4)): dependencies: acorn: 8.16.0 - astro: 6.3.1(@types/node@25.0.3)(rollup@4.60.3)(yaml@2.8.4) + astro: 6.3.1(lightningcss@1.32.0)(rollup@4.60.3)(yaml@2.8.4) - astro-embed@0.13.0(astro@6.3.1(@types/node@25.0.3)(rollup@4.60.3)(yaml@2.8.4)): + astro-embed@0.13.0(astro@6.3.1(lightningcss@1.32.0)(rollup@4.60.3)(yaml@2.8.4)): dependencies: '@astro-community/astro-embed-baseline-status': 0.2.2 '@astro-community/astro-embed-bluesky': 0.2.1 '@astro-community/astro-embed-gist': 0.1.0 - '@astro-community/astro-embed-integration': 0.12.0(astro@6.3.1(@types/node@25.0.3)(rollup@4.60.3)(yaml@2.8.4)) + '@astro-community/astro-embed-integration': 0.12.0(astro@6.3.1(lightningcss@1.32.0)(rollup@4.60.3)(yaml@2.8.4)) '@astro-community/astro-embed-link-preview': 0.3.1 '@astro-community/astro-embed-mastodon': 0.1.1 '@astro-community/astro-embed-twitter': 0.5.11 '@astro-community/astro-embed-vimeo': 0.3.12 '@astro-community/astro-embed-youtube': 0.5.10 - astro: 6.3.1(@types/node@25.0.3)(rollup@4.60.3)(yaml@2.8.4) + astro: 6.3.1(lightningcss@1.32.0)(rollup@4.60.3)(yaml@2.8.4) - astro@6.3.1(@types/node@25.0.3)(rollup@4.60.3)(yaml@2.8.4): + astro@6.3.1(lightningcss@1.32.0)(rollup@4.60.3)(yaml@2.8.4): dependencies: '@astrojs/compiler': 4.0.0 '@astrojs/internal-helpers': 0.9.0 @@ -3139,8 +3488,8 @@ snapshots: unist-util-visit: 5.1.0 unstorage: 1.17.5 vfile: 6.0.3 - vite: 7.3.3(@types/node@25.0.3)(yaml@2.8.4) - vitefu: 1.1.3(vite@7.3.3(@types/node@25.0.3)(yaml@2.8.4)) + vite: 7.3.3(lightningcss@1.32.0)(yaml@2.8.4) + vitefu: 1.1.3(vite@7.3.3(lightningcss@1.32.0)(yaml@2.8.4)) xxhash-wasm: 1.1.0 yargs-parser: 22.0.0 zod: 4.4.3 @@ -3662,6 +4011,55 @@ snapshots: kleur@4.1.5: {} + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lite-youtube-embed@0.3.4: {} longest-streak@3.1.0: {} @@ -4215,11 +4613,11 @@ snapshots: picomatch@4.0.4: {} - playwright-core@1.59.1: {} + playwright-core@1.60.0: {} - playwright@1.59.1: + playwright@1.60.0: dependencies: - playwright-core: 1.59.1 + playwright-core: 1.60.0 optionalDependencies: fsevents: 2.3.2 @@ -4229,6 +4627,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prettier@3.8.3: {} prismjs@1.30.0: {} @@ -4396,6 +4800,27 @@ snapshots: retext-stringify: 4.0.0 unified: 11.0.5 + rolldown@1.0.2: + dependencies: + '@oxc-project/types': 0.132.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.2 + '@rolldown/binding-darwin-arm64': 1.0.2 + '@rolldown/binding-darwin-x64': 1.0.2 + '@rolldown/binding-freebsd-x64': 1.0.2 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 + '@rolldown/binding-linux-arm64-gnu': 1.0.2 + '@rolldown/binding-linux-arm64-musl': 1.0.2 + '@rolldown/binding-linux-ppc64-gnu': 1.0.2 + '@rolldown/binding-linux-s390x-gnu': 1.0.2 + '@rolldown/binding-linux-x64-gnu': 1.0.2 + '@rolldown/binding-linux-x64-musl': 1.0.2 + '@rolldown/binding-openharmony-arm64': 1.0.2 + '@rolldown/binding-wasm32-wasi': 1.0.2 + '@rolldown/binding-win32-arm64-msvc': 1.0.2 + '@rolldown/binding-win32-x64-msvc': 1.0.2 + rollup@4.60.3: dependencies: '@types/estree': 1.0.8 @@ -4429,7 +4854,8 @@ snapshots: sax@1.6.0: {} - semver@7.7.3: {} + semver@7.7.3: + optional: true semver@7.7.4: {} @@ -4463,6 +4889,7 @@ snapshots: '@img/sharp-win32-arm64': 0.34.5 '@img/sharp-win32-ia32': 0.34.5 '@img/sharp-win32-x64': 0.34.5 + optional: true shiki@4.0.2: dependencies: @@ -4564,9 +4991,6 @@ snapshots: uncrypto@0.1.3: {} - undici-types@7.16.0: - optional: true - unicode-segmenter@0.14.5: {} unified@11.0.5: @@ -4667,7 +5091,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@7.3.3(@types/node@25.0.3)(yaml@2.8.4): + vite@7.3.3(lightningcss@1.32.0)(yaml@2.8.4): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) @@ -4676,18 +5100,30 @@ snapshots: rollup: 4.60.3 tinyglobby: 0.2.16 optionalDependencies: - '@types/node': 25.0.3 fsevents: 2.3.3 + lightningcss: 1.32.0 yaml: 2.8.4 - vitefu@1.1.3(vite@7.3.3(@types/node@25.0.3)(yaml@2.8.4)): + vite@8.0.14(esbuild@0.27.7)(yaml@2.8.4): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.2 + tinyglobby: 0.2.16 + optionalDependencies: + esbuild: 0.27.7 + fsevents: 2.3.3 + yaml: 2.8.4 + + vitefu@1.1.3(vite@7.3.3(lightningcss@1.32.0)(yaml@2.8.4)): optionalDependencies: - vite: 7.3.3(@types/node@25.0.3)(yaml@2.8.4) + vite: 7.3.3(lightningcss@1.32.0)(yaml@2.8.4) - vitest@4.1.5(@types/node@25.0.3)(vite@7.3.3(@types/node@25.0.3)(yaml@2.8.4)): + vitest@4.1.5(vite@8.0.14(esbuild@0.27.7)(yaml@2.8.4)): dependencies: '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(vite@7.3.3(@types/node@25.0.3)(yaml@2.8.4)) + '@vitest/mocker': 4.1.5(vite@8.0.14(esbuild@0.27.7)(yaml@2.8.4)) '@vitest/pretty-format': 4.1.5 '@vitest/runner': 4.1.5 '@vitest/snapshot': 4.1.5 @@ -4704,10 +5140,8 @@ snapshots: tinyexec: 1.1.1 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 7.3.3(@types/node@25.0.3)(yaml@2.8.4) + vite: 8.0.14(esbuild@0.27.7)(yaml@2.8.4) why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 25.0.3 transitivePeerDependencies: - msw diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c733e937..d5072d1c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,6 @@ +allowBuilds: + esbuild: true + sharp: true onlyBuiltDependencies: - '@google/genai' - esbuild diff --git a/scripts/generate-og-cards.mjs b/scripts/generate-og-cards.mjs new file mode 100644 index 00000000..672f0211 --- /dev/null +++ b/scripts/generate-og-cards.mjs @@ -0,0 +1,34 @@ +import path from "node:path"; +import { + createCardsFromContentFiles, + generateOpenGraphCards, +} from "@schalkneethling/opengraph-cards-maker"; + +const outputDir = path.resolve("public/og/posts"); +const background = path.resolve("src/assets/open-graph/post-card-template.png"); + +const cards = await createCardsFromContentFiles({ + contentDir: "src/content/posts", + extensions: [".md", ".mdx"], + eyebrow: "Scripting on Caffeine", + mapCard: (card) => ({ + ...card, + background: { + src: background, + }, + contentAlign: "align-end", + theme: { + accent: "#9D003D", + foreground: "#fff", + muted: "#fff", + }, + }), +}); + +await generateOpenGraphCards({ + outputDir, + cleanOutputDir: true, + cards, +}); + +console.log(`Generated ${cards.length} post Open Graph card(s).`); diff --git a/src/assets/open-graph/post-card-template.png b/src/assets/open-graph/post-card-template.png new file mode 100644 index 00000000..7ac7eb9b Binary files /dev/null and b/src/assets/open-graph/post-card-template.png differ diff --git a/src/content/posts/build-a-profile-page-html-css-part2-impl.mdx b/src/content/posts/build-a-profile-page-html-css-part2-impl.mdx index 1f16af61..83c0556b 100644 --- a/src/content/posts/build-a-profile-page-html-css-part2-impl.mdx +++ b/src/content/posts/build-a-profile-page-html-css-part2-impl.mdx @@ -1,5 +1,5 @@ --- -title: "Part Two: Building a Profile Page with HTML and CSS: Implementation" +title: "Building a Profile Page with HTML and CSS: Implementation" pubDate: 2025-01-07 description: "In this post we will start implementing the design by working through some of the issues, learning more about Git and GitHub, get set up for development, create our first visual component, and learn about performance, CSS, and modern image formats." author: "Schalk Neethling" diff --git a/src/content/posts/build-a-profile-page-html-css-part3-wrap-up.mdx b/src/content/posts/build-a-profile-page-html-css-part3-wrap-up.mdx index 88d3a53f..854cc28b 100644 --- a/src/content/posts/build-a-profile-page-html-css-part3-wrap-up.mdx +++ b/src/content/posts/build-a-profile-page-html-css-part3-wrap-up.mdx @@ -1,5 +1,5 @@ --- -title: "Part Three: Building a Profile Page with HTML and CSS: Typography And Getting Responsive" +title: "Building a Profile Page with HTML and CSS: Typography And Getting Responsive" pubDate: 2025-01-19 description: "Learn how to build a responsive profile page with modern typography, media queries, and CSS Grid. Part three of our hands-on web development tutorial series." author: "Schalk Neethling" diff --git a/src/content/posts/build-a-profile-page-html-css-part4-refactor-linktree.mdx b/src/content/posts/build-a-profile-page-html-css-part4-refactor-linktree.mdx index ca51fbbd..ffa0633e 100644 --- a/src/content/posts/build-a-profile-page-html-css-part4-refactor-linktree.mdx +++ b/src/content/posts/build-a-profile-page-html-css-part4-refactor-linktree.mdx @@ -1,5 +1,5 @@ --- -title: "Part Four: Building a Profile Page with HTML and CSS: Refactor, Custom Properties, LinkTree" +title: "Building a Profile Page with HTML and CSS: Refactor, Custom Properties, LinkTree" pubDate: 2025-01-24 description: "In part four of the series, we will do some refactoring of our CSS so that we take advantage of CSS custom properties. We will also complete the mobile layout by adding the LinkTree component to our page." author: "Schalk Neethling" diff --git a/src/content/posts/build-a-profile-page-html-css-part5-final-page.mdx b/src/content/posts/build-a-profile-page-html-css-part5-final-page.mdx index 7fd05981..f71e86f7 100644 --- a/src/content/posts/build-a-profile-page-html-css-part5-final-page.mdx +++ b/src/content/posts/build-a-profile-page-html-css-part5-final-page.mdx @@ -1,5 +1,5 @@ --- -title: "Part Five: Building a Profile Page with HTML and CSS: Responsive, Favicon, Open Graph" +title: "Building a Profile Page with HTML and CSS: Responsive, Favicon, Open Graph" pubDate: 2025-02-07 description: "In part five of the series, we will complete our layout, add a favicon, and set up Open Graph meta tags for social sharing." author: "Schalk Neethling" diff --git a/src/content/posts/build-a-profile-page-html-css-part6-dialog-form-netlify.mdx b/src/content/posts/build-a-profile-page-html-css-part6-dialog-form-netlify.mdx index d81f90e8..40133138 100644 --- a/src/content/posts/build-a-profile-page-html-css-part6-dialog-form-netlify.mdx +++ b/src/content/posts/build-a-profile-page-html-css-part6-dialog-form-netlify.mdx @@ -1,5 +1,5 @@ --- -title: "Part Six: Building a Profile Page with HTML and CSS: A form, A Dialog, and Netlify" +title: "Building a Profile Page with HTML and CSS: A form, A Dialog, and Netlify" pubDate: 2025-02-17 description: "In this part, we will add a form to our profile page, use a dialog to display our form, deploy our page using Netlify, and set up Netlify to handle form submissions." author: "Schalk Neethling" diff --git a/src/content/posts/build-a-profile-page-html-css-planning.mdx b/src/content/posts/build-a-profile-page-html-css-planning.mdx index a3ec8058..0757330e 100644 --- a/src/content/posts/build-a-profile-page-html-css-planning.mdx +++ b/src/content/posts/build-a-profile-page-html-css-planning.mdx @@ -1,5 +1,5 @@ --- -title: "Part One: Building a Profile Page with HTML and CSS: Planning" +title: "Building a Profile Page with HTML and CSS: Planning" pubDate: 2025-01-03 description: "In this part of the series we will break down the design into components using Atomic Design, set up our repository on GitHub, and create the issues we will need to work on." author: "Schalk Neethling" diff --git a/src/content/posts/cutting-across-the-what-and-the-how-what-building-a-polyfill-is-teaching-me-about-web-standards.md b/src/content/posts/cutting-across-the-what-and-the-how-what-building-a-polyfill-is-teaching-me-about-web-standards.md index fb10a26d..8eb18cbb 100644 --- a/src/content/posts/cutting-across-the-what-and-the-how-what-building-a-polyfill-is-teaching-me-about-web-standards.md +++ b/src/content/posts/cutting-across-the-what-and-the-how-what-building-a-polyfill-is-teaching-me-about-web-standards.md @@ -1,5 +1,5 @@ --- -title: "Cutting Across The What And The How: What Building A Polyfill Is Teaching Me About Web Standards" +title: "The What And The How: What Building A Polyfill Is Teaching Me About Web Standards" pubDate: 2026-03-26 description: "What happens when you try to implement two seemingly straightforward CSS features as a JavaScript polyfill? You discover that specifications are simple because they made hard decisions, and the complexity does not disappear; it just moves." author: "Schalk Neethling" diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index de3bb7d7..51d5fc0b 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -6,9 +6,13 @@ import Header from "../components/Header.astro"; import "../styles/minimalist/minimalist.css"; import "../styles/global.css"; -const { customCanonical, pageDescription, pageTitle } = Astro.props; +const { customCanonical, pageDescription, pageTitle, socialImage } = Astro.props; const currentPageURL = new URL(Astro.url.pathname, Astro.site); const canonical = customCanonical || currentPageURL; +const fallbackSocialImage = new URL("/social-graph.png", Astro.site); +const socialImageUrl = socialImage + ? new URL(socialImage, Astro.site) + : fallbackSocialImage; --- @@ -45,7 +49,7 @@ const canonical = customCanonical || currentPageURL; @@ -56,8 +60,10 @@ const canonical = customCanonical || currentPageURL; + + diff --git a/src/layouts/MarkdownPostLayout.astro b/src/layouts/MarkdownPostLayout.astro index 43267d7e..6cec6995 100644 --- a/src/layouts/MarkdownPostLayout.astro +++ b/src/layouts/MarkdownPostLayout.astro @@ -15,10 +15,11 @@ interface Props { tags: string[]; title: string; }; + socialImage?: string; sourceEditUrl: string; } -const { frontmatter, sourceEditUrl } = Astro.props as Props; +const { frontmatter, socialImage, sourceEditUrl } = Astro.props as Props; const tags = frontmatter.tags; --- @@ -26,6 +27,7 @@ const tags = frontmatter.tags; customCanonical={frontmatter.canonical} pageDescription={frontmatter.description} pageTitle={frontmatter.title} + socialImage={socialImage} >
diff --git a/src/pages/posts/[...slug].astro b/src/pages/posts/[...slug].astro index bfbfa122..d7ff42c3 100644 --- a/src/pages/posts/[...slug].astro +++ b/src/pages/posts/[...slug].astro @@ -26,8 +26,9 @@ const { post } = Astro.props; const { Content } = await render(post); const sourcePath = getPostSourcePath(post.filePath); const sourceEditUrl = `${REPO_EDIT_BASE_URL}${sourcePath}`; +const socialImage = `/og/posts/${post.id}.png`; --- - + diff --git a/src/styles/global.css b/src/styles/global.css index 4589a38a..8bf05720 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -72,6 +72,12 @@ tbody code { color: inherit; } +/* Improve Dracula comment token contrast in Shiki code blocks. */ +.astro-code.dracula span[style*="#6272A4"], +.astro-code.dracula span[style*="#6272a4"] { + color: #9aa6d1 !important; +} + /* Ensure all table cell content is visible */ tbody td, tbody td strong, diff --git a/tests/a11y.spec.ts b/tests/a11y.spec.ts index 76cd676d..89a4f12b 100644 --- a/tests/a11y.spec.ts +++ b/tests/a11y.spec.ts @@ -1,11 +1,9 @@ import fs from "node:fs/promises"; -import type { AxeResults, NodeResult, Result } from "axe-core"; import type { TestInfo } from "@playwright/test"; +import { formatFullReport } from "@schalkneethling/axe-aggregate-reporter"; import { test, expect } from "./fixtures/axe-test-fixture.js"; -import urls from "./urls.json" assert { type: "json" }; - -type Impact = "minor" | "moderate" | "serious" | "critical" | "unknown"; +import urls from "./urls.json" with { type: "json" }; type TestUrl = { title: string; @@ -17,67 +15,22 @@ for (const url of urls as TestUrl[]) { page, makeAxeBuilder, }, testInfo: TestInfo) => { - await page.goto(url.url, { waitUntil: "domcontentloaded" }); + await page.goto(url.url, { waitUntil: "load" }); + await page.waitForLoadState("networkidle"); const axeBuilder = makeAxeBuilder(); const results = await axeBuilder.analyze(); - if (results.violations.length) { - const file = testInfo.outputPath("axe.json"); - await fs.writeFile( - file, - JSON.stringify(formatViolations(results), null, 2), - ); - testInfo.attach("axe.json", { - path: file, - contentType: "application/json", - }); - } + const file = testInfo.outputPath("axe.json"); + await fs.writeFile( + file, + JSON.stringify(formatFullReport(results), null, 2), + ); + await testInfo.attach("axe.json", { + path: file, + contentType: "application/json", + }); expect(results.violations).toEqual([]); }); } - -const IMPACT_ORDER = { - critical: 0, - serious: 1, - moderate: 2, - minor: 3, - unknown: 4, -} as const; - -function extractSuccessCriteriaId(tags: string[]) { - // matches for example wcag241, wcag242, etc. - return tags.filter((tag) => /^wcag\d{3}$/.test(tag)); -} - -function formatNodes(nodes: NodeResult[], limit = 10) { - if (!nodes.length) { - return []; - } - - return nodes.slice(0, limit).map((node: NodeResult) => { - return { - summary: node.failureSummary ?? "No failure summary", - html: node.html, - impact: node.impact ?? "unknown", - }; - }); -} - -function formatViolations(results: AxeResults) { - const violations = results.violations.map((violation: Result) => ({ - id: violation.id, - successCriteriaId: extractSuccessCriteriaId(violation.tags ?? []), - description: violation.description, - help: violation.help, - helpUrl: violation.helpUrl, - impact: (violation.impact ?? "unknown") as Impact, - nodes: formatNodes(violation.nodes), - totalNodes: violation.nodes.length, - })); - - return violations.toSorted( - (a, b) => IMPACT_ORDER[a.impact] - IMPACT_ORDER[b.impact], - ); -} diff --git a/tests/fixtures/axe-test-fixture.ts b/tests/fixtures/axe-test-fixture.ts index 21dff3e8..e9634cb4 100644 --- a/tests/fixtures/axe-test-fixture.ts +++ b/tests/fixtures/axe-test-fixture.ts @@ -11,7 +11,9 @@ export const test = base.extend({ use: (r: () => AxeBuilder) => Promise, ) => { const makeAxeBuilder = () => - new AxeBuilder({ page }).withTags(["wcag22a", "wcag22aa"]); + new AxeBuilder({ page }) + .options({ reporter: "v2" }) + .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"]); await use(makeAxeBuilder); }, }); diff --git a/tests/reporter/axe-aggregate-reporter.ts b/tests/reporter/axe-aggregate-reporter.ts deleted file mode 100644 index b2c4d6f0..00000000 --- a/tests/reporter/axe-aggregate-reporter.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { - Reporter, - FullConfig, - TestCase, - TestResult, -} from "@playwright/test/reporter"; -import fs from "node:fs/promises"; -import path from "node:path"; - -type Aggregated = Array<{ - testId: string; - title: string; - status: TestResult["status"]; - axe?: unknown; -}>; - -class AxeAggregateReporter implements Reporter { - private outputDir: string; - private rows: Aggregated = []; - - onBegin(config: FullConfig) { - this.outputDir = config.rootDir; - console.info("\nRunning accessibility tests and aggregating results..."); - } - - async onTestEnd(test: TestCase, result: TestResult) { - const attachment = result.attachments.find( - (attachment: TestResult["attachments"][number]) => - attachment.name === "axe.json" && attachment.path, - ); - - if (!attachment || !attachment.path) { - return; - } - - let axe: unknown; - try { - const raw = await fs.readFile(attachment.path, "utf-8"); - axe = JSON.parse(raw); - } catch (error) { - throw new Error( - `Failed to load or parse axe.json attachment for test ${test.title}`, - { cause: error }, - ); - } - - this.rows.push({ - testId: test.id, - title: test.title, - status: result.status, - axe, - }); - - console.info(`\n${test.title} completed`); - } - - async onEnd() { - const outputFile = path.join(this.outputDir, "full-report.json"); - await fs.writeFile(outputFile, JSON.stringify(this.rows, null, 2)); - console.log(`\nWrote full report to ${outputFile}\n`); - } -} - -export default AxeAggregateReporter; diff --git a/tests/urls.json b/tests/urls.json index e902cbda..52e2cac8 100644 --- a/tests/urls.json +++ b/tests/urls.json @@ -1,34 +1,34 @@ [ { "title": "Homepage", - "url": "https://schalkneethling.com/" + "url": "/" }, { "title": "Now", - "url": "https://schalkneethling.com/now" + "url": "/now" }, { "title": "About", - "url": "https://schalkneethling.com/about" + "url": "/about" }, { "title": "Contact", - "url": "https://schalkneethling.com/contact" + "url": "/contact" }, { "title": "Blog", - "url": "https://schalkneethling.com/blog" + "url": "/blog" }, { "title": "Projects", - "url": "https://schalkneethling.com/projects" + "url": "/projects" }, { "title": "Build a profile page", - "url": "https://schalkneethling.com/posts/build-a-profile-page-html-css-part6-dialog-form-netlify" + "url": "/posts/build-a-profile-page-html-css-part6-dialog-form-netlify" }, { "title": "HTML Dialog", - "url": "https://schalkneethling.com/posts/html-dialog-native-solution-for-accessible-modal-interactions" + "url": "/posts/html-dialog-native-solution-for-accessible-modal-interactions" } ] diff --git a/tsconfig.json b/tsconfig.json index 4ddf662a..927b4c8f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,9 @@ { "extends": "astro/tsconfigs/base", "compilerOptions": { - "baseUrl": ".", "paths": { - "@components/*": ["src/components/*"], - "@assets/*": ["src/assets/*"] + "@components/*": ["./src/components/*"], + "@assets/*": ["./src/assets/*"] } } }