commit 017bf6e0673b0648e714439c919b163d6214fd7e Author: Adrian Altner Date: Mon Mar 30 14:16:43 2026 +0200 init diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..f2e7f82 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "Astro Dev", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["dev"], + "port": 4321 + }, + { + "name": "Astro Preview", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["preview"], + "port": 4321 + } + ] +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6bb2a9b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.DS_Store +.git +.gitignore +.astro +dist +node_modules +.env +.env.local +.env.development +.env.development.local +.env.test +.env.test.local diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ade7829 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Anthropic API key — used by scripts/vision.ts to generate photo sidecars +# Get yours at https://console.anthropic.com/settings/keys +ANTHROPIC_API_KEY= + +# Optional: tune vision script behaviour +# VISION_CONCURRENCY=2 # parallel API requests (default: 2) +# VISION_MAX_RETRIES=8 # retries on rate limit (default: 8) +# VISION_BASE_BACKOFF_MS=1500 # initial backoff in ms (default: 1500) + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ba1afad --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + actions-deps: + patterns: + - "*" + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + groups: + patch-minor: + update-types: ["patch", "minor"] + \ No newline at end of file diff --git a/.github/workflows/codeberg_mirror.yml b/.github/workflows/codeberg_mirror.yml new file mode 100644 index 0000000..8af0a04 --- /dev/null +++ b/.github/workflows/codeberg_mirror.yml @@ -0,0 +1,47 @@ +name: 🪞 Mirror to Codeberg +on: + push: + branches: [main] + workflow_dispatch: + schedule: + - cron: "30 0 * * 0" + +jobs: + codeberg: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure SSH + run: | + mkdir -p ~/.ssh + printf '%s\n' "${{ secrets.CODEBERG_SSH }}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -H codeberg.org >> ~/.ssh/known_hosts + cat <<'EOF' > ~/.ssh/config + Host codeberg.org + HostName codeberg.org + User git + IdentityFile ~/.ssh/id_ed25519 + IdentitiesOnly yes + EOF + + - name: Verify SSH access + run: | + ssh -T git@codeberg.org || true + git ls-remote git@codeberg.org:adrian-altner/www.adrian-altner.com.git > /dev/null + + - name: Mirror to Codeberg + run: | + git remote add mirror git@codeberg.org:adrian-altner/www.adrian-altner.com.git + # Remove previously mirrored remote-tracking refs (e.g. refs/remotes/origin/*). + while IFS= read -r ref; do + git push mirror ":${ref}" + done < <(git ls-remote --refs mirror 'refs/remotes/*' | awk '{print $2}') + + # Mirror only real branches and tags. + git push --prune mirror \ + +refs/heads/*:refs/heads/* \ + +refs/tags/*:refs/tags/* \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b584100 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# build output +dist/ +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# local content hotfolder +src/content/* + +# environment variables +.env +.env.local +.env.development +.env.production + +# macOS-specific files +.DS_Store + +# jetbrains setting folder +.idea/ + +# Flickr upload tracking (local state, not for version control) +scripts/flickr-tracking.json + +# Mastodon syndication tracking (persists on VPS, not version controlled) +syndicated.json diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..7543f37 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm check:fix diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c3c7b64 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "astro-build.astro-vscode", + "deque-systems.vscode-axe-linter", + "biomejs.biome" + ], + "unwantedRecommendations": ["esbenp.prettier-vscode"] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d642209 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "command": "./node_modules/.bin/astro dev", + "name": "Development server", + "request": "launch", + "type": "node-terminal" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..058daea --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit" + }, + "editor.defaultFormatter": "biomejs.biome", + "chat.disableAIFeatures": true +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1b13574 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,50 @@ +### 1. Plan Node Default +- Enter plan mode for ANY non-trivial task (3+ steps or architectural decisions) +- If something goes sideways, STOP and re-plan immediately - don't keep pushing +- Use plan mode for verification steps, not just building +- Write detailed specs upfront to reduce ambiguity + +### 2. Subagent Strategy +- Use subagents liberally to keep main context window clean +- Offload research, exploration, and parallel analysis to subagents +- For complex problems, throw more compute at it via subagents +- One tack per subagent for focused execution + +### 3. Self-Improvement Loop +- After ANY correction from the user: update `tasks/lessons.md` with the pattern +- Write rules for yourself that prevent the same mistake +- Ruthlessly iterate on these lessons until mistake rate drops +- Review lessons at session start for relevant project + +### 4. Verification Before Done +- Never mark a task complete without proving it works +- Diff behavior between main and your changes when relevant +- Ask yourself: "Would a staff engineer approve this?" +- Run tests, check logs, demonstrate correctness + +### 5. Demand Elegance (Balanced) +- For non-trivial changes: pause and ask "is there a more elegant way?" +- If a fix feels hacky: "Knowing everything I know now, implement the elegant solution" +- Skip this for simple, obvious fixes - don't over-engineer +- Challenge your own work before presenting it + +### 6. Autonomous Bug Fixing +- When given a bug report: just fix it. Don't ask for hand-holding +- Point at logs, errors, failing tests - then resolve them +- Zero context switching required from the user +- Go fix failing CI tests without being told how + +## Task Management + +1. **Plan First**: Write plan to `tasks/todo.md` with checkable items +2. **Verify Plan**: Check in before starting implementation +3. **Track Progress**: Mark items complete as you go +4. **Explain Changes**: High-level summary at each step +5. **Document Results**: Add review section to `tasks/todo.md` +6. **Capture Lessons**: Update `tasks/lessons.md` after corrections + +## Core Principles + +- **Simplicity First**: Make every change as simple as possible. Impact minimal code. +- **No Laziness**: Find root causes. No temporary fixes. Senior developer standards. +- **Minimat Impact**: Changes should only touch what's necessary. Avoid introducing bugs. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..630bbb6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,95 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +npm run dev # Start dev server (localhost:4321) +npm run build # astro check + build + copy-sw (use this to verify changes) +npm run check # Type check + Biome lint +npm run check:fix # Type check + Biome lint with auto-fix +npm run stylelint # Lint CSS/Astro styles +npm run stylelint:fix # Fix style issues +``` + +There are no automated tests. Verification is done via `npm run build` (0 errors required) and the preview MCP tools. + +## Architecture + +Astro 6 site running in SSR mode (Node.js standalone adapter) with static output for most routes. TypeScript strict mode. Path alias `@/*` → `src/*`. + +**Formatter/linter:** Biome (not ESLint/Prettier). Run `check:fix` after larger edits. + +### Content Collections (`src/content.config.ts`) + +Five collections defined with Zod schemas: + +| Collection | Path | Notes | +|---|---|---| +| `blog` | `src/content/blog/` | Posts with series support (`seriesParent`, `seriesOrder`), tags, category ref, syndication URLs | +| `categories` | `src/content/categories/` | Referenced by blog posts | +| `notes` | `src/content/notes/` | Short-form with optional cover image | +| `links` | `src/content/links/` | Curated external links | +| `collections_photos` | `src/content/photos/collections/` | Photo collections; photos stored as JPG + JSON sidecar files in `img/` subdirs | + +### Key Routing Patterns + +- `/blog/[...slug]` — Blog posts use the full content path as slug (e.g. `2026/03/01/post-name`) +- `/og/blog/[...slug].png` — OG images generated server-side via Satori (`src/lib/og.ts`) +- `/rss/blog.xml`, `/rss/notes.xml`, `/rss/links.xml`, `/rss/photos.xml` — Separate RSS feeds per content type +- `/photos/collections/[...slug]` — Nested photo collections with breadcrumb navigation + +### Lib Utilities (`src/lib/`) + +- `collections.ts` — Photo collection helpers: `collectionSlug()`, `buildCollectionPhotos()`, `buildBreadcrumbs()`, `getChildCollections()` +- `og.ts` — OG image generation using Satori (`buildArticleVNode`, `renderOgImage`) +- `webmentions.ts` — Fetch and filter webmentions from webmention.io +- `photo-albums.ts` — Photo album organisation utilities + +### Scripts (`scripts/`) + +- `mastodon-syndicate.js` — Scans `src/content/blog` and `src/content/notes` for posts without a `syndication` field, posts to Mastodon, writes the status URL back to frontmatter. Env vars: `MASTODON_BASE_URL`, `MASTODON_ACCESS_TOKEN`, `MASTODON_VISIBILITY`, `MASTODON_DRY_RUN`, `MASTODON_LIMIT` +- `publish-posts.sh` — Full deploy orchestration: rsync content to VPS → rebuild container → send webmentions → run mastodon-syndicate + +### IndieWeb / Syndication + +Blog posts support POSSE via `mastodon-syndicate.js`. After posting, the Mastodon status URL is written to frontmatter as `syndication: ["https://..."]`. The `SyndicationLinks` component reads this and renders `u-syndication` microformat links. Webmentions are fetched at build time from webmention.io and displayed via `WebMentions.astro`. + +--- + +## Workflow + +### 1. Plan First +- Enter plan mode for ANY non-trivial task (3+ steps or architectural decisions) +- If something goes sideways, STOP and re-plan immediately - don't keep pushing +- Write plan to `tasks/todo.md` with checkable items (not just TodoWrite tool) +- Check in before starting implementation + +### 2. Subagent Strategy +- Use subagents liberally to keep main context window clean +- Offload research, exploration, and parallel analysis to subagents +- For complex problems, throw more compute at it via subagents + +### 3. Self-Improvement Loop +- After ANY correction from the user: update `tasks/lessons.md` with the pattern +- Review `tasks/lessons.md` at session start +- Write rules that prevent the same mistake from recurring + +### 4. Verification Before Done +- Never mark a task complete without proving it works +- Run `npm run build` — must complete with 0 errors +- Use preview MCP tools to visually verify UI changes + +### 5. Demand Elegance (Balanced) +- For non-trivial changes: pause and ask "is there a more elegant way?" +- Skip for simple, obvious fixes — don't over-engineer + +### 6. Autonomous Bug Fixing +- When given a bug report: just fix it. Point at logs/errors, then resolve them. + +## Core Principles + +- **Simplicity First**: Make every change as simple as possible. Impact minimal code. +- **No Laziness**: Find root causes. No temporary fixes. Senior developer standards. +- **Minimal Impact**: Changes should only touch what's necessary. diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..694d351 --- /dev/null +++ b/Containerfile @@ -0,0 +1,36 @@ +FROM node:22-bookworm-slim AS build + +WORKDIR /app + +RUN corepack enable + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ + +RUN pnpm install --frozen-lockfile + +COPY . . + +RUN pnpm run build:production + + +FROM node:22-bookworm-slim AS runtime + +WORKDIR /app + +ENV NODE_ENV=production +ENV HOST=0.0.0.0 +ENV PORT=4321 + +RUN corepack enable + +COPY --from=build --chown=node:node /app/package.json ./package.json +COPY --from=build --chown=node:node /app/pnpm-lock.yaml ./pnpm-lock.yaml +COPY --from=build --chown=node:node /app/pnpm-workspace.yaml ./pnpm-workspace.yaml +COPY --from=build --chown=node:node /app/node_modules ./node_modules +COPY --from=build --chown=node:node /app/dist ./dist + +USER node + +EXPOSE 4321 + +CMD ["node", "dist/server/entry.mjs"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aef6e46 --- /dev/null +++ b/LICENSE @@ -0,0 +1,32 @@ +MIT License + +Copyright (c) 2026 Adrian Altner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +## Content License + +All content in `src/content/` and `public/` — including but not limited to +articles, notes, and photographs — is licensed under +[Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)](https://creativecommons.org/licenses/by-sa/4.0/). + +You are free to share and adapt the content, as long as you give appropriate +credit and distribute your contributions under the same license. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8aa2c67 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# Astro Starter + +An Astro starter project set up with pnpm, Biome, Stylelint, Husky, and Astro Sitemap, plus sensible VS Code defaults and recommended workspace extensions. + +## Getting Started + +1. Click "Use this template" and create a new repository. +2. In `astro.config.mjs`, update `site` from `https://mysite.com` to your site URL. +3. In `src/layouts/BaseLayout.astro`, update `siteName` to your site name. +4. Good to go! + +## What's included + +**Basics** +- A blank [Astro](https://astro.build/) project with TypeScript enabled +- `pnpm` package management plus `only-allow pnpm` +- Import aliases of `src` using `~` to enable using `import { Component } from '~/components'` +- Basic meta tags and open graph tags in `layouts/BaseLayout.astro` +- `initial-scale=1` on `viewport` to prevent default mobile zoom-out +- Site name displays after the page title (`Page Title · Site Name`) on pages other than the main index +- [smartypants](https://github.com/othree/smartypants.js) smart quotes for page titles and descriptions + +**Styles** +- Josh Comeau's [reset.css](https://www.joshwcomeau.com/css/custom-css-reset/) + +**Linting and Formatting** +- Linting and formatting with [Biome](https://biomejs.dev/) +- Style linting with [Stylelint](https://stylelint.io/) and [stylelint-config-astro](https://github.com/mattpfeffer/stylelint-config-astro) + +**VS Code** +- Format on save and on paste +- Default formatter set to Astro +- Workplace recommendations for [Astro](https://marketplace.visualstudio.com/items?itemName=astro-build.astro-vscode), [axe Accessibility Linter](https://marketplace.visualstudio.com/items?itemName=deque-systems.vscode-axe-linter), and [Biome](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) + +**Package.json scripts** +- `check` — Run Astro check + Biome lint and format +- `check:fix` — Run Astro check + Biome lint and format + fix errors +- `stylelint` — Run Stylelint +- `stylelint:fix` — Run Stylelint and fix errors + +**Vision script** (requires `ANTHROPIC_API_KEY` in `.env.local`) + +Generate AI metadata (title, description, tags) + EXIF sidecars for a photo album: + +```bash +node --env-file=.env.local --experimental-strip-types scripts/vision.ts src/content/photos/albums/ +``` + +Optional flags: +- `--refresh` — overwrite existing JSON sidecars +- `--concurrency=N` — parallel API calls (default: 2) +- `--retries=N` — max retries on rate limits (default: 8) +- `--backoff-ms=N` — base backoff in ms (default: 1500) + +**Husky** +- `pnpm check` on pre-commit + +**Dependabot** +- Weekly dependency update checks +- All GitHub Action updates grouped in a single pull request +- All patch or minor package updates grouped in a single pull request +- All major package updates created as individual pull requests + +**Config** +- Link prefetching enabled +- Dev toolbar disabled +- [Astro Sitemap](https://docs.astro.build/en/guides/integrations-guide/sitemap/) installed diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..3ab334c --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,38 @@ +import mdx from "@astrojs/mdx"; +import node from "@astrojs/node"; +import sitemap from "@astrojs/sitemap"; +import { defineConfig } from "astro/config"; +import EmbedPlugin from "astro-embed/integration"; +import rehypeExternalLinks from "rehype-external-links"; +import { remarkObsidianLinks } from "./src/lib/remark-obsidian-links.mjs"; + +export default defineConfig({ + site: "https://adrian-altner.com", + + devToolbar: { + enabled: false, + }, + + markdown: { + remarkPlugins: [remarkObsidianLinks], + rehypePlugins: [ + [ + rehypeExternalLinks, + { target: "_blank", rel: ["noopener", "noreferrer"] }, + ], + ], + }, + + integrations: [sitemap(), EmbedPlugin(), mdx()], + prefetch: true, + + vite: { + ssr: { + noExternal: ["smartypants"], + }, + }, + + adapter: node({ + mode: "standalone", + }), +}); diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 0000000..76120a0 --- /dev/null +++ b/biome.jsonc @@ -0,0 +1,42 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.7/schema.json", + "files": { + "includes": [ + "**", + "!**/build", + "!**/dist", + "!**/node_modules", + "!**/playwright-report", + "!**/.vercel", + "!**/.netlify", + "!**/.vscode", + "!**/.astro", + "!scripts/flickr-tracking.json" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf" + }, + "assist": { "actions": { "source": { "organizeImports": "on" } } }, + "linter": { "enabled": true, "rules": { "recommended": true } }, + "overrides": [ + { + "includes": ["**/*.astro"], + "linter": { + "rules": { + "style": { + "useConst": "off", + "useImportType": "off" + }, + "correctness": { + "noUnusedVariables": "off", + "noUnusedImports": "off" + } + } + } + } + ] +} diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..f6696e5 --- /dev/null +++ b/compose.yml @@ -0,0 +1,16 @@ +name: www-adrian-altner-com + +services: + website: + build: + context: . + dockerfile: Containerfile + image: localhost/www.adrian-altner.com:latest + container_name: www.adrian-altner.com + ports: + - "4321:4321" + environment: + NODE_ENV: production + HOST: 0.0.0.0 + PORT: 4321 + restart: unless-stopped \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..d0a8dce --- /dev/null +++ b/package.json @@ -0,0 +1,81 @@ +{ + "name": "@adrian-altner/adrian-altner.com", + "version": "0.0.1", + "title": "Adrian Altner", + "description": "The personal website of Adrian Altner", + "homepage": "https://adrian-altner.photo", + "author": { + "name": "Adrian Altner", + "url": "https://adrian-altner.com/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/adrian-altner/adrian-altner.com.git" + }, + "bugs": { + "url": "https://github.com/adrian-altner/adrian-altner.com/issues" + }, + "license": "MIT", + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "preinstall": "npx only-allow pnpm", + "prepare": "husky", + "dev": "astro dev", + "start": "astro dev", + "build": "astro check && astro build && node scripts/copy-sw.js", + "build:production": "astro build --mode production && node scripts/copy-sw.js", + "preview": "astro preview", + "astro": "astro", + "check": "astro check && biome check", + "check:fix": "astro check && biome check --write", + "stylelint": "stylelint '**/*.{astro,css}'", + "stylelint:fix": "stylelint '**/*.{astro,css}' --fix", + "generate:icons": "pwa-assets-generator" + }, + "pnpm": { + "peerDependencyRules": { + "allowedVersions": { + "@vite-pwa/astro>astro": "*" + } + } + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.80.0", + "@astrojs/check": "^0.9.7", + "@astrojs/mdx": "^5.0.2", + "@astrojs/node": "^10.0.2", + "@astrojs/rss": "^4.0.17", + "@astrojs/sitemap": "^3.7.1", + "@fontsource/exo-2": "^5.2.8", + "astro": "^6.0.4", + "astro-embed": "^0.12.0", + "chart.js": "^4.5.1", + "consola": "^3.4.2", + "justified-layout": "^4.1.0", + "leaflet": "^1.9.4", + "leaflet.markercluster": "^1.5.3", + "rehype-external-links": "^3.0.0", + "satori": "^0.26.0", + "smartypants": "^0.2.2", + "typescript": "^5.9.3" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.7", + "@types/justified-layout": "^4.1.4", + "@types/leaflet": "^1.9.21", + "@types/leaflet.markercluster": "^1.5.6", + "@vite-pwa/assets-generator": "^1.0.2", + "dotenv": "^17.3.1", + "fast-glob": "^3.3.3", + "husky": "^9.1.7", + "sharp": "^0.34.5", + "stylelint": "^16.26.1", + "stylelint-config-astro": "^2.0.0", + "stylelint-config-standard": "^39.0.1", + "unist-util-visit": "^5.1.0", + "workbox-window": "^7.4.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..36ee3b2 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,6430 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@anthropic-ai/sdk': + specifier: ^0.80.0 + version: 0.80.0(zod@4.3.6) + '@astrojs/check': + specifier: ^0.9.7 + version: 0.9.7(prettier-plugin-astro@0.14.1)(prettier@3.8.1)(typescript@5.9.3) + '@astrojs/mdx': + specifier: ^5.0.2 + version: 5.0.2(astro@6.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2)) + '@astrojs/node': + specifier: ^10.0.2 + version: 10.0.2(astro@6.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2)) + '@astrojs/rss': + specifier: ^4.0.17 + version: 4.0.17 + '@astrojs/sitemap': + specifier: ^3.7.1 + version: 3.7.1 + '@fontsource/exo-2': + specifier: ^5.2.8 + version: 5.2.8 + astro: + specifier: ^6.0.4 + version: 6.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2) + astro-embed: + specifier: ^0.12.0 + version: 0.12.0(astro@6.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2)) + chart.js: + specifier: ^4.5.1 + version: 4.5.1 + consola: + specifier: ^3.4.2 + version: 3.4.2 + justified-layout: + specifier: ^4.1.0 + version: 4.1.0 + leaflet: + specifier: ^1.9.4 + version: 1.9.4 + leaflet.markercluster: + specifier: ^1.5.3 + version: 1.5.3(leaflet@1.9.4) + rehype-external-links: + specifier: ^3.0.0 + version: 3.0.0 + satori: + specifier: ^0.26.0 + version: 0.26.0 + smartypants: + specifier: ^0.2.2 + version: 0.2.2 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + devDependencies: + '@biomejs/biome': + specifier: ^2.4.7 + version: 2.4.7 + '@types/justified-layout': + specifier: ^4.1.4 + version: 4.1.4 + '@types/leaflet': + specifier: ^1.9.21 + version: 1.9.21 + '@types/leaflet.markercluster': + specifier: ^1.5.6 + version: 1.5.6 + '@vite-pwa/assets-generator': + specifier: ^1.0.2 + version: 1.0.2 + dotenv: + specifier: ^17.3.1 + version: 17.3.1 + fast-glob: + specifier: ^3.3.3 + version: 3.3.3 + husky: + specifier: ^9.1.7 + version: 9.1.7 + sharp: + specifier: ^0.34.5 + version: 0.34.5 + stylelint: + specifier: ^16.26.1 + version: 16.26.1(typescript@5.9.3) + stylelint-config-astro: + specifier: ^2.0.0 + version: 2.0.0(postcss-html@1.7.0)(stylelint@16.26.1(typescript@5.9.3)) + stylelint-config-standard: + specifier: ^39.0.1 + version: 39.0.1(stylelint@16.26.1(typescript@5.9.3)) + unist-util-visit: + specifier: ^5.1.0 + version: 5.1.0 + workbox-window: + specifier: ^7.4.0 + version: 7.4.0 + +packages: + + '@anthropic-ai/sdk@0.80.0': + resolution: {integrity: sha512-WeXLn7zNVk3yjeshn+xZHvld6AoFUOR3Sep6pSoHho5YbSi6HwcirqgPA5ccFuW8QTVJAAU7N8uQQC6Wa9TG+g==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@astro-community/astro-embed-baseline-status@0.2.2': + resolution: {integrity: sha512-07TBEb+xQWWZfMuoHohcZv/r2VSB80/1xN5iLhzSqavLmdsMyebEnbc6tvw3yMkxvX9IBLduNA5SxvVkpmowNQ==} + + '@astro-community/astro-embed-bluesky@0.1.6': + resolution: {integrity: sha512-3y6Y3cRelLnR9AYMItmEAjcr83KAEa6WvsxQ1eHq1cPBzICXknuzphaZlmQZ+QG5NTtmEJD+2lQWrFba/BfM1A==} + + '@astro-community/astro-embed-gist@0.1.0': + resolution: {integrity: sha512-wP3EoBZZjDoPLH6TZzem8jDJxOuweDoK5zWmSra0QBKz3Lry1tZGCwKII5mlnOL2AmTKLrfqrBXTxSGwb7AimQ==} + + '@astro-community/astro-embed-integration@0.11.0': + resolution: {integrity: sha512-xmwXN8039zUT0/lBO2GUr8cm5t/v+9Fh8QkPUhTWy+A7RR0+PwT1M3PBm8q01A1rK9q0myOyFHEcSOp+WkH5tg==} + peerDependencies: + astro: ^5.0.0 || ^6.0.0-alpha + + '@astro-community/astro-embed-link-preview@0.3.1': + resolution: {integrity: sha512-TI++efm08+kJqxqA7bvxBr7+Zt4yCceA6s3wvAQJ87eiaxbLqAFUSQ+paQD66ET9dIC+IuKzHOMwsoDfqBidYw==} + + '@astro-community/astro-embed-mastodon@0.1.1': + resolution: {integrity: sha512-g5Mt1H6GxjkIvXC0HcKqLanZgXHu1e0vNqiQJ8ckryPKmbijYPfhGJYJLPHxE6PaFEA5tmwcmJouVcMPMjf2Kg==} + + '@astro-community/astro-embed-twitter@0.5.11': + resolution: {integrity: sha512-6cmyQY4LVVJj6x7qC6XrhWcxNffLvR+QGE/iw5HTOtAn60AStr6u+IX2Txpy6N6bta0DLjGqhTBhkC3NxmVKJg==} + + '@astro-community/astro-embed-utils@0.2.0': + resolution: {integrity: sha512-Ia70AMCFOUOSoaMfMaK7Ovk7VyIY4opwzBJoA6GeL+omkvpFwDbSWmA8MOiMF4gJC0j/1dgrEir+txIb+WvsCA==} + + '@astro-community/astro-embed-vimeo@0.3.12': + resolution: {integrity: sha512-VLNcsniT5qZ/7GaSGFWnX4ar0qcGyAYB1HQnAH362Zjqs0QI2he9u1nWv1kEx4xr3fZVxl6D2QuNN4xKtd8/ig==} + + '@astro-community/astro-embed-youtube@0.5.10': + resolution: {integrity: sha512-hVlx77KQLjKzElVQnrU5znQ5/E60keVSAPrhuWvQQHuqva5auJtt8YBpOThkwDMuEKXjQybEF1/3C07RZ8MAOQ==} + + '@astrojs/check@0.9.7': + resolution: {integrity: sha512-dA7U5/OFg8/xaMUb2vUOOJuuJXnMpHy6F0BM8ZhL7WT5OkTBwJ0GoW38n4fC4CXt+lT9mLWL0y8Pa74tFByBpQ==} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + + '@astrojs/compiler@2.13.1': + resolution: {integrity: sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg==} + + '@astrojs/compiler@3.0.0': + resolution: {integrity: sha512-MwAbDE5mawZ1SS+D8qWiHdprdME5Tlj2e0YjxnEICvcOpbSukNS7Sa7hA5PK+6RrmUr/t6Gi5YgrdZKjbO/WPQ==} + + '@astrojs/internal-helpers@0.8.0': + resolution: {integrity: sha512-J56GrhEiV+4dmrGLPNOl2pZjpHXAndWVyiVDYGDuw6MWKpBSEMLdFxHzeM/6sqaknw9M+HFfHZAcvi3OfT3D/w==} + + '@astrojs/language-server@2.16.4': + resolution: {integrity: sha512-42oqz9uX+hU1/rFniJvtYW9FbfZJ6syM2fYZFi7Ub71/kOvF1GSeMS8sA3Ogs3iOeNUWefk/ImwBiiHeNmJfSA==} + hasBin: true + peerDependencies: + prettier: ^3.0.0 + prettier-plugin-astro: '>=0.11.0' + peerDependenciesMeta: + prettier: + optional: true + prettier-plugin-astro: + optional: true + + '@astrojs/markdown-remark@7.0.0': + resolution: {integrity: sha512-jTAXHPy45L7o1ljH4jYV+ShtOHtyQUa1mGp3a5fJp1soX8lInuTJQ6ihmldHzVM4Q7QptU4SzIDIcKbBJO7sXQ==} + + '@astrojs/markdown-remark@7.0.1': + resolution: {integrity: sha512-zAfLJmn07u9SlDNNHTpjv0RT4F8D4k54NR7ReRas8CO4OeGoqSvOuKwqCFg2/cqN3wHwdWlK/7Yv/lMXlhVIaw==} + + '@astrojs/mdx@5.0.2': + resolution: {integrity: sha512-0as6odPH9ZQhS3pdH9dWmVOwgXuDtytJiE4VvYgR0lSFBvF4PSTyE0HdODHm/d7dBghvWTPc2bQaBm4y4nTBNw==} + engines: {node: '>=22.12.0'} + peerDependencies: + astro: ^6.0.0 + + '@astrojs/node@10.0.2': + resolution: {integrity: sha512-utZCETOanWnrJWy6X6xFk+/wvOdZyPvgymV/wEN+SKRzEomg5q+VkkcsIv/6jkCHFvlXyjXRmPwt/oluT5cpcg==} + peerDependencies: + astro: ^6.0.0 + + '@astrojs/prism@4.0.0': + resolution: {integrity: sha512-NndtNPpxaGinRpRytljGBvYHpTOwHycSZ/c+lQi5cHvkqqrHKWdkPEhImlODBNmbuB+vyQUNUDXyjzt66CihJg==} + engines: {node: ^20.19.1 || >=22.12.0} + + '@astrojs/prism@4.0.1': + resolution: {integrity: sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ==} + engines: {node: '>=22.12.0'} + + '@astrojs/rss@4.0.17': + resolution: {integrity: sha512-eV+wdMbeVKC9+sPaV0LN8JL1LGo9YAh3GKl4Ou4nzMNLmXM/aswYpSGxVEAuHilgBZ6/++/Pv08ICmuOqX107w==} + + '@astrojs/sitemap@3.7.1': + resolution: {integrity: sha512-IzQqdTeskaMX+QDZCzMuJIp8A8C1vgzMBp/NmHNnadepHYNHcxQdGLQZYfkbd2EbRXUfOS+UDIKx8sKg0oWVdw==} + + '@astrojs/telemetry@3.3.0': + resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==} + engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} + + '@astrojs/yaml2ts@0.2.3': + resolution: {integrity: sha512-PJzRmgQzUxI2uwpdX2lXSHtP4G8ocp24/t+bZyf5Fy0SZLSF9f9KXZoMlFM/XCGue+B0nH/2IZ7FpBYQATBsCg==} + + '@atproto/api@0.13.35': + resolution: {integrity: sha512-vsEfBj0C333TLjDppvTdTE0IdKlXuljKSveAeI4PPx/l6eUKNnDTsYxvILtXUVzwUlTDmSRqy5O4Ryh78n1b7g==} + + '@atproto/common-web@0.4.19': + resolution: {integrity: sha512-3BTi58p5WpT+9/zb6UZrdsXcfPo5P45UJm0E4iwHLILr+jc37CuBj9JReDSZ4U0i9RTrI3ZkfySyZ9bd+LnMsw==} + + '@atproto/lex-data@0.0.14': + resolution: {integrity: sha512-53DUa9664SS76nGAMYopWsO10OH0AAdf7P/HSKB6Wzx3iqe6lk/K61QZnKxOG1LreYl5CfvIJU6eNf4txI6GlQ==} + + '@atproto/lex-json@0.0.14': + resolution: {integrity: sha512-6lPkDKqe7teEu4WrN5q7400cvZKgYS3uwUMvzG3F9XkgVYhOwSDCtouV/nSLBbpvo3l9OP0kiigtclcNcyekww==} + + '@atproto/lexicon@0.4.14': + resolution: {integrity: sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ==} + + '@atproto/syntax@0.3.4': + resolution: {integrity: sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg==} + + '@atproto/syntax@0.4.3': + resolution: {integrity: sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==} + + '@atproto/syntax@0.5.1': + resolution: {integrity: sha512-J8DJjgKgACIyCTbpfvoTnf7+ofTx1kxTGO7KAftkC+jczaMdQhKdgIBAg2DaYy+80cvYGTHy5q/HI9qMAwGbWw==} + + '@atproto/xrpc@0.6.12': + resolution: {integrity: sha512-Ut3iISNLujlmY9Gu8sNU+SPDJDvqlVzWddU8qUr0Yae5oD4SguaUFjjhireMGhQ3M5E0KljQgDbTmnBo1kIZ3w==} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@2.4.7': + resolution: {integrity: sha512-vXrgcmNGZ4lpdwZSpMf1hWw1aWS6B+SyeSYKTLrNsiUsAdSRN0J4d/7mF3ogJFbIwFFSOL3wT92Zzxia/d5/ng==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.7': + resolution: {integrity: sha512-Oo0cF5mHzmvDmTXw8XSjhCia8K6YrZnk7aCS54+/HxyMdZMruMO3nfpDsrlar/EQWe41r1qrwKiCa2QDYHDzWA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.7': + resolution: {integrity: sha512-I+cOG3sd/7HdFtvDSnF9QQPrWguUH7zrkIMMykM3PtfWU9soTcS2yRb9Myq6MHmzbeCT08D1UmY+BaiMl5CcoQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.7': + resolution: {integrity: sha512-I2NvM9KPb09jWml93O2/5WMfNR7Lee5Latag1JThDRMURVhPX74p9UDnyTw3Ae6cE1DgXfw7sqQgX7rkvpc0vw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.4.7': + resolution: {integrity: sha512-om6FugwmibzfP/6ALj5WRDVSND4H2G9X0nkI1HZpp2ySf9lW2j0X68oQSaHEnls6666oy4KDsc5RFjT4m0kV0w==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.4.7': + resolution: {integrity: sha512-00kx4YrBMU8374zd2wHuRV5wseh0rom5HqRND+vDldJPrWwQw+mzd/d8byI9hPx926CG+vWzq6AeiT7Yi5y59g==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.4.7': + resolution: {integrity: sha512-bV8/uo2Tj+gumnk4sUdkerWyCPRabaZdv88IpbmDWARQQoA/Q0YaqPz1a+LSEDIL7OfrnPi9Hq1Llz4ZIGyIQQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.4.7': + resolution: {integrity: sha512-hOUHBMlFCvDhu3WCq6vaBoG0dp0LkWxSEnEEsxxXvOa9TfT6ZBnbh72A/xBM7CBYB7WgwqboetzFEVDnMxelyw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.7': + resolution: {integrity: sha512-qEpGjSkPC3qX4ycbMUthXvi9CkRq7kZpkqMY1OyhmYlYLnANnooDQ7hDerM8+0NJ+DZKVnsIc07h30XOpt7LtQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@cacheable/memory@2.0.6': + resolution: {integrity: sha512-7e8SScMocHxcAb8YhtkbMhGG+EKLRIficb1F5sjvhSYsWTZGxvg4KIDp8kgxnV2PUJ3ddPe6J9QESjKvBWRDkg==} + + '@cacheable/utils@2.3.2': + resolution: {integrity: sha512-8kGE2P+HjfY8FglaOiW+y8qxcaQAfAhVML+i66XJR3YX5FtyDqn6Txctr3K2FrbxLKixRRYYBWMbuGciOhYNDg==} + + '@canvas/image-data@1.1.0': + resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==} + + '@capsizecss/unpack@4.0.0': + resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==} + engines: {node: '>=18'} + + '@clack/core@1.1.0': + resolution: {integrity: sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==} + + '@clack/prompts@1.1.0': + resolution: {integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==} + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.20': + resolution: {integrity: sha512-8BHsjXfSciZxjmHQOuVdW2b8WLUPts9a+mfL13/PzEviufUEW2xnvQuOlKs9dRBHgRqJ53SF/DUoK9+MZk72oQ==} + engines: {node: '>=18'} + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@csstools/media-query-list-parser@4.0.3': + resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@dual-bundle/import-meta-resolve@4.2.1': + resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==} + + '@emmetio/abbreviation@2.3.3': + resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==} + + '@emmetio/css-abbreviation@2.1.8': + resolution: {integrity: sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==} + + '@emmetio/css-parser@0.4.1': + resolution: {integrity: sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ==} + + '@emmetio/html-matcher@1.3.0': + resolution: {integrity: sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==} + + '@emmetio/scanner@1.0.4': + resolution: {integrity: sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==} + + '@emmetio/stream-reader-utils@0.1.0': + resolution: {integrity: sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==} + + '@emmetio/stream-reader@2.2.0': + resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} + + '@emnapi/runtime@1.7.0': + resolution: {integrity: sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==} + + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@fontsource/exo-2@5.2.8': + resolution: {integrity: sha512-cOZuiqW+gKg2SJAjymCicup0A8qto4M/lpyGahal3twMeLCRma8x/okFCdLYlTvYeQesF7d3WLKi+N04iPb40A==} + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/bigmap@1.3.0': + resolution: {integrity: sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.5.4 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + + '@mdx-js/mdx@3.1.1': + resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + + '@parse5/tools@0.7.0': + resolution: {integrity: sha512-JDvrGhc8kYBq7/SM4obJkpgwWo6pRjF/fo9CCaiJyVOkDf203Ciq2UF6TjzCFXKs7Q/zS2sS4deyBx0XzRvh9Q==} + peerDependencies: + parse5: 7.x || 8.x + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@shikijs/core@4.0.2': + resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.0.2': + resolution: {integrity: sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.0.2': + resolution: {integrity: sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==} + engines: {node: '>=20'} + + '@shikijs/langs@4.0.2': + resolution: {integrity: sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.0.2': + resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==} + engines: {node: '>=20'} + + '@shikijs/themes@4.0.2': + resolution: {integrity: sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==} + engines: {node: '>=20'} + + '@shikijs/types@4.0.2': + resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@shuding/opentype.js@1.4.0-beta.0': + resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} + engines: {node: '>= 8.0.0'} + hasBin: true + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/justified-layout@4.1.4': + resolution: {integrity: sha512-q2ybP0u0NVj87oMnGZOGxY2iUN8ddr48zPOBHBdbOLpsMTA/keGj+93ou+OMCnJk0xewzlNIaVEkxM6VBD3E2w==} + + '@types/leaflet.markercluster@1.5.6': + resolution: {integrity: sha512-I7hZjO2+isVXGYWzKxBp8PsCzAYCJBc29qBdFpquOCkS7zFDqUsUvkEOyQHedsk/Cy5tocQzf+Ndorm5W9YKTQ==} + + '@types/leaflet@1.9.21': + resolution: {integrity: sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/nlcst@2.0.3': + resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + + '@types/node@24.12.0': + resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} + + '@types/node@25.0.6': + resolution: {integrity: sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q==} + + '@types/sax@1.2.7': + resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vite-pwa/assets-generator@1.0.2': + resolution: {integrity: sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==} + engines: {node: '>=16.14.0'} + hasBin: true + + '@volar/kit@2.4.28': + resolution: {integrity: sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg==} + peerDependencies: + typescript: '*' + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/language-server@2.4.28': + resolution: {integrity: sha512-NqcLnE5gERKuS4PUFwlhMxf6vqYo7hXtbMFbViXcbVkbZ905AIVWhnSo0ZNBC2V127H1/2zP7RvVOVnyITFfBw==} + + '@volar/language-service@2.4.28': + resolution: {integrity: sha512-Rh/wYCZJrI5vCwMk9xyw/Z+MsWxlJY1rmMZPsxUoJKfzIRjS/NF1NmnuEcrMbEVGja00aVpCsInJfixQTMdvLw==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + '@vscode/emmet-helper@2.11.0': + resolution: {integrity: sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==} + + '@vscode/l10n@0.0.18': + resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-iterate@2.0.1: + resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + + astro-auto-import@0.4.6: + resolution: {integrity: sha512-8EgeOTChgHX6x31s2CjeOUCDuG2s0wgT9D9zXI4CxgmljEoJeCAWIq/henhdmvZ+Y103MfH7CYNw5VW7GiM6xQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + astro: ^2.0.0 || ^3.0.0-beta || ^4.0.0-beta || ^5.0.0-beta + + astro-embed@0.12.0: + resolution: {integrity: sha512-Hp/zfIFsibBSCXEC09Lk38uYq5IJyXClbNASiT/06fqrMvgWJzPEPvtnCEo1qIw8hxIh+4+esAJoktu5YKRIEA==} + peerDependencies: + astro: ^5.0.0 || ^6.0.0-alpha + + astro@6.0.4: + resolution: {integrity: sha512-1piLJCPTL/x7AMO2cjVFSTFyRqKuC3W8sSEySCt1aJio+p/wGs5H3K+Xr/rE9ftKtknLUtjxCqCE7/0NsXfGpQ==} + engines: {node: ^20.19.1 || >=22.12.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} + hasBin: true + + await-lock@2.2.2: + resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cacheable@2.3.0: + resolution: {integrity: sha512-HHiAvOBmlcR2f3SQ7kdlYD8+AUJG+wlFZ/Ze8tl1Vzvz0MdOh8IYA/EFU4ve8t1/sZ0j4MGi7ST5MoTwHessQA==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + common-ancestor-path@2.0.0: + resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} + engines: {node: '>= 18'} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + css-background-parser@0.1.0: + resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} + + css-box-shadow@1.0.0-3: + resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==} + + css-color-keywords@1.0.0: + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} + engines: {node: '>=4'} + + css-functions-list@3.2.3: + resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==} + engines: {node: '>=12 || >=16'} + + css-gradient-parser@0.0.17: + resolution: {integrity: sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==} + engines: {node: '>=16'} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-selector-parser@3.3.0: + resolution: {integrity: sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==} + + css-to-react-native@3.2.0: + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-bmp@0.2.1: + resolution: {integrity: sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA==} + engines: {node: '>=8.6.0'} + + decode-ico@0.4.1: + resolution: {integrity: sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA==} + engines: {node: '>=8.6'} + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@5.6.4: + resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@8.0.3: + resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} + engines: {node: '>=0.3.1'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + engines: {node: '>=12'} + + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emmet@2.4.11: + resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} + + emoji-regex-xs@2.0.1: + resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==} + engines: {node: '>=10.0.0'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + + esast-util-from-estree@2.0.0: + resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} + + esast-util-from-js@2.0.1: + resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-scope@1.0.0: + resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} + + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fast-xml-builder@1.1.4: + resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + + fast-xml-parser@5.4.1: + resolution: {integrity: sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==} + hasBin: true + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.7.4: + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + + file-entry-cache@11.1.1: + resolution: {integrity: sha512-TPVFSDE7q91Dlk1xpFLvFllf8r0HyOMOlnWy7Z2HBku5H3KhIeOGInexrIeg2D64DosVB/JXkrrk6N/7Wriq4A==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + flat-cache@6.1.19: + resolution: {integrity: sha512-l/K33newPTZMTGAnnzaiqSl6NnH7Namh8jBNjrgjprWxGmZUuxx/sJNIRaijOh3n7q7ESbhNZC+pvVZMFdeU4A==} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + flattie@1.1.1: + resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} + engines: {node: '>=8'} + + fontace@0.4.1: + resolution: {integrity: sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==} + + fontkitten@1.0.3: + resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} + engines: {node: '>=20'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + + h3@1.15.6: + resolution: {integrity: sha512-oi15ESLW5LRthZ+qPCi5GNasY/gvynSKUQxgiovrY63bPAtG59wtM+LSrlcwvOHAXzGrXVLnI97brbkdPF9WoQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hashery@1.3.0: + resolution: {integrity: sha512-fWltioiy5zsSAs9ouEnvhsVJeAXRybGCNNv0lvzpzNOSDbULXRy7ivFWwCCv4I5Am6kSo75hmbsCduOoc2/K4w==} + engines: {node: '>=20'} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-estree@3.1.3: + resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + hex-rgb@4.3.0: + resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} + engines: {node: '>=6'} + + hookified@1.13.0: + resolution: {integrity: sha512-6sPYUY8olshgM/1LDNW4QZQN0IqgKhtl/1C8koNZBJrKLBk3AZl6chQtNwpNztvfiApHMEwMHek5rv993PRbWw==} + + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + ico-endec@0.1.6: + resolution: {integrity: sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-absolute-url@4.0.1: + resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iso-datestring-validator@2.2.2: + resolution: {integrity: sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + jsonc-parser@2.3.1: + resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + justified-layout@4.1.0: + resolution: {integrity: sha512-M5FimNMXgiOYerVRGsXZ2YK9YNCaTtwtYp7Hb2308U1Q9TXXHx5G0p08mcVR5O53qf8bWY4NJcPBxE6zuayXSg==} + + keyv@5.5.4: + resolution: {integrity: sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + leaflet.markercluster@1.5.3: + resolution: {integrity: sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==} + peerDependencies: + leaflet: ^1.3.1 + + leaflet@1.9.4: + resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==} + + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lite-youtube-embed@0.3.4: + resolution: {integrity: sha512-aXgxpwK7AIW58GEbRzA8EYaY4LWvF3FKak6B9OtSJmuNyLhX2ouD4cMTxz/yR5HFInhknaYd2jLWOTRTvT8oAw==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + lru-cache@11.2.7: + resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} + engines: {node: 20 || >=22} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.2: + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} + + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mathml-tag-names@2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + + mdast-util-definitions@6.0.0: + resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-mdx-expression@3.0.1: + resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} + + micromark-extension-mdx-jsx@3.0.2: + resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==} + + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-mdx-expression@2.0.3: + resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-events-to-acorn@2.0.3: + resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + multiformats@9.9.0: + resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + neotraverse@0.6.18: + resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} + engines: {node: '>= 10'} + + nlcst-to-string@4.0.0: + resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-mock-http@1.0.4: + resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.5: + resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==} + + p-limit@7.3.0: + resolution: {integrity: sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==} + engines: {node: '>=20'} + + p-queue@9.1.0: + resolution: {integrity: sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==} + engines: {node: '>=20'} + + p-timeout@7.0.1: + resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} + engines: {node: '>=20'} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-css-color@0.2.1: + resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-latin@7.0.0: + resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-expression-matcher@1.2.0: + resolution: {integrity: sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==} + engines: {node: '>=14.0.0'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + piccolore@0.1.3: + resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss-html@1.7.0: + resolution: {integrity: sha512-MfcMpSUIaR/nNgeVS8AyvyDugXlADjN9AcV7e5rDfrF1wduIAGSkL4q2+wgrZgA3sHVAHLDO9FuauHhZYW2nBw==} + engines: {node: ^12 || >=14} + + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@6.0.0: + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + prettier-plugin-astro@0.14.1: + resolution: {integrity: sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==} + engines: {node: ^14.15.0 || >=16.0.0} + + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + qified@0.5.2: + resolution: {integrity: sha512-7gJ6mxcQb9vUBOtbKm5mDevbe2uRcOEVp1g4gb/Q+oLntB3HY8eBhOYRxFI2mlDFlY1e4DOSCptzxarXRvzxCA==} + engines: {node: '>=20'} + + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + recma-build-jsx@1.0.0: + resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} + + recma-jsx@1.0.1: + resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + recma-parse@1.0.0: + resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} + + recma-stringify@1.0.0: + resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + rehype-external-links@3.0.0: + resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==} + + rehype-parse@9.0.1: + resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-recma@1.0.0: + resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + + rehype-stringify@10.0.1: + resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + + rehype@13.0.2: + resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-mdx@3.1.1: + resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-smartypants@3.0.2: + resolution: {integrity: sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==} + engines: {node: '>=16.0.0'} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + request-light@0.5.8: + resolution: {integrity: sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==} + + request-light@0.7.0: + resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + retext-latin@4.0.0: + resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} + + retext-smartypants@6.2.0: + resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} + + retext-stringify@4.0.0: + resolution: {integrity: sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==} + + retext@9.0.0: + resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + s.color@0.0.15: + resolution: {integrity: sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==} + + sass-formatter@0.7.9: + resolution: {integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==} + + satori@0.26.0: + resolution: {integrity: sha512-tkMFrfIs3l2mQ2JEcyW0ADTy3zGggFRFzi6Ef8YozQSFsFKEqaSO1Y8F9wJg4//PJGQauMalHGTUEkPrFwhVPA==} + engines: {node: '>=16'} + + sax@1.5.0: + resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==} + engines: {node: '>=11.0.0'} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + server-destroy@1.0.1: + resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sharp-ico@0.1.5: + resolution: {integrity: sha512-a3jODQl82NPp1d5OYb0wY+oFaPk7AvyxipIowCHk7pBsZCWgbe0yAkU2OOXdoH0ENyANhyOQbs9xkAiRHcF02Q==} + + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shiki@4.0.2: + resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==} + engines: {node: '>=20'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + sitemap@9.0.1: + resolution: {integrity: sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==} + engines: {node: '>=20.19.5', npm: '>=10.8.2'} + hasBin: true + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + smartypants@0.2.2: + resolution: {integrity: sha512-TzobUYoEft/xBtb2voRPryAUIvYguG0V7Tt3de79I1WfXgCwelqVsGuZSnu3GFGRZhXR90AeEYIM+icuB/S06Q==} + hasBin: true + + smol-toml@1.6.0: + resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} + engines: {node: '>= 18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + stream-replace-string@2.0.0: + resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string.prototype.codepointat@0.2.1: + resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strnum@2.2.2: + resolution: {integrity: sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==} + + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + stylelint-config-astro@2.0.0: + resolution: {integrity: sha512-BSl+wNEa3h1+GhHAfI3WO/fPylcVoePLIMd+JX1hz1Pt2cnqRswjfA4EqD6Wy2DqrariqYJE1xXZCnuJNrjb8w==} + peerDependencies: + postcss-html: ^1.0.0 + stylelint: '>=14.0.0' + + stylelint-config-recommended@17.0.0: + resolution: {integrity: sha512-WaMSdEiPfZTSFVoYmJbxorJfA610O0tlYuU2aEwY33UQhSPgFbClrVJYWvy3jGJx+XW37O+LyNLiZOEXhKhJmA==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.23.0 + + stylelint-config-standard@39.0.1: + resolution: {integrity: sha512-b7Fja59EYHRNOTa3aXiuWnhUWXFU2Nfg6h61bLfAb5GS5fX3LMUD0U5t4S8N/4tpHQg3Acs2UVPR9jy2l1g/3A==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.23.0 + + stylelint@16.26.1: + resolution: {integrity: sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw==} + engines: {node: '>=18.12.0'} + hasBin: true + + suf-log@2.5.3: + resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + + svgo@4.0.1: + resolution: {integrity: sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==} + engines: {node: '>=16'} + hasBin: true + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + terser@5.46.1: + resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} + engines: {node: '>=10'} + hasBin: true + + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + tinyclip@0.1.12: + resolution: {integrity: sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA==} + engines: {node: ^16.14.0 || >= 17.3.0} + + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tlds@1.261.0: + resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} + hasBin: true + + to-data-view@1.1.0: + resolution: {integrity: sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typesafe-path@0.2.2: + resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} + + typescript-auto-import-cache@0.3.6: + resolution: {integrity: sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + uint8arrays@3.0.0: + resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==} + + ultrahtml@1.6.0: + resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} + + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + unconfig@7.5.0: + resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==} + + 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==} + + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unifont@0.7.4: + resolution: {integrity: sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-modify-children@4.0.0: + resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==} + + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-select@5.1.0: + resolution: {integrity: sha512-4A5mfokSHG/rNQ4g7gSbdEs+H586xyd24sdJqF1IWamqrLHvYb+DH48fzxowyOhOfK7YSqX+XlCojAyuuyyT2A==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-children@3.0.0: + resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + unstorage@1.17.4: + resolution: {integrity: sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6 || ^7 || ^8 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1 || ^2 || ^3 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.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 + jiti: + optional: true + less: + optional: true + lightningcss: + 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.2: + resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + + volar-service-css@0.0.68: + resolution: {integrity: sha512-lJSMh6f3QzZ1tdLOZOzovLX0xzAadPhx8EKwraDLPxBndLCYfoTvnNuiFFV8FARrpAlW5C0WkH+TstPaCxr00Q==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-emmet@0.0.68: + resolution: {integrity: sha512-nHvixrRQ83EzkQ4G/jFxu9Y4eSsXS/X2cltEPDM+K9qZmIv+Ey1w0tg1+6caSe8TU5Hgw4oSTwNMf/6cQb3LzQ==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-html@0.0.68: + resolution: {integrity: sha512-fru9gsLJxy33xAltXOh4TEdi312HP80hpuKhpYQD4O5hDnkNPEBdcQkpB+gcX0oK0VxRv1UOzcGQEUzWCVHLfA==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-prettier@0.0.68: + resolution: {integrity: sha512-grUmWHkHlebMOd6V8vXs2eNQUw/bJGJMjekh/EPf/p2ZNTK0Uyz7hoBRngcvGfJHMsSXZH8w/dZTForIW/4ihw==} + peerDependencies: + '@volar/language-service': ~2.4.0 + prettier: ^2.2 || ^3.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + prettier: + optional: true + + volar-service-typescript-twoslash-queries@0.0.68: + resolution: {integrity: sha512-NugzXcM0iwuZFLCJg47vI93su5YhTIweQuLmZxvz5ZPTaman16JCvmDZexx2rd5T/75SNuvvZmrTOTNYUsfe5w==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-typescript@0.0.68: + resolution: {integrity: sha512-z7B/7CnJ0+TWWFp/gh2r5/QwMObHNDiQiv4C9pTBNI2Wxuwymd4bjEORzrJ/hJ5Yd5+OzeYK+nFCKevoGEEeKw==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-yaml@0.0.68: + resolution: {integrity: sha512-84XgE02LV0OvTcwfqhcSwVg4of3MLNUWPMArO6Aj8YXqyEVnPu8xTEMY2btKSq37mVAPuaEVASI4e3ptObmqcA==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + vscode-css-languageservice@6.3.10: + resolution: {integrity: sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA==} + + vscode-html-languageservice@5.6.2: + resolution: {integrity: sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg==} + + vscode-json-languageservice@4.1.8: + resolution: {integrity: sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==} + engines: {npm: '>=7.0.0'} + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-nls@5.2.0: + resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==} + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + which-pm-runs@1.1.0: + resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} + engines: {node: '>=4'} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + workbox-core@7.4.0: + resolution: {integrity: sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==} + + workbox-window@7.4.0: + resolution: {integrity: sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + xxhash-wasm@1.1.0: + resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml-language-server@1.19.2: + resolution: {integrity: sha512-9F3myNmJzUN/679jycdMxqtydPSDRAarSj3wPiF7pchEPnO9Dg07Oc+gIYLqXR4L+g+FSEVXXv2+mr54StLFOg==} + hasBin: true + + yaml@2.7.1: + resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} + engines: {node: '>= 14'} + hasBin: true + + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + + yoga-layout@3.2.1: + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@anthropic-ai/sdk@0.80.0(zod@4.3.6)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.3.6 + + '@astro-community/astro-embed-baseline-status@0.2.2': + dependencies: + '@astro-community/astro-embed-utils': 0.2.0 + + '@astro-community/astro-embed-bluesky@0.1.6': + dependencies: + '@atproto/api': 0.13.35 + + '@astro-community/astro-embed-gist@0.1.0': + dependencies: + '@astro-community/astro-embed-utils': 0.2.0 + + '@astro-community/astro-embed-integration@0.11.0(astro@6.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2))': + dependencies: + '@astro-community/astro-embed-bluesky': 0.1.6 + '@astro-community/astro-embed-gist': 0.1.0 + '@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 + '@types/unist': 3.0.3 + astro: 6.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2) + astro-auto-import: 0.4.6(astro@6.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2)) + unist-util-select: 5.1.0 + + '@astro-community/astro-embed-link-preview@0.3.1': + dependencies: + '@astro-community/astro-embed-utils': 0.2.0 + '@parse5/tools': 0.7.0(parse5@8.0.0) + parse5: 8.0.0 + + '@astro-community/astro-embed-mastodon@0.1.1': + dependencies: + '@astro-community/astro-embed-utils': 0.2.0 + + '@astro-community/astro-embed-twitter@0.5.11': + dependencies: + '@astro-community/astro-embed-utils': 0.2.0 + + '@astro-community/astro-embed-utils@0.2.0': {} + + '@astro-community/astro-embed-vimeo@0.3.12': + dependencies: + '@astro-community/astro-embed-utils': 0.2.0 + + '@astro-community/astro-embed-youtube@0.5.10': + dependencies: + lite-youtube-embed: 0.3.4 + + '@astrojs/check@0.9.7(prettier-plugin-astro@0.14.1)(prettier@3.8.1)(typescript@5.9.3)': + dependencies: + '@astrojs/language-server': 2.16.4(prettier-plugin-astro@0.14.1)(prettier@3.8.1)(typescript@5.9.3) + chokidar: 4.0.3 + kleur: 4.1.5 + typescript: 5.9.3 + yargs: 17.7.2 + transitivePeerDependencies: + - prettier + - prettier-plugin-astro + + '@astrojs/compiler@2.13.1': {} + + '@astrojs/compiler@3.0.0': {} + + '@astrojs/internal-helpers@0.8.0': + dependencies: + picomatch: 4.0.3 + + '@astrojs/language-server@2.16.4(prettier-plugin-astro@0.14.1)(prettier@3.8.1)(typescript@5.9.3)': + dependencies: + '@astrojs/compiler': 2.13.1 + '@astrojs/yaml2ts': 0.2.3 + '@jridgewell/sourcemap-codec': 1.5.5 + '@volar/kit': 2.4.28(typescript@5.9.3) + '@volar/language-core': 2.4.28 + '@volar/language-server': 2.4.28 + '@volar/language-service': 2.4.28 + muggle-string: 0.4.1 + tinyglobby: 0.2.15 + volar-service-css: 0.0.68(@volar/language-service@2.4.28) + volar-service-emmet: 0.0.68(@volar/language-service@2.4.28) + volar-service-html: 0.0.68(@volar/language-service@2.4.28) + volar-service-prettier: 0.0.68(@volar/language-service@2.4.28)(prettier@3.8.1) + volar-service-typescript: 0.0.68(@volar/language-service@2.4.28) + volar-service-typescript-twoslash-queries: 0.0.68(@volar/language-service@2.4.28) + volar-service-yaml: 0.0.68(@volar/language-service@2.4.28) + vscode-html-languageservice: 5.6.2 + vscode-uri: 3.1.0 + optionalDependencies: + prettier: 3.8.1 + prettier-plugin-astro: 0.14.1 + transitivePeerDependencies: + - typescript + + '@astrojs/markdown-remark@7.0.0': + dependencies: + '@astrojs/internal-helpers': 0.8.0 + '@astrojs/prism': 4.0.0 + github-slugger: 2.0.0 + hast-util-from-html: 2.0.3 + hast-util-to-text: 4.0.2 + js-yaml: 4.1.1 + mdast-util-definitions: 6.0.0 + rehype-raw: 7.0.0 + rehype-stringify: 10.0.1 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remark-smartypants: 3.0.2 + shiki: 4.0.2 + smol-toml: 1.6.0 + unified: 11.0.5 + unist-util-remove-position: 5.0.0 + unist-util-visit: 5.1.0 + unist-util-visit-parents: 6.0.2 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@astrojs/markdown-remark@7.0.1': + dependencies: + '@astrojs/internal-helpers': 0.8.0 + '@astrojs/prism': 4.0.1 + github-slugger: 2.0.0 + hast-util-from-html: 2.0.3 + hast-util-to-text: 4.0.2 + js-yaml: 4.1.1 + mdast-util-definitions: 6.0.0 + rehype-raw: 7.0.0 + rehype-stringify: 10.0.1 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remark-smartypants: 3.0.2 + shiki: 4.0.2 + smol-toml: 1.6.0 + unified: 11.0.5 + unist-util-remove-position: 5.0.0 + unist-util-visit: 5.1.0 + unist-util-visit-parents: 6.0.2 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@astrojs/mdx@5.0.2(astro@6.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2))': + dependencies: + '@astrojs/markdown-remark': 7.0.1 + '@mdx-js/mdx': 3.1.1 + acorn: 8.16.0 + astro: 6.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2) + es-module-lexer: 2.0.0 + estree-util-visit: 2.0.0 + hast-util-to-html: 9.0.5 + piccolore: 0.1.3 + rehype-raw: 7.0.0 + remark-gfm: 4.0.1 + remark-smartypants: 3.0.2 + source-map: 0.7.6 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@astrojs/node@10.0.2(astro@6.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2))': + dependencies: + '@astrojs/internal-helpers': 0.8.0 + astro: 6.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2) + send: 1.2.1 + server-destroy: 1.0.1 + transitivePeerDependencies: + - supports-color + + '@astrojs/prism@4.0.0': + dependencies: + prismjs: 1.30.0 + + '@astrojs/prism@4.0.1': + dependencies: + prismjs: 1.30.0 + + '@astrojs/rss@4.0.17': + dependencies: + fast-xml-parser: 5.4.1 + piccolore: 0.1.3 + zod: 4.3.6 + + '@astrojs/sitemap@3.7.1': + dependencies: + sitemap: 9.0.1 + stream-replace-string: 2.0.0 + zod: 4.3.6 + + '@astrojs/telemetry@3.3.0': + dependencies: + ci-info: 4.4.0 + debug: 4.4.3 + dlv: 1.1.3 + dset: 3.1.4 + is-docker: 3.0.0 + is-wsl: 3.1.1 + which-pm-runs: 1.1.0 + transitivePeerDependencies: + - supports-color + + '@astrojs/yaml2ts@0.2.3': + dependencies: + yaml: 2.8.2 + + '@atproto/api@0.13.35': + dependencies: + '@atproto/common-web': 0.4.19 + '@atproto/lexicon': 0.4.14 + '@atproto/syntax': 0.3.4 + '@atproto/xrpc': 0.6.12 + await-lock: 2.2.2 + multiformats: 9.9.0 + tlds: 1.261.0 + zod: 3.25.76 + + '@atproto/common-web@0.4.19': + dependencies: + '@atproto/lex-data': 0.0.14 + '@atproto/lex-json': 0.0.14 + '@atproto/syntax': 0.5.1 + zod: 3.25.76 + + '@atproto/lex-data@0.0.14': + dependencies: + multiformats: 9.9.0 + tslib: 2.8.1 + uint8arrays: 3.0.0 + unicode-segmenter: 0.14.5 + + '@atproto/lex-json@0.0.14': + dependencies: + '@atproto/lex-data': 0.0.14 + tslib: 2.8.1 + + '@atproto/lexicon@0.4.14': + dependencies: + '@atproto/common-web': 0.4.19 + '@atproto/syntax': 0.4.3 + iso-datestring-validator: 2.2.2 + multiformats: 9.9.0 + zod: 3.25.76 + + '@atproto/syntax@0.3.4': {} + + '@atproto/syntax@0.4.3': + dependencies: + tslib: 2.8.1 + + '@atproto/syntax@0.5.1': + dependencies: + tslib: 2.8.1 + + '@atproto/xrpc@0.6.12': + dependencies: + '@atproto/lexicon': 0.4.14 + zod: 3.25.76 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/runtime@7.29.2': {} + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@biomejs/biome@2.4.7': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.7 + '@biomejs/cli-darwin-x64': 2.4.7 + '@biomejs/cli-linux-arm64': 2.4.7 + '@biomejs/cli-linux-arm64-musl': 2.4.7 + '@biomejs/cli-linux-x64': 2.4.7 + '@biomejs/cli-linux-x64-musl': 2.4.7 + '@biomejs/cli-win32-arm64': 2.4.7 + '@biomejs/cli-win32-x64': 2.4.7 + + '@biomejs/cli-darwin-arm64@2.4.7': + optional: true + + '@biomejs/cli-darwin-x64@2.4.7': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.7': + optional: true + + '@biomejs/cli-linux-arm64@2.4.7': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.7': + optional: true + + '@biomejs/cli-linux-x64@2.4.7': + optional: true + + '@biomejs/cli-win32-arm64@2.4.7': + optional: true + + '@biomejs/cli-win32-x64@2.4.7': + optional: true + + '@cacheable/memory@2.0.6': + dependencies: + '@cacheable/utils': 2.3.2 + '@keyv/bigmap': 1.3.0(keyv@5.5.4) + hookified: 1.13.0 + keyv: 5.5.4 + + '@cacheable/utils@2.3.2': + dependencies: + hashery: 1.3.0 + keyv: 5.5.4 + + '@canvas/image-data@1.1.0': {} + + '@capsizecss/unpack@4.0.0': + dependencies: + fontkitten: 1.0.3 + + '@clack/core@1.1.0': + dependencies: + sisteransi: 1.0.5 + + '@clack/prompts@1.1.0': + dependencies: + '@clack/core': 1.1.0 + sisteransi: 1.0.5 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.20': {} + + '@csstools/css-tokenizer@3.0.4': {} + + '@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.1)': + dependencies: + postcss-selector-parser: 7.1.1 + + '@dual-bundle/import-meta-resolve@4.2.1': {} + + '@emmetio/abbreviation@2.3.3': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/css-abbreviation@2.1.8': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/css-parser@0.4.1': + dependencies: + '@emmetio/stream-reader': 2.2.0 + '@emmetio/stream-reader-utils': 0.1.0 + + '@emmetio/html-matcher@1.3.0': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/scanner@1.0.4': {} + + '@emmetio/stream-reader-utils@0.1.0': {} + + '@emmetio/stream-reader@2.2.0': {} + + '@emnapi/runtime@1.7.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true + + '@esbuild/linux-arm64@0.27.4': + optional: true + + '@esbuild/linux-arm@0.27.4': + optional: true + + '@esbuild/linux-ia32@0.27.4': + optional: true + + '@esbuild/linux-loong64@0.27.4': + optional: true + + '@esbuild/linux-mips64el@0.27.4': + optional: true + + '@esbuild/linux-ppc64@0.27.4': + optional: true + + '@esbuild/linux-riscv64@0.27.4': + optional: true + + '@esbuild/linux-s390x@0.27.4': + optional: true + + '@esbuild/linux-x64@0.27.4': + optional: true + + '@esbuild/netbsd-arm64@0.27.4': + optional: true + + '@esbuild/netbsd-x64@0.27.4': + optional: true + + '@esbuild/openbsd-arm64@0.27.4': + optional: true + + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': + optional: true + + '@esbuild/sunos-x64@0.27.4': + optional: true + + '@esbuild/win32-arm64@0.27.4': + optional: true + + '@esbuild/win32-ia32@0.27.4': + optional: true + + '@esbuild/win32-x64@0.27.4': + optional: true + + '@fontsource/exo-2@5.2.8': {} + + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.7.0 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.7.0 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + optional: true + + '@jridgewell/resolve-uri@3.1.2': + optional: true + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + optional: true + + '@keyv/bigmap@1.3.0(keyv@5.5.4)': + dependencies: + hashery: 1.3.0 + hookified: 1.13.0 + keyv: 5.5.4 + + '@keyv/serialize@1.1.1': {} + + '@kurkle/color@0.3.4': {} + + '@mdx-js/mdx@3.1.1': + dependencies: + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdx': 2.0.13 + acorn: 8.16.0 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.0 + estree-walker: 3.0.3 + hast-util-to-jsx-runtime: 2.3.6 + markdown-extensions: 2.0.0 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.1(acorn@8.16.0) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + source-map: 0.7.6 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@oslojs/encoding@1.1.0': {} + + '@parse5/tools@0.7.0(parse5@8.0.0)': + dependencies: + parse5: 8.0.0 + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rollup/pluginutils@5.3.0(rollup@4.59.0)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.59.0 + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@shikijs/core@4.0.2': + dependencies: + '@shikijs/primitive': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.5 + + '@shikijs/engine-oniguruma@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + + '@shikijs/primitive@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/themes@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + + '@shikijs/types@4.0.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@shuding/opentype.js@1.4.0-beta.0': + dependencies: + fflate: 0.7.4 + string.prototype.codepointat: 0.2.1 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + + '@types/estree@1.0.8': {} + + '@types/geojson@7946.0.16': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/justified-layout@4.1.4': {} + + '@types/leaflet.markercluster@1.5.6': + dependencies: + '@types/leaflet': 1.9.21 + + '@types/leaflet@1.9.21': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdx@2.0.13': {} + + '@types/ms@2.1.0': {} + + '@types/nlcst@2.0.3': + dependencies: + '@types/unist': 3.0.3 + + '@types/node@24.12.0': + dependencies: + undici-types: 7.16.0 + + '@types/node@25.0.6': + dependencies: + undici-types: 7.16.0 + optional: true + + '@types/sax@1.2.7': + dependencies: + '@types/node': 24.12.0 + + '@types/trusted-types@2.0.7': {} + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.3.0': {} + + '@vite-pwa/assets-generator@1.0.2': + dependencies: + cac: 6.7.14 + colorette: 2.0.20 + consola: 3.4.2 + sharp: 0.33.5 + sharp-ico: 0.1.5 + unconfig: 7.5.0 + + '@volar/kit@2.4.28(typescript@5.9.3)': + dependencies: + '@volar/language-service': 2.4.28 + '@volar/typescript': 2.4.28 + typesafe-path: 0.2.2 + typescript: 5.9.3 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/language-server@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + '@volar/language-service': 2.4.28 + '@volar/typescript': 2.4.28 + path-browserify: 1.0.1 + request-light: 0.7.0 + vscode-languageserver: 9.0.1 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + + '@volar/language-service@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vscode/emmet-helper@2.11.0': + dependencies: + emmet: 2.4.11 + jsonc-parser: 2.3.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.1.0 + + '@vscode/l10n@0.0.18': {} + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv-draft-04@1.0.0(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-iterate@2.0.1: {} + + array-union@2.1.0: {} + + astral-regex@2.0.0: {} + + astring@1.9.0: {} + + astro-auto-import@0.4.6(astro@6.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2)): + dependencies: + acorn: 8.16.0 + astro: 6.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2) + + astro-embed@0.12.0(astro@6.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2)): + dependencies: + '@astro-community/astro-embed-baseline-status': 0.2.2 + '@astro-community/astro-embed-bluesky': 0.1.6 + '@astro-community/astro-embed-gist': 0.1.0 + '@astro-community/astro-embed-integration': 0.11.0(astro@6.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2)) + '@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.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2) + + astro@6.0.4(@types/node@25.0.6)(jiti@2.6.1)(rollup@4.59.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.2): + dependencies: + '@astrojs/compiler': 3.0.0 + '@astrojs/internal-helpers': 0.8.0 + '@astrojs/markdown-remark': 7.0.0 + '@astrojs/telemetry': 3.3.0 + '@capsizecss/unpack': 4.0.0 + '@clack/prompts': 1.1.0 + '@oslojs/encoding': 1.1.0 + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + aria-query: 5.3.2 + axobject-query: 4.1.0 + ci-info: 4.4.0 + clsx: 2.1.1 + common-ancestor-path: 2.0.0 + cookie: 1.1.1 + devalue: 5.6.4 + diff: 8.0.3 + dlv: 1.1.3 + dset: 3.1.4 + es-module-lexer: 2.0.0 + esbuild: 0.27.4 + flattie: 1.1.1 + fontace: 0.4.1 + github-slugger: 2.0.0 + html-escaper: 3.0.3 + http-cache-semantics: 4.2.0 + js-yaml: 4.1.1 + magic-string: 0.30.21 + magicast: 0.5.2 + mrmime: 2.0.1 + neotraverse: 0.6.18 + obug: 2.1.1 + p-limit: 7.3.0 + p-queue: 9.1.0 + package-manager-detector: 1.6.0 + piccolore: 0.1.3 + picomatch: 4.0.3 + rehype: 13.0.2 + semver: 7.7.4 + shiki: 4.0.2 + smol-toml: 1.6.0 + svgo: 4.0.1 + tinyclip: 0.1.12 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + tsconfck: 3.1.6(typescript@5.9.3) + ultrahtml: 1.6.0 + unifont: 0.7.4 + unist-util-visit: 5.1.0 + unstorage: 1.17.4 + vfile: 6.0.3 + vite: 7.3.1(@types/node@25.0.6)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2) + vitefu: 1.1.2(vite@7.3.1(@types/node@25.0.6)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2)) + xxhash-wasm: 1.1.0 + yargs-parser: 22.0.0 + zod: 4.3.6 + optionalDependencies: + sharp: 0.34.5 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - idb-keyval + - ioredis + - jiti + - less + - lightningcss + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - uploadthing + - yaml + + await-lock@2.2.2: {} + + axobject-query@4.1.0: {} + + bail@2.0.2: {} + + balanced-match@2.0.0: {} + + base64-js@0.0.8: {} + + boolbase@1.0.0: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-from@1.1.2: + optional: true + + cac@6.7.14: {} + + cacheable@2.3.0: + dependencies: + '@cacheable/memory': 2.0.6 + '@cacheable/utils': 2.3.2 + hookified: 1.13.0 + keyv: 5.5.4 + qified: 0.5.2 + + callsites@3.1.0: {} + + camelize@1.0.1: {} + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + ci-info@4.4.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + collapse-white-space@2.1.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + colord@2.9.3: {} + + colorette@2.0.20: {} + + comma-separated-tokens@2.0.3: {} + + commander@11.1.0: {} + + commander@2.20.3: + optional: true + + common-ancestor-path@2.0.0: {} + + consola@3.4.2: {} + + cookie-es@1.2.2: {} + + cookie@1.1.1: {} + + cosmiconfig@9.0.0(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + css-background-parser@0.1.0: {} + + css-box-shadow@1.0.0-3: {} + + css-color-keywords@1.0.0: {} + + css-functions-list@3.2.3: {} + + css-gradient-parser@0.0.17: {} + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-selector-parser@3.3.0: {} + + css-to-react-native@3.2.0: + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css-what@6.2.2: {} + + cssesc@3.0.0: {} + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-bmp@0.2.1: + dependencies: + '@canvas/image-data': 1.1.0 + to-data-view: 1.1.0 + + decode-ico@0.4.1: + dependencies: + '@canvas/image-data': 1.1.0 + decode-bmp: 0.2.1 + to-data-view: 1.1.0 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + defu@6.1.4: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + devalue@5.6.4: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@8.0.3: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dlv@1.1.3: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dotenv@17.3.1: {} + + dset@3.1.4: {} + + ee-first@1.1.1: {} + + emmet@2.4.11: + dependencies: + '@emmetio/abbreviation': 2.3.3 + '@emmetio/css-abbreviation': 2.1.8 + + emoji-regex-xs@2.0.1: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-module-lexer@2.0.0: {} + + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 + + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.16.0 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.3 + + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@5.0.0: {} + + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.8 + + estree-util-build-jsx@3.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.3 + + estree-util-is-identifier-name@3.0.0: {} + + estree-util-scope@1.0.0: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + + estree-util-to-js@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.9.0 + source-map: 0.7.6 + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 3.0.3 + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + etag@1.8.1: {} + + eventemitter3@5.0.4: {} + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-uri@3.1.0: {} + + fast-xml-builder@1.1.4: + dependencies: + path-expression-matcher: 1.2.0 + + fast-xml-parser@5.4.1: + dependencies: + fast-xml-builder: 1.1.4 + strnum: 2.2.2 + + fastest-levenshtein@1.0.16: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fflate@0.7.4: {} + + file-entry-cache@11.1.1: + dependencies: + flat-cache: 6.1.19 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + flat-cache@6.1.19: + dependencies: + cacheable: 2.3.0 + flatted: 3.3.3 + hookified: 1.13.0 + + flatted@3.3.3: {} + + flattie@1.1.1: {} + + fontace@0.4.1: + dependencies: + fontkitten: 1.0.3 + + fontkitten@1.0.3: + dependencies: + tiny-inflate: 1.0.3 + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + + github-slugger@2.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globjoin@0.1.4: {} + + h3@1.15.6: + dependencies: + cookie-es: 1.2.2 + crossws: 0.3.5 + defu: 6.1.4 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.4 + radix3: 1.1.2 + ufo: 1.6.3 + uncrypto: 0.1.3 + + has-flag@4.0.0: {} + + hashery@1.3.0: + dependencies: + hookified: 1.13.0 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.3 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.1 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-estree@3.1.3: + dependencies: + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-to-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + + hex-rgb@4.3.0: {} + + hookified@1.13.0: {} + + html-escaper@3.0.3: {} + + html-tags@3.3.1: {} + + html-void-elements@3.0.0: {} + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + http-cache-semantics@4.2.0: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + husky@9.1.7: {} + + ico-endec@0.1.6: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + inline-style-parser@0.2.7: {} + + iron-webcrypto@1.2.1: {} + + is-absolute-url@4.0.1: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.4: {} + + is-decimal@2.0.1: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@2.0.1: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-number@7.0.0: {} + + is-plain-obj@4.1.0: {} + + is-plain-object@5.0.0: {} + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + iso-datestring-validator@2.2.2: {} + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-parse-even-better-errors@2.3.1: {} + + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.2 + ts-algebra: 2.0.0 + + json-schema-traverse@1.0.0: {} + + jsonc-parser@2.3.1: {} + + jsonc-parser@3.3.1: {} + + justified-layout@4.1.0: {} + + keyv@5.5.4: + dependencies: + '@keyv/serialize': 1.1.1 + + kind-of@6.0.3: {} + + kleur@4.1.5: {} + + known-css-properties@0.37.0: {} + + leaflet.markercluster@1.5.3(leaflet@1.9.4): + dependencies: + leaflet: 1.9.4 + + leaflet@1.9.4: {} + + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + + lines-and-columns@1.2.4: {} + + lite-youtube-embed@0.3.4: {} + + lodash.truncate@4.4.2: {} + + lodash@4.17.21: {} + + longest-streak@3.1.0: {} + + lru-cache@11.2.7: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.2: + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + markdown-extensions@2.0.0: {} + + markdown-table@3.0.4: {} + + mathml-tag-names@2.1.3: {} + + mdast-util-definitions@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdn-data@2.0.28: {} + + mdn-data@2.12.2: {} + + mdn-data@2.27.1: {} + + meow@13.2.0: {} + + merge2@1.4.1: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-expression@3.0.1: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-jsx@3.0.2: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-extension-mdx-md@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-mdxjs-esm@3.0.0: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-extension-mdxjs@3.0.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + micromark-extension-mdx-expression: 3.0.1 + micromark-extension-mdx-jsx: 3.0.2 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-mdx-expression@2.0.3: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-events-to-acorn@2.0.3: + dependencies: + '@types/estree': 1.0.8 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + multiformats@9.9.0: {} + + nanoid@3.3.11: {} + + neotraverse@0.6.18: {} + + nlcst-to-string@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + + node-fetch-native@1.6.7: {} + + node-mock-http@1.0.4: {} + + normalize-path@3.0.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + obug@2.1.1: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.3 + + ohash@2.0.11: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.5: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.1.0 + regex-recursion: 6.0.2 + + p-limit@7.3.0: + dependencies: + yocto-queue: 1.2.2 + + p-queue@9.1.0: + dependencies: + eventemitter3: 5.0.4 + p-timeout: 7.0.1 + + p-timeout@7.0.1: {} + + package-manager-detector@1.6.0: {} + + pako@0.2.9: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-css-color@0.2.1: + dependencies: + color-name: 1.1.4 + hex-rgb: 4.3.0 + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-latin@7.0.0: + dependencies: + '@types/nlcst': 2.0.3 + '@types/unist': 3.0.3 + nlcst-to-string: 4.0.0 + unist-util-modify-children: 4.0.0 + unist-util-visit-children: 3.0.0 + vfile: 6.0.3 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + parse5@8.0.0: + dependencies: + entities: 6.0.1 + + path-browserify@1.0.1: {} + + path-expression-matcher@1.2.0: {} + + path-type@4.0.0: {} + + piccolore@0.1.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + postcss-html@1.7.0: + dependencies: + htmlparser2: 8.0.2 + js-tokens: 9.0.1 + postcss: 8.5.8 + postcss-safe-parser: 6.0.0(postcss@8.5.8) + + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@6.0.0(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + + postcss-safe-parser@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prettier-plugin-astro@0.14.1: + dependencies: + '@astrojs/compiler': 2.13.1 + prettier: 3.8.1 + sass-formatter: 0.7.9 + optional: true + + prettier@3.8.1: {} + + prismjs@1.30.0: {} + + property-information@7.1.0: {} + + qified@0.5.2: + dependencies: + hookified: 1.13.0 + + quansync@1.0.0: {} + + queue-microtask@1.2.3: {} + + radix3@1.1.2: {} + + range-parser@1.2.1: {} + + readdirp@4.1.2: {} + + readdirp@5.0.0: {} + + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.8 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 + + recma-jsx@1.0.1(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.8 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.8 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + rehype-external-links@3.0.0: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.3.0 + hast-util-is-element: 3.0.0 + is-absolute-url: 4.0.1 + space-separated-tokens: 2.0.2 + unist-util-visit: 5.1.0 + + rehype-parse@9.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-html: 2.0.3 + unified: 11.0.5 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + hast-util-to-estree: 3.1.3 + transitivePeerDependencies: + - supports-color + + rehype-stringify@10.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + unified: 11.0.5 + + rehype@13.0.2: + dependencies: + '@types/hast': 3.0.4 + rehype-parse: 9.0.1 + rehype-stringify: 10.0.1 + unified: 11.0.5 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-mdx@3.1.1: + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-smartypants@3.0.2: + dependencies: + retext: 9.0.0 + retext-smartypants: 6.2.0 + unified: 11.0.5 + unist-util-visit: 5.1.0 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + request-light@0.5.8: {} + + request-light@0.7.0: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + retext-latin@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + parse-latin: 7.0.0 + unified: 11.0.5 + + retext-smartypants@6.2.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unist-util-visit: 5.1.0 + + retext-stringify@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unified: 11.0.5 + + retext@9.0.0: + dependencies: + '@types/nlcst': 2.0.3 + retext-latin: 4.0.0 + retext-stringify: 4.0.0 + unified: 11.0.5 + + reusify@1.1.0: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + s.color@0.0.15: + optional: true + + sass-formatter@0.7.9: + dependencies: + suf-log: 2.5.3 + optional: true + + satori@0.26.0: + dependencies: + '@shuding/opentype.js': 1.4.0-beta.0 + css-background-parser: 0.1.0 + css-box-shadow: 1.0.0-3 + css-gradient-parser: 0.0.17 + css-to-react-native: 3.2.0 + emoji-regex-xs: 2.0.1 + escape-html: 1.0.3 + linebreak: 1.1.0 + parse-css-color: 0.2.1 + postcss-value-parser: 4.2.0 + yoga-layout: 3.2.1 + + sax@1.5.0: {} + + semver@7.7.3: {} + + semver@7.7.4: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + server-destroy@1.0.1: {} + + setprototypeof@1.2.0: {} + + sharp-ico@0.1.5: + dependencies: + decode-ico: 0.4.1 + ico-endec: 0.1.6 + sharp: 0.34.5 + + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + shiki@4.0.2: + dependencies: + '@shikijs/core': 4.0.2 + '@shikijs/engine-javascript': 4.0.2 + '@shikijs/engine-oniguruma': 4.0.2 + '@shikijs/langs': 4.0.2 + '@shikijs/themes': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + + sisteransi@1.0.5: {} + + sitemap@9.0.1: + dependencies: + '@types/node': 24.12.0 + '@types/sax': 1.2.7 + arg: 5.0.2 + sax: 1.5.0 + + slash@3.0.0: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + smartypants@0.2.2: {} + + smol-toml@1.6.0: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + optional: true + + source-map@0.6.1: + optional: true + + source-map@0.7.6: {} + + space-separated-tokens@2.0.2: {} + + statuses@2.0.2: {} + + stream-replace-string@2.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string.prototype.codepointat@0.2.1: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strnum@2.2.2: {} + + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + + stylelint-config-astro@2.0.0(postcss-html@1.7.0)(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + postcss-html: 1.7.0 + stylelint: 16.26.1(typescript@5.9.3) + typescript: 5.9.3 + + stylelint-config-recommended@17.0.0(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + + stylelint-config-standard@39.0.1(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended: 17.0.0(stylelint@16.26.1(typescript@5.9.3)) + + stylelint@16.26.1(typescript@5.9.3): + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-syntax-patches-for-csstree': 1.0.20 + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1) + '@dual-bundle/import-meta-resolve': 4.2.1 + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 9.0.0(typescript@5.9.3) + css-functions-list: 3.2.3 + css-tree: 3.1.0 + debug: 4.4.3 + fast-glob: 3.3.3 + fastest-levenshtein: 1.0.16 + file-entry-cache: 11.1.1 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 7.0.5 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.37.0 + mathml-tag-names: 2.1.3 + meow: 13.2.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-resolve-nested-selector: 0.1.6 + postcss-safe-parser: 7.0.1(postcss@8.5.6) + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + supports-hyperlinks: 3.2.0 + svg-tags: 1.0.0 + table: 6.9.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + - typescript + + suf-log@2.5.3: + dependencies: + s.color: 0.0.15 + optional: true + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + svg-tags@1.0.0: {} + + svgo@4.0.1: + dependencies: + commander: 11.1.0 + css-select: 5.2.2 + css-tree: 3.2.1 + css-what: 6.2.2 + csso: 5.0.5 + picocolors: 1.1.1 + sax: 1.5.0 + + table@6.9.0: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + terser@5.46.1: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + optional: true + + tiny-inflate@1.0.3: {} + + tinyclip@0.1.12: {} + + tinyexec@1.0.4: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tlds@1.261.0: {} + + to-data-view@1.1.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-algebra@2.0.0: {} + + tsconfck@3.1.6(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + tslib@2.8.1: {} + + typesafe-path@0.2.2: {} + + typescript-auto-import-cache@0.3.6: + dependencies: + semver: 7.7.4 + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + uint8arrays@3.0.0: + dependencies: + multiformats: 9.9.0 + + ultrahtml@1.6.0: {} + + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + unconfig@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + defu: 6.1.4 + jiti: 2.6.1 + quansync: 1.0.0 + unconfig-core: 7.5.0 + + uncrypto@0.1.3: {} + + undici-types@7.16.0: {} + + unicode-segmenter@0.14.5: {} + + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unifont@0.7.4: + dependencies: + css-tree: 3.2.1 + ofetch: 1.5.1 + ohash: 2.0.11 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-modify-children@4.0.0: + dependencies: + '@types/unist': 3.0.3 + array-iterate: 2.0.1 + + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + + unist-util-select@5.1.0: + dependencies: + '@types/unist': 3.0.3 + css-selector-parser: 3.3.0 + devlop: 1.1.0 + nth-check: 2.1.1 + zwitch: 2.0.4 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-children@3.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + unstorage@1.17.4: + dependencies: + anymatch: 3.1.3 + chokidar: 5.0.0 + destr: 2.0.5 + h3: 1.15.6 + lru-cache: 11.2.7 + node-fetch-native: 1.6.7 + ofetch: 1.5.1 + ufo: 1.6.3 + + util-deprecate@1.0.2: {} + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@7.3.1(@types/node@25.0.6)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2): + dependencies: + esbuild: 0.27.4 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.0.6 + fsevents: 2.3.3 + jiti: 2.6.1 + terser: 5.46.1 + yaml: 2.8.2 + + vitefu@1.1.2(vite@7.3.1(@types/node@25.0.6)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2)): + optionalDependencies: + vite: 7.3.1(@types/node@25.0.6)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2) + + volar-service-css@0.0.68(@volar/language-service@2.4.28): + dependencies: + vscode-css-languageservice: 6.3.10 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + optionalDependencies: + '@volar/language-service': 2.4.28 + + volar-service-emmet@0.0.68(@volar/language-service@2.4.28): + dependencies: + '@emmetio/css-parser': 0.4.1 + '@emmetio/html-matcher': 1.3.0 + '@vscode/emmet-helper': 2.11.0 + vscode-uri: 3.1.0 + optionalDependencies: + '@volar/language-service': 2.4.28 + + volar-service-html@0.0.68(@volar/language-service@2.4.28): + dependencies: + vscode-html-languageservice: 5.6.2 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + optionalDependencies: + '@volar/language-service': 2.4.28 + + volar-service-prettier@0.0.68(@volar/language-service@2.4.28)(prettier@3.8.1): + dependencies: + vscode-uri: 3.1.0 + optionalDependencies: + '@volar/language-service': 2.4.28 + prettier: 3.8.1 + + volar-service-typescript-twoslash-queries@0.0.68(@volar/language-service@2.4.28): + dependencies: + vscode-uri: 3.1.0 + optionalDependencies: + '@volar/language-service': 2.4.28 + + volar-service-typescript@0.0.68(@volar/language-service@2.4.28): + dependencies: + path-browserify: 1.0.1 + semver: 7.7.4 + typescript-auto-import-cache: 0.3.6 + vscode-languageserver-textdocument: 1.0.12 + vscode-nls: 5.2.0 + vscode-uri: 3.1.0 + optionalDependencies: + '@volar/language-service': 2.4.28 + + volar-service-yaml@0.0.68(@volar/language-service@2.4.28): + dependencies: + vscode-uri: 3.1.0 + yaml-language-server: 1.19.2 + optionalDependencies: + '@volar/language-service': 2.4.28 + + vscode-css-languageservice@6.3.10: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.1.0 + + vscode-html-languageservice@5.6.2: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.1.0 + + vscode-json-languageservice@4.1.8: + dependencies: + jsonc-parser: 3.3.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-nls: 5.2.0 + vscode-uri: 3.1.0 + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-nls@5.2.0: {} + + vscode-uri@3.1.0: {} + + web-namespaces@2.0.1: {} + + which-pm-runs@1.1.0: {} + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + workbox-core@7.4.0: {} + + workbox-window@7.4.0: + dependencies: + '@types/trusted-types': 2.0.7 + workbox-core: 7.4.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + xxhash-wasm@1.1.0: {} + + y18n@5.0.8: {} + + yaml-language-server@1.19.2: + dependencies: + '@vscode/l10n': 0.0.18 + ajv: 8.18.0 + ajv-draft-04: 1.0.0(ajv@8.18.0) + lodash: 4.17.21 + prettier: 3.8.1 + request-light: 0.5.8 + vscode-json-languageservice: 4.1.8 + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.1.0 + yaml: 2.7.1 + + yaml@2.7.1: {} + + yaml@2.8.2: {} + + yargs-parser@21.1.1: {} + + yargs-parser@22.0.0: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@1.2.2: {} + + yoga-layout@3.2.1: {} + + zod@3.25.76: {} + + zod@4.3.6: {} + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..d0b7dbe --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +onlyBuiltDependencies: + - esbuild + - sharp diff --git a/public/apple-touch-icon-180x180.png b/public/apple-touch-icon-180x180.png new file mode 100644 index 0000000..45949c0 Binary files /dev/null and b/public/apple-touch-icon-180x180.png differ diff --git a/public/avatar.jpg b/public/avatar.jpg new file mode 100644 index 0000000..7b470e0 Binary files /dev/null and b/public/avatar.jpg differ diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png new file mode 100644 index 0000000..c6231d3 Binary files /dev/null and b/public/favicon-16x16.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 0000000..a2db27e Binary files /dev/null and b/public/favicon-32x32.png differ diff --git a/public/fonts/Exo2-Italic-VariableFont_wght.ttf b/public/fonts/Exo2-Italic-VariableFont_wght.ttf new file mode 100644 index 0000000..ca98a99 Binary files /dev/null and b/public/fonts/Exo2-Italic-VariableFont_wght.ttf differ diff --git a/public/fonts/Exo2-VariableFont_wght.ttf b/public/fonts/Exo2-VariableFont_wght.ttf new file mode 100644 index 0000000..2170b15 Binary files /dev/null and b/public/fonts/Exo2-VariableFont_wght.ttf differ diff --git a/public/fonts/JetBrainsMono-Italic-VariableFont_wght.ttf b/public/fonts/JetBrainsMono-Italic-VariableFont_wght.ttf new file mode 100644 index 0000000..ecb5f73 Binary files /dev/null and b/public/fonts/JetBrainsMono-Italic-VariableFont_wght.ttf differ diff --git a/public/fonts/JetBrainsMono-VariableFont_wght.ttf b/public/fonts/JetBrainsMono-VariableFont_wght.ttf new file mode 100644 index 0000000..4c96e79 Binary files /dev/null and b/public/fonts/JetBrainsMono-VariableFont_wght.ttf differ diff --git a/pwa-assets.config.ts b/pwa-assets.config.ts new file mode 100644 index 0000000..04e07ec --- /dev/null +++ b/pwa-assets.config.ts @@ -0,0 +1,9 @@ +import { + defineConfig, + minimal2023Preset, +} from "@vite-pwa/assets-generator/config"; + +export default defineConfig({ + preset: minimal2023Preset, + images: ["public/favicon.svg"], +}); diff --git a/scripts/copy-sw.js b/scripts/copy-sw.js new file mode 100644 index 0000000..a656a86 --- /dev/null +++ b/scripts/copy-sw.js @@ -0,0 +1,17 @@ +// Copies sw.js + workbox-*.js from dist/server/ to dist/client/ after build. +// @astrojs/node standalone only serves static files from dist/client/, but +// @vite-pwa/astro generates the service worker into dist/server/ during the +// SSR Vite build pass. +import { copyFile, readdir } from "node:fs/promises"; +import { join } from "node:path"; + +const serverDir = "dist/server"; +const clientDir = "dist/client"; + +const files = await readdir(serverDir).catch(() => []); +for (const file of files) { + if (file === "sw.js" || file.startsWith("workbox-")) { + await copyFile(join(serverDir, file), join(clientDir, file)); + console.log(`[copy-sw] ${file} → dist/client/`); + } +} diff --git a/scripts/new-note-mdx-prompt.sh b/scripts/new-note-mdx-prompt.sh new file mode 100755 index 0000000..82ee6aa --- /dev/null +++ b/scripts/new-note-mdx-prompt.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Standalone wrapper für Obsidian Script Runner — neue Note mit Cover-Bild (MDX) +set -euo pipefail + +VAULT='/Users/adrian/Obsidian/Web/adrian-altner-com' + +TITLE=$(osascript \ + -e 'Tell application "System Events" to display dialog "Note title (with cover):" default answer ""' \ + -e 'text returned of result' 2>/dev/null) || exit 0 + +if [[ -z "$TITLE" ]]; then exit 0; fi + +SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ \+/-/g' | sed 's/^-\+//;s/-\+$//') +DATE_FOLDER=$(date +%Y/%m/%d) +PUBLISH_DATE=$(date +%Y-%m-%d) +DIR="$VAULT/content/notes/$DATE_FOLDER" +FILE="$DIR/$SLUG.mdx" + +mkdir -p "$DIR" + +if [[ -f "$FILE" ]]; then + osascript -e "display notification \"File already exists: $SLUG.mdx\" with title \"New Note\"" 2>/dev/null || true + exit 1 +fi + +cat > "$FILE" << EOF +--- +title: "$TITLE" +publishDate: $PUBLISH_DATE +description: "" +cover: "./$SLUG.jpg" +coverAlt: "" +tags: [] +draft: false +syndication: +--- +EOF + +echo "Created: $FILE" diff --git a/scripts/new-note-prompt.sh b/scripts/new-note-prompt.sh new file mode 100755 index 0000000..6f65d7b --- /dev/null +++ b/scripts/new-note-prompt.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Standalone wrapper für Obsidian Script Runner — neue Note anlegen +set -euo pipefail + +VAULT='/Users/adrian/Obsidian/Web/adrian-altner-com' + +TITLE=$(osascript \ + -e 'Tell application "System Events" to display dialog "Note title:" default answer ""' \ + -e 'text returned of result' 2>/dev/null) || exit 0 + +if [[ -z "$TITLE" ]]; then exit 0; fi + +SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ \+/-/g' | sed 's/^-\+//;s/-\+$//') +DATE_FOLDER=$(date +%Y/%m/%d) +PUBLISH_DATE=$(date +%Y-%m-%d) +DIR="$VAULT/content/notes/$DATE_FOLDER" +FILE="$DIR/$SLUG.md" + +mkdir -p "$DIR" + +if [[ -f "$FILE" ]]; then + osascript -e "display notification \"File already exists: $SLUG.md\" with title \"New Note\"" 2>/dev/null || true + exit 1 +fi + +cat > "$FILE" << EOF +--- +title: "$TITLE" +publishDate: $PUBLISH_DATE +description: "" +tags: [] +draft: false +syndication: +--- +EOF + +echo "Created: $FILE" diff --git a/scripts/new-note.sh b/scripts/new-note.sh new file mode 100755 index 0000000..c892390 --- /dev/null +++ b/scripts/new-note.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Usage: new-note.sh "Note Title" [--mdx] +# Creates a new note in the Obsidian vault with correct frontmatter and folder structure. +# Use --mdx for notes that need a cover image or custom components (creates .mdx file). +set -euo pipefail + +VAULT='/Users/adrian/Obsidian/Web/adrian-altner-com' + +if [[ -z "${1:-}" ]]; then + echo "Usage: new-note.sh \"Note Title\" [--mdx]" >&2 + exit 1 +fi + +TITLE="$1" +MDX=false +if [[ "${2:-}" == "--mdx" ]]; then + MDX=true +fi + +SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ \+/-/g' | sed 's/^-\+//;s/-\+$//') +DATE_FOLDER=$(date +%Y/%m/%d) +PUBLISH_DATE=$(date +%Y-%m-%d) +DIR="$VAULT/content/notes/$DATE_FOLDER" + +if $MDX; then + EXT="mdx" +else + EXT="md" +fi + +FILE="$DIR/$SLUG.$EXT" + +mkdir -p "$DIR" + +if [[ -f "$FILE" ]]; then + echo "File already exists: $FILE" >&2 + exit 1 +fi + +if $MDX; then + cat > "$FILE" << EOF +--- +title: "$TITLE" +publishDate: $PUBLISH_DATE +description: "" +cover: "./$SLUG.jpg" +coverAlt: "" +tags: [] +draft: false +syndication: +--- +EOF +else + cat > "$FILE" << EOF +--- +title: "$TITLE" +publishDate: $PUBLISH_DATE +description: "" +tags: [] +draft: false +syndication: +--- +EOF +fi + +echo "Created: $FILE" diff --git a/scripts/new-post-prompt.sh b/scripts/new-post-prompt.sh new file mode 100755 index 0000000..76411d6 --- /dev/null +++ b/scripts/new-post-prompt.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Standalone wrapper für Obsidian Script Runner — neuen Blog-Post anlegen +set -euo pipefail + +VAULT='/Users/adrian/Obsidian/Web/adrian-altner-com' + +TITLE=$(osascript \ + -e 'Tell application "System Events" to display dialog "Post title:" default answer ""' \ + -e 'text returned of result' 2>/dev/null) || exit 0 + +if [[ -z "$TITLE" ]]; then exit 0; fi + +SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ \+/-/g' | sed 's/^-\+//;s/-\+$//') +DATE_FOLDER=$(date +%Y/%m/%d) +PUBLISH_DATE=$(date +%Y-%m-%dT%H:%M:%S%z) +DIR="$VAULT/content/blog/posts/$DATE_FOLDER" +FILE="$DIR/$SLUG.md" + +mkdir -p "$DIR" + +if [[ -f "$FILE" ]]; then + osascript -e "display notification \"File already exists: $SLUG.md\" with title \"New Post\"" 2>/dev/null || true + exit 1 +fi + +cat > "$FILE" << EOF +--- +title: "$TITLE" +description: "" +publishDate: $PUBLISH_DATE +tags: [] +category: general +draft: true +syndication: +--- +EOF + +echo "Created: $FILE" diff --git a/scripts/new-post.sh b/scripts/new-post.sh new file mode 100755 index 0000000..910638c --- /dev/null +++ b/scripts/new-post.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Usage: new-post.sh "Post Title" +# Creates a new blog post in the Obsidian vault with correct frontmatter and folder structure. +set -euo pipefail + +VAULT='/Users/adrian/Obsidian/Web/adrian-altner-com' + +if [[ -z "${1:-}" ]]; then + echo "Usage: new-post.sh \"Post Title\"" >&2 + exit 1 +fi + +TITLE="$1" +SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ \+/-/g' | sed 's/^-\+//;s/-\+$//') +DATE_FOLDER=$(date +%Y/%m/%d) +PUBLISH_DATE=$(date +%Y-%m-%dT%H:%M:%S%z) +DIR="$VAULT/content/blog/posts/$DATE_FOLDER" +FILE="$DIR/$SLUG.md" + +mkdir -p "$DIR" + +if [[ -f "$FILE" ]]; then + echo "File already exists: $FILE" >&2 + exit 1 +fi + +cat > "$FILE" << EOF +--- +title: "$TITLE" +description: "" +publishDate: $PUBLISH_DATE +tags: [] +category: general +draft: true +syndication: +--- +EOF + +echo "Created: $FILE" diff --git a/scripts/publish-all.sh b/scripts/publish-all.sh new file mode 100755 index 0000000..c4ef86e --- /dev/null +++ b/scripts/publish-all.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +VAULT_BLOG='/Users/adrian/Obsidian/Web/adrian-altner-com/content/blog' +VAULT_PHOTOS='/Users/adrian/Obsidian/Web/adrian-altner-com/content/photos' +VPS="${1:-hetzner}" +REMOTE_BRANCH="${2:-main}" + +REMOTE_BASE='/opt/websites/www.adrian-altner.com' +REMOTE_BLOG="${REMOTE_BASE}/src/content/blog" +REMOTE_PHOTOS="${REMOTE_BASE}/src/content/photos" + +# --- 1. Sync vault to VPS --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + git fetch --prune origin '$REMOTE_BRANCH' + git checkout '$REMOTE_BRANCH' + git reset --hard 'origin/$REMOTE_BRANCH' + git clean -fd -e .env -e .env.production + mkdir -p '$REMOTE_BLOG' + mkdir -p '$REMOTE_PHOTOS' +" + +rsync -az --delete \ + --include='*/' \ + --include='*.md' \ + --include='*.mdx' \ + --exclude='*' \ + "$VAULT_BLOG/" "$VPS:$REMOTE_BLOG/" + +rsync -az --delete \ + --include='*/' \ + --include='*.md' \ + --include='*.mdx' \ + --include='*.jpg' \ + --include='*.jpeg' \ + --include='*.JPG' \ + --include='*.JPEG' \ + --include='*.json' \ + --exclude='.DS_Store' \ + --exclude='*' \ + "$VAULT_PHOTOS/" "$VPS:$REMOTE_PHOTOS/" + +# --- 2. Build + cleanup --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + podman-compose -f compose.yml up --build -d --force-recreate + podman image prune -af + podman builder prune -af +" + +echo "Redeploy done via $VPS (branch: $REMOTE_BRANCH)." + +# --- 3. Webmentions --- +WEBMENTION_APP_TOKEN="$(ssh "$VPS" "grep '^WEBMENTION_APP_TOKEN=' '$REMOTE_BASE/.env.production' | cut -d= -f2-" 2>/dev/null || true)" +if [[ -n "$WEBMENTION_APP_TOKEN" ]]; then + echo "Sending webmentions via webmention.app..." + for feed in rss/blog.xml rss/notes.xml rss/links.xml rss/photos.xml; do + curl -s -X POST "https://webmention.app/check?url=https://adrian-altner.com/${feed}&token=${WEBMENTION_APP_TOKEN}" \ + | grep -o '"status":"[^"]*"' || true + done + echo "Webmentions triggered." +else + echo "No WEBMENTION_APP_TOKEN in .env.production — skipping webmentions." +fi diff --git a/scripts/publish-blog.sh b/scripts/publish-blog.sh new file mode 100755 index 0000000..d539682 --- /dev/null +++ b/scripts/publish-blog.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Usage: publish-blog.sh [vps-host] [branch] +# Can be called from any directory — no dependency on the repo being the working dir. +set -euo pipefail + +VAULT_BLOG='/Users/adrian/Obsidian/Web/adrian-altner-com/content/blog' +VPS="${1:-hetzner}" +REMOTE_BRANCH="${2:-main}" + +REMOTE_BASE='/opt/websites/www.adrian-altner.com' +REMOTE_BLOG="${REMOTE_BASE}/src/content/blog" + +# --- 1. Sync vault to VPS --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + git fetch --prune origin '$REMOTE_BRANCH' + git checkout '$REMOTE_BRANCH' + git reset --hard 'origin/$REMOTE_BRANCH' + git clean -fd -e .env -e .env.production + mkdir -p '$REMOTE_BLOG' +" + +rsync -az --delete \ + --include='*/' \ + --include='*.md' \ + --include='*.mdx' \ + --include='*.jpg' \ + --include='*.jpeg' \ + --include='*.JPG' \ + --include='*.JPEG' \ + --include='*.png' \ + --include='*.PNG' \ + --include='*.webp' \ + --include='*.gif' \ + --exclude='.DS_Store' \ + --exclude='*' \ + "$VAULT_BLOG/" "$VPS:$REMOTE_BLOG/" + +# --- 2. Build + cleanup --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + + podman-compose -f compose.yml up --build -d --force-recreate + + podman image prune -af + podman builder prune -af +" + +echo "Blog deploy done via $VPS (branch: $REMOTE_BRANCH)." + +# --- 3. Webmentions --- +WEBMENTION_APP_TOKEN="$(ssh "$VPS" "grep '^WEBMENTION_APP_TOKEN=' '$REMOTE_BASE/.env.production' | cut -d= -f2-" 2>/dev/null || true)" +if [[ -n "$WEBMENTION_APP_TOKEN" ]]; then + echo "Sending webmentions..." + curl -s -X POST "https://webmention.app/check?url=https://adrian-altner.com/rss/blog.xml&token=${WEBMENTION_APP_TOKEN}" \ + | grep -o '"status":"[^"]*"' || true + echo "Webmentions triggered." +else + echo "No WEBMENTION_APP_TOKEN in .env.production — skipping webmentions." +fi diff --git a/scripts/publish-links.sh b/scripts/publish-links.sh new file mode 100755 index 0000000..2ccb721 --- /dev/null +++ b/scripts/publish-links.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Usage: publish-links.sh [vps-host] [branch] +# Can be called from any directory — no dependency on the repo being the working dir. +set -euo pipefail + +VAULT_LINKS='/Users/adrian/Obsidian/Web/adrian-altner-com/content/links' +VPS="${1:-hetzner}" +REMOTE_BRANCH="${2:-main}" + +REMOTE_BASE='/opt/websites/www.adrian-altner.com' +REMOTE_LINKS="${REMOTE_BASE}/src/content/links" + +# --- 1. Sync vault to VPS --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + git fetch --prune origin '$REMOTE_BRANCH' + git checkout '$REMOTE_BRANCH' + git reset --hard 'origin/$REMOTE_BRANCH' + git clean -fd -e .env -e .env.production + mkdir -p '$REMOTE_LINKS' +" + +rsync -az --delete \ + --include='*/' \ + --include='*.md' \ + --include='*.mdx' \ + --exclude='.DS_Store' \ + --exclude='*' \ + "$VAULT_LINKS/" "$VPS:$REMOTE_LINKS/" + +# --- 2. Build + cleanup --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + podman-compose -f compose.yml up --build -d --force-recreate + podman image prune -af + podman builder prune -af +" + +echo "Links deploy done via $VPS (branch: $REMOTE_BRANCH)." + +# --- 3. Webmentions --- +WEBMENTION_APP_TOKEN="$(ssh "$VPS" "grep '^WEBMENTION_APP_TOKEN=' '$REMOTE_BASE/.env.production' | cut -d= -f2-" 2>/dev/null || true)" +if [[ -n "$WEBMENTION_APP_TOKEN" ]]; then + echo "Sending webmentions..." + curl -s -X POST "https://webmention.app/check?url=https://adrian-altner.com/rss/links.xml&token=${WEBMENTION_APP_TOKEN}" \ + | grep -o '"status":"[^"]*"' || true + echo "Webmentions triggered." +else + echo "No WEBMENTION_APP_TOKEN in .env.production — skipping webmentions." +fi diff --git a/scripts/publish-notes.sh b/scripts/publish-notes.sh new file mode 100755 index 0000000..1c87748 --- /dev/null +++ b/scripts/publish-notes.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Usage: publish-notes.sh [vps-host] [branch] +# Can be called from any directory — no dependency on the repo being the working dir. +set -euo pipefail + +VAULT_NOTES='/Users/adrian/Obsidian/Web/adrian-altner-com/content/notes' +VPS="${1:-hetzner}" +REMOTE_BRANCH="${2:-main}" + +REMOTE_BASE='/opt/websites/www.adrian-altner.com' +REMOTE_NOTES="${REMOTE_BASE}/src/content/notes" + +# --- 1. Sync vault to VPS --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + git fetch --prune origin '$REMOTE_BRANCH' + git checkout '$REMOTE_BRANCH' + git reset --hard 'origin/$REMOTE_BRANCH' + git clean -fd -e .env -e .env.production + mkdir -p '$REMOTE_NOTES' +" + +rsync -az --delete \ + --include='*/' \ + --include='*.md' \ + --include='*.mdx' \ + --include='*.jpg' \ + --include='*.jpeg' \ + --include='*.JPG' \ + --include='*.JPEG' \ + --exclude='.DS_Store' \ + --exclude='*' \ + "$VAULT_NOTES/" "$VPS:$REMOTE_NOTES/" + +# --- 2. Build + cleanup --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + + podman-compose -f compose.yml up --build -d --force-recreate + + podman image prune -af + podman builder prune -af +" + +echo "Notes deploy done via $VPS (branch: $REMOTE_BRANCH)." + +# --- 3. Webmentions --- +WEBMENTION_APP_TOKEN="$(ssh "$VPS" "grep '^WEBMENTION_APP_TOKEN=' '$REMOTE_BASE/.env.production' | cut -d= -f2-" 2>/dev/null || true)" +if [[ -n "$WEBMENTION_APP_TOKEN" ]]; then + echo "Sending webmentions..." + curl -s -X POST "https://webmention.app/check?url=https://adrian-altner.com/rss/notes.xml&token=${WEBMENTION_APP_TOKEN}" \ + | grep -o '"status":"[^"]*"' || true + echo "Webmentions triggered." +else + echo "No WEBMENTION_APP_TOKEN in .env.production — skipping webmentions." +fi diff --git a/scripts/publish-photos.sh b/scripts/publish-photos.sh new file mode 100755 index 0000000..513682d --- /dev/null +++ b/scripts/publish-photos.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Usage: publish-photos.sh [vps-host] [branch] +# Can be called from any directory — no dependency on the repo being the working dir. +set -euo pipefail + +VAULT_PHOTOS='/Users/adrian/Obsidian/Web/adrian-altner-com/content/photos' +VPS="${1:-hetzner}" +REMOTE_BRANCH="${2:-main}" + +REMOTE_BASE='/opt/websites/www.adrian-altner.com' +REMOTE_PHOTOS="${REMOTE_BASE}/src/content/photos" + +# --- 1. Sync vault to VPS --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + git fetch --prune origin '$REMOTE_BRANCH' + git checkout '$REMOTE_BRANCH' + git reset --hard 'origin/$REMOTE_BRANCH' + git clean -fd -e .env -e .env.production + mkdir -p '$REMOTE_PHOTOS' +" + +rsync -az --delete \ + --include='*/' \ + --include='*.md' \ + --include='*.mdx' \ + --include='*.jpg' \ + --include='*.jpeg' \ + --include='*.JPG' \ + --include='*.JPEG' \ + --include='*.json' \ + --exclude='.DS_Store' \ + --exclude='*' \ + "$VAULT_PHOTOS/" "$VPS:$REMOTE_PHOTOS/" + +# --- 2. Build + cleanup --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + podman-compose -f compose.yml up --build -d --force-recreate + podman image prune -af + podman builder prune -af +" + +echo "Photos deploy done via $VPS (branch: $REMOTE_BRANCH)." + +# --- 3. Webmentions --- +WEBMENTION_APP_TOKEN="$(ssh "$VPS" "grep '^WEBMENTION_APP_TOKEN=' '$REMOTE_BASE/.env.production' | cut -d= -f2-" 2>/dev/null || true)" +if [[ -n "$WEBMENTION_APP_TOKEN" ]]; then + echo "Sending webmentions..." + curl -s -X POST "https://webmention.app/check?url=https://adrian-altner.com/rss/photos.xml&token=${WEBMENTION_APP_TOKEN}" \ + | grep -o '"status":"[^"]*"' || true + echo "Webmentions triggered." +else + echo "No WEBMENTION_APP_TOKEN in .env.production — skipping webmentions." +fi diff --git a/scripts/publish-projects.sh b/scripts/publish-projects.sh new file mode 100755 index 0000000..094623a --- /dev/null +++ b/scripts/publish-projects.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Usage: publish-projects.sh [vps-host] [branch] +# Can be called from any directory — no dependency on the repo being the working dir. +set -euo pipefail + +VAULT_PROJECTS='/Users/adrian/Obsidian/Web/adrian-altner-com/content/projects' +VPS="${1:-hetzner}" +REMOTE_BRANCH="${2:-main}" + +REMOTE_BASE='/opt/websites/www.adrian-altner.com' +REMOTE_PROJECTS="${REMOTE_BASE}/src/content/projects" + +# --- 1. Sync vault to VPS --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + git fetch --prune origin '$REMOTE_BRANCH' + git checkout '$REMOTE_BRANCH' + git reset --hard 'origin/$REMOTE_BRANCH' + git clean -fd -e .env -e .env.production + mkdir -p '$REMOTE_PROJECTS' +" + +rsync -az --delete \ + --include='*/' \ + --include='*.md' \ + --include='*.mdx' \ + --include='*.jpg' \ + --include='*.jpeg' \ + --include='*.JPG' \ + --include='*.JPEG' \ + --include='*.png' \ + --include='*.PNG' \ + --include='*.webp' \ + --include='*.gif' \ + --exclude='.DS_Store' \ + --exclude='*' \ + "$VAULT_PROJECTS/" "$VPS:$REMOTE_PROJECTS/" + +# --- 2. Build + cleanup --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + podman-compose -f compose.yml up --build -d --force-recreate + podman image prune -af + podman builder prune -af +" + +echo "Projects deploy done via $VPS (branch: $REMOTE_BRANCH)." diff --git a/scripts/squash-history.sh b/scripts/squash-history.sh new file mode 100755 index 0000000..d5c5264 --- /dev/null +++ b/scripts/squash-history.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# squash-history.sh — Replaces entire git history with a single "init" commit. +# WARNING: Destructive and irreversible. Force-pushes to remote. + +set -euo pipefail + +COMMIT_MSG="${1:-init}" +REMOTE="${2:-origin}" +BRANCH="main" +TEMP="temp-squash-$$" + +echo "⚠️ This will destroy all git history and force-push to $REMOTE/$BRANCH." +read -r -p "Continue? [y/N] " confirm +[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; } + +git checkout --orphan "$TEMP" +git add -A +git commit -m "$COMMIT_MSG" +git branch -D "$BRANCH" +git branch -m "$TEMP" "$BRANCH" +git push --force "$REMOTE" "$BRANCH" + +echo "Done. $(git log --oneline)" diff --git a/scripts/vision.spec.ts b/scripts/vision.spec.ts new file mode 100644 index 0000000..d76d9bf --- /dev/null +++ b/scripts/vision.spec.ts @@ -0,0 +1,87 @@ +import assert from "node:assert/strict"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { + ExifMetadata, + ImageMetadataSuggestion, + VisionAIResult, +} from "./vision.ts"; +import { getImagesToProcess, mergeMetaAndVisionData } from "./vision.ts"; + +const FINAL: ImageMetadataSuggestion = { + id: "2R9A2805", + title: [ + "Blossom and Buzz", + "Spring's Gentle Awakening", + "Cherry Blossom Haven", + "Nature's Delicate Balance", + "A Bee's Spring Feast", + ], + image: "./2R9A2805.jpg", + alt: "Close-up of vibrant pink cherry blossoms on a branch with a honeybee collecting nectar. The bee's wings are slightly blurred, capturing its motion as it works. The background is a soft, dreamy pink hue, complementing the sharp details of the blossoms and the bee.", + location: "48 deg 8' 37.56\" N, 11 deg 34' 13.32\" E", + date: "2024-03-17", + tags: ["nature", "cherryblossom", "bee", "spring", "floral"], + exif: { + camera: "Canon EOS R6m2", + lens: "RF70-200mm F2.8 L IS USM", + aperture: "2.8", + iso: "125", + focal_length: "200.0", + shutter_speed: "1/1000", + }, +}; + +const VISION_DATA: VisionAIResult = { + title_ideas: [ + "Blossom and Buzz", + "Spring's Gentle Awakening", + "Cherry Blossom Haven", + "Nature's Delicate Balance", + "A Bee's Spring Feast", + ], + description: + "Close-up of vibrant pink cherry blossoms on a branch with a honeybee collecting nectar. The bee's wings are slightly blurred, capturing its motion as it works. The background is a soft, dreamy pink hue, complementing the sharp details of the blossoms and the bee.", + tags: ["nature", "cherryblossom", "bee", "spring", "floral"], +}; + +const EXIF_DATA: ExifMetadata = { + SourceFile: "/Users/flori/Sites/flori-dev/src/content/grid/2R9A2805.jpg", + FileName: "2R9A2805.jpg", + Model: "Canon EOS R6m2", + ExposureTime: "1/1000", + FNumber: 2.8, + ISO: 125, + DateTimeOriginal: "2024:03:17 15:06:16", + FocalLength: "200.0 mm", + LensModel: "RF70-200mm F2.8 L IS USM", + GPSPosition: "48 deg 8' 37.56\" N, 11 deg 34' 13.32\" E", +}; + +async function main() { + const tempRoot = await mkdtemp(join(tmpdir(), "vision-photos-")); + + try { + assert.deepEqual(mergeMetaAndVisionData(EXIF_DATA, VISION_DATA), FINAL); + + const albumDirectory = join(tempRoot, "chiang-mai"); + const missingImage = join(albumDirectory, "2025-10-06-121017.jpg"); + const completeImage = join(albumDirectory, "2025-10-06-121212.jpg"); + + await mkdir(albumDirectory, { recursive: true }); + await writeFile(missingImage, ""); + await writeFile(completeImage, ""); + await writeFile(join(albumDirectory, "2025-10-06-121212.json"), "{}"); + + assert.deepEqual(await getImagesToProcess(tempRoot), [missingImage]); + assert.deepEqual(await getImagesToProcess(tempRoot, { refresh: true }), [ + missingImage, + completeImage, + ]); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +} + +await main(); diff --git a/scripts/vision.ts b/scripts/vision.ts new file mode 100644 index 0000000..b60274d --- /dev/null +++ b/scripts/vision.ts @@ -0,0 +1,522 @@ +#!/usr/bin/env -S node --experimental-strip-types + +import { execFile } from "node:child_process"; +import { readFile, writeFile } from "node:fs/promises"; +import { relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +import Anthropic from "@anthropic-ai/sdk"; +import { consola } from "consola"; +import { + getImagesMissingMetadata, + getMetadataPathForImage, + getPhotoAbsolutePath, + getPhotoDirectories, + PHOTOS_DIRECTORY, +} from "../src/lib/photo-albums.ts"; + +const execFileAsync = promisify(execFile); + +/** + * Define the directory where the images are located. + */ +const PHOTOS_DIR = PHOTOS_DIRECTORY; + +/** + * Instantiate the Anthropic client. + */ +let anthropic: Anthropic | undefined; + +function getAnthropicClient(): Anthropic { + anthropic ??= new Anthropic({ maxRetries: 0 }); + return anthropic; +} + +function assertRequiredEnvironment(): void { + if (!process.env.ANTHROPIC_API_KEY) { + throw new Error( + "Missing ANTHROPIC_API_KEY. `pnpm run vision` loads `.env.local` automatically. If you run the script directly, use `node --env-file=.env.local --experimental-strip-types scripts/vision.ts`.", + ); + } +} + +/** + * Represents the metadata of an image in the Exif format. + */ +export interface ExifMetadata { + SourceFile: string; + FileName: string; + Model: string; + FNumber: number; + FocalLength: string; + ExposureTime: string; + ISO: number; + DateTimeOriginal: string; + LensModel: string; + GPSPosition?: string; + GPSLatitude?: string; + GPSLongitude?: string; +} + +/** + * Represents the result of the AI analysis. + */ +export interface VisionAIResult { + title_ideas: string[]; + description: string; + tags: string[]; +} + +/** + * Represents the final metadata suggestion for an image. + */ +export interface ImageMetadataSuggestion { + id: string; + title: string[]; + image: string; + alt: string; + location: string; + date: string; + tags: string[]; + exif: { + camera: string; + lens: string; + aperture: string; + iso: string; + focal_length: string; + shutter_speed: string; + }; +} + +interface VisionCliOptions { + refresh: boolean; + photosDirectory?: string; + visionConcurrency: number; + visionMaxRetries: number; + visionBaseBackoffMs: number; +} + +function parseCliOptions(argv: string[]): VisionCliOptions { + const getNumericOption = (name: string, fallback: number): number => { + const prefix = `--${name}=`; + const rawValue = argv + .find((arg) => arg.startsWith(prefix)) + ?.slice(prefix.length); + const parsed = Number.parseInt(rawValue ?? "", 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; + }; + + const envConcurrency = Number.parseInt( + process.env.VISION_CONCURRENCY ?? "", + 10, + ); + const envMaxRetries = Number.parseInt( + process.env.VISION_MAX_RETRIES ?? "", + 10, + ); + const envBaseBackoffMs = Number.parseInt( + process.env.VISION_BASE_BACKOFF_MS ?? "", + 10, + ); + const nonFlagArgs = argv.filter((arg) => !arg.startsWith("--")); + + return { + refresh: argv.includes("--refresh"), + photosDirectory: resolve(nonFlagArgs[0] ?? PHOTOS_DIR), + visionConcurrency: getNumericOption( + "concurrency", + Number.isFinite(envConcurrency) && envConcurrency > 0 + ? envConcurrency + : 2, + ), + visionMaxRetries: getNumericOption( + "retries", + Number.isFinite(envMaxRetries) && envMaxRetries > 0 ? envMaxRetries : 8, + ), + visionBaseBackoffMs: getNumericOption( + "backoff-ms", + Number.isFinite(envBaseBackoffMs) && envBaseBackoffMs > 0 + ? envBaseBackoffMs + : 1500, + ), + }; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function isRateLimitError(error: unknown): boolean { + return error instanceof Anthropic.RateLimitError; +} + +function extractRetryAfterMs(error: unknown): number | null { + if (!(error instanceof Anthropic.RateLimitError)) { + return null; + } + + const retryAfter = error.headers?.get("retry-after"); + if (retryAfter) { + const seconds = Number.parseFloat(retryAfter); + if (Number.isFinite(seconds) && seconds > 0) { + return Math.ceil(seconds * 1000); + } + } + + return null; +} + +async function mapWithConcurrency( + values: T[], + concurrency: number, + mapper: (value: T, index: number) => Promise, +): Promise { + if (values.length === 0) { + return []; + } + + const results: R[] = new Array(values.length); + const workerCount = Math.max(1, Math.min(concurrency, values.length)); + let cursor = 0; + + const workers = Array.from({ length: workerCount }, async () => { + while (true) { + const currentIndex = cursor; + cursor += 1; + + if (currentIndex >= values.length) { + return; + } + + const value = values[currentIndex]; + if (typeof value === "undefined") { + continue; + } + + results[currentIndex] = await mapper(value, currentIndex); + } + }); + + await Promise.all(workers); + return results; +} + +/** + * Get all images that don't have a JSON file and therefore need to be processed. + */ +export async function getImagesToProcess( + photosDirectory = PHOTOS_DIR, + options: Pick = { refresh: false }, +): Promise { + const relativeImagePaths = options.refresh + ? (await getPhotoDirectories(photosDirectory)).flatMap( + (directory) => directory.imagePaths, + ) + : await getImagesMissingMetadata(photosDirectory); + + consola.info( + options.refresh + ? `Refreshing ${relativeImagePaths.length} ${relativeImagePaths.length === 1 ? "image" : "images"} with metadata sidecars` + : `Found ${relativeImagePaths.length} ${relativeImagePaths.length === 1 ? "image" : "images"} without metadata`, + ); + + return relativeImagePaths.map((imagePath) => + getPhotoAbsolutePath(imagePath, photosDirectory), + ); +} + +/** + * Extracts the EXIF metadata from an image file. + * @param imagePath - The path to the image file. + * + * @returns A promise that resolves to the extracted EXIF metadata. + */ +export async function extractExifMetadata( + imagePath: string, +): Promise { + /// Check if `exiftool` is installed. + try { + await execFileAsync("exiftool", ["--version"]); + } catch (_error) { + consola.error( + "exiftool is not installed. Please run `brew install exiftool`.", + ); + process.exit(1); + } + + /// Extract the metadata + const { stdout } = await execFileAsync("exiftool", ["-j", imagePath]); + const output = JSON.parse(stdout) as ExifMetadata[]; + + if (!output[0]) { + throw new Error(`No EXIF metadata found for ${imagePath}.`); + } + + return output[0]; +} + +/** + * Encodes an image file to base64. + * @param imagePath - The path to the image file. + * @returns A Promise that resolves to the base64 encoded image. + */ +async function base64EncodeImage(imagePath: string): Promise { + const buffer = await readFile(imagePath); + return buffer.toString("base64"); +} + +const VISION_TOOL = { + name: "vision_response", + description: "Return the vision analysis of the image.", + input_schema: { + type: "object" as const, + additionalProperties: false, + properties: { + title_ideas: { type: "array", items: { type: "string" } }, + description: { type: "string" }, + tags: { type: "array", items: { type: "string" } }, + }, + required: ["title_ideas", "description", "tags"], + }, +}; + +/** + * Generates image description, title suggestions and tags using AI. + * + * @param metadata - The metadata of the image. + * @returns A Promise that resolves to a VisionAIResult object containing the generated image description, title suggestions, and tags. + */ +async function generateImageDescriptionTitleSuggestionsAndTags( + metadata: ExifMetadata, + options: Pick, +): Promise { + /// Base64 encode the image in order to pass it to the API + const encodedImage = await base64EncodeImage(metadata.SourceFile); + + const prompt = + "Create an accurate and detailed description of this image that would also work as an alt text. The alt text should not contain words like image, photograph, illustration or such. Describe the scene as it is. Also come up with 5 title suggestions for this image. At last suggest 5 tags that suit the image description. These tags should be single words only. Identify the main subject or theme and make sure to put the according tag first. Return the description, the title suggestions and tags."; + + let lastError: unknown; + + for (let attempt = 0; attempt <= options.visionMaxRetries; attempt += 1) { + try { + const response = await getAnthropicClient().messages.create({ + model: "claude-opus-4-6", + max_tokens: 2048, + tools: [VISION_TOOL], + tool_choice: { type: "tool", name: "vision_response" }, + messages: [ + { + role: "user", + content: [ + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: encodedImage, + }, + }, + { type: "text", text: prompt }, + ], + }, + ], + }); + + const toolUseBlock = response.content.find((b) => b.type === "tool_use"); + if (!toolUseBlock || toolUseBlock.type !== "tool_use") { + throw new Error( + `No tool use response from AI for ${metadata.SourceFile}.`, + ); + } + + const parsedResponse = toolUseBlock.input as VisionAIResult; + + if ( + parsedResponse.title_ideas.length === 0 || + parsedResponse.description.length === 0 || + parsedResponse.tags.length === 0 + ) { + throw new Error( + `Incomplete vision response for ${metadata.SourceFile}.`, + ); + } + + return parsedResponse; + } catch (error) { + lastError = error; + if (!isRateLimitError(error) || attempt >= options.visionMaxRetries) { + break; + } + + const retryAfterMs = extractRetryAfterMs(error); + const exponentialBackoffMs = options.visionBaseBackoffMs * 2 ** attempt; + const jitterMs = Math.floor(Math.random() * 350); + const waitMs = + Math.max(retryAfterMs ?? 0, exponentialBackoffMs) + jitterMs; + const relativeSourcePath = relative(process.cwd(), metadata.SourceFile); + const nextAttempt = attempt + 1; + consola.warn( + `Rate limit for ${relativeSourcePath}. Retry ${nextAttempt}/${options.visionMaxRetries} in ${Math.ceil(waitMs / 1000)}s...`, + ); + await sleep(waitMs); + } + } + + throw lastError; +} + +function ensureVisionCanRun(imagesToProcess: string[]): void { + if (imagesToProcess.length === 0) { + return; + } + + assertRequiredEnvironment(); +} + +function getLocationFromExif(exifData: ExifMetadata): string { + if (exifData.GPSPosition) { + return exifData.GPSPosition; + } + + if (exifData.GPSLatitude && exifData.GPSLongitude) { + return `${exifData.GPSLatitude}, ${exifData.GPSLongitude}`; + } + + return ""; +} + +/** + * Merges the metadata from EXIF data and vision data to create an ImageMetadataSuggestion object. + * @param exifData - The EXIF metadata of the image. + * @param visionData - The vision AI result data of the image. + * @returns The merged ImageMetadataSuggestion object. + */ +export function mergeMetaAndVisionData( + exifData: ExifMetadata, + visionData: VisionAIResult, +): ImageMetadataSuggestion { + const [date] = exifData.DateTimeOriginal.split(" "); + + if (!date) { + throw new Error(`Missing original date for ${exifData.SourceFile}.`); + } + + return { + id: exifData.FileName.replace(".jpg", ""), + title: visionData.title_ideas, + image: `./${exifData.FileName}`, + alt: visionData.description, + location: getLocationFromExif(exifData), + date: date.replaceAll(":", "-"), + tags: visionData.tags, + exif: { + camera: exifData.Model, + lens: exifData.LensModel, + aperture: exifData.FNumber.toString(), + iso: exifData.ISO.toString(), + focal_length: exifData.FocalLength.replace(" mm", ""), + shutter_speed: exifData.ExposureTime, + }, + }; +} + +/** + * Writes the given image metadata to a JSON file. + * @param imageMetadata - The image metadata to be written. + * @returns A Promise that resolves when the JSON file is written successfully. + */ +async function writeToJsonFile( + imageMetadata: ImageMetadataSuggestion, + imagePath: string, + photosDirectory: string, +): Promise { + const relativeImagePath = relative(photosDirectory, imagePath); + const jsonPath = getMetadataPathForImage(relativeImagePath, photosDirectory); + const json = JSON.stringify(imageMetadata, null, 2); + await writeFile(jsonPath, json); +} + +/** + * Main. + */ +async function main() { + consola.start("Checking for images to process..."); + const cliOptions = parseCliOptions(process.argv.slice(2)); + const photosDirectory = cliOptions.photosDirectory ?? PHOTOS_DIR; + + /// Load all images that don't have a JSON file. + const images = await getImagesToProcess(photosDirectory, cliOptions); + + if (images.length === 0) { + consola.success( + cliOptions.refresh + ? "No images found to refresh." + : "No images require metadata.", + ); + return; + } + + consola.info( + `Vision settings: concurrency=${cliOptions.visionConcurrency}, retries=${cliOptions.visionMaxRetries}, backoff=${cliOptions.visionBaseBackoffMs}ms`, + ); + + ensureVisionCanRun(images); + + /// Extract EXIF metadata from these images. + const exifData = await mapWithConcurrency( + images, + 8, + async (imagePath, index) => { + consola.info(`Extracting EXIF ${index + 1}/${images.length}...`); + return await extractExifMetadata(imagePath); + }, + ); + + /// Determine the image description, title suggestions and tags for each image with AI. + const visionData = await mapWithConcurrency( + exifData, + cliOptions.visionConcurrency, + async (exifEntry, index) => { + consola.info(`Generating AI metadata ${index + 1}/${exifData.length}...`); + return await generateImageDescriptionTitleSuggestionsAndTags( + exifEntry, + cliOptions, + ); + }, + ); + + /// Merge the EXIF and Vision data to create the final metadata suggestion. + const imageData = exifData.map((e, i) => { + const currentVisionData = visionData[i]; + + if (!currentVisionData) { + throw new Error(`Missing vision data for ${e.SourceFile}.`); + } + + return mergeMetaAndVisionData(e, currentVisionData); + }); + + /// Write the metadata to JSON files. + await mapWithConcurrency(imageData, 8, async (imageMetadata, index) => { + const sourceFile = exifData[index]?.SourceFile; + + if (!sourceFile) { + throw new Error(`Missing source file for ${imageMetadata.id}.`); + } + + await writeToJsonFile(imageMetadata, sourceFile, photosDirectory); + consola.info(`Wrote metadata ${index + 1}/${imageData.length}.`); + }); + + consola.success("All images processed successfully."); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + try { + await main(); + } catch (error) { + consola.error(error); + process.exit(1); + } +} diff --git a/src/assets/icons/swipe.png b/src/assets/icons/swipe.png new file mode 100644 index 0000000..c93e065 Binary files /dev/null and b/src/assets/icons/swipe.png differ diff --git a/src/assets/images/about.jpg b/src/assets/images/about.jpg new file mode 100644 index 0000000..5e8c42d Binary files /dev/null and b/src/assets/images/about.jpg differ diff --git a/src/assets/images/intro.jpg b/src/assets/images/intro.jpg new file mode 100644 index 0000000..7b470e0 Binary files /dev/null and b/src/assets/images/intro.jpg differ diff --git a/src/assets/images/me-bangkok.jpg b/src/assets/images/me-bangkok.jpg new file mode 100644 index 0000000..c3d6d60 Binary files /dev/null and b/src/assets/images/me-bangkok.jpg differ diff --git a/src/components/BlogPostItem.astro b/src/components/BlogPostItem.astro new file mode 100644 index 0000000..5aca70e --- /dev/null +++ b/src/components/BlogPostItem.astro @@ -0,0 +1,168 @@ +--- +import type { CollectionEntry } from "astro:content"; + +interface Props { + post: CollectionEntry<"blog">; + categoryTitleMap?: Map; + seriesCountMap?: Map; + activeTagId?: string; +} + +const { post, categoryTitleMap, seriesCountMap, activeTagId } = Astro.props; + +function capitalize(s: string) { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +function formatDate(date: Date) { + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); +} + +const showCategory = categoryTitleMap && post.data.category; +const hasMeta = + showCategory || post.data.tags.length > 0 || !!post.data.seriesParent; +--- + +
  • + +

    {post.data.title}

    + {formatDate(post.data.publishDate)} +
    + {post.data.description &&

    {post.data.description}

    } + { + hasMeta && ( + + ) + } +
  • + + diff --git a/src/components/BlogPostSeries.astro b/src/components/BlogPostSeries.astro new file mode 100644 index 0000000..f5f674e --- /dev/null +++ b/src/components/BlogPostSeries.astro @@ -0,0 +1,144 @@ +--- +import type { CollectionEntry } from "astro:content"; + +interface Props { + children: CollectionEntry<"blog">[]; + currentId?: string; + parent?: CollectionEntry<"blog">; + sidebar?: boolean; +} + +const { children, currentId, parent, sidebar = false } = Astro.props; +--- + + + + diff --git a/src/components/Footer.astro b/src/components/Footer.astro new file mode 100644 index 0000000..3a158ef --- /dev/null +++ b/src/components/Footer.astro @@ -0,0 +1,182 @@ +--- +interface Props { + dark?: boolean | undefined; +} +const { dark } = Astro.props; +--- + + + + diff --git a/src/components/JsonLd.astro b/src/components/JsonLd.astro new file mode 100644 index 0000000..8f9c159 --- /dev/null +++ b/src/components/JsonLd.astro @@ -0,0 +1,9 @@ +--- +interface Props { + schema: Record; +} + +const { schema } = Astro.props; +--- + + + + diff --git a/src/components/PhotoDetail.astro b/src/components/PhotoDetail.astro new file mode 100644 index 0000000..461c9f9 --- /dev/null +++ b/src/components/PhotoDetail.astro @@ -0,0 +1,423 @@ +--- +import { Image } from "astro:assets"; +import type { ImageMetadata } from "astro"; +import swipeIcon from "@/assets/icons/swipe.png"; + +type PhotoSidecar = { + id: string; + title: string[]; + alt: string; + location: string; + date: string; + tags: string[]; + exif: { + camera: string; + lens: string; + aperture: string; + iso: string; + focal_length: string; + shutter_speed: string; + }; +}; + +interface Props { + sidecar: PhotoSidecar; + image: ImageMetadata; + prevHref: string | null; + nextHref: string | null; + backHref: string; + backLabel: string; + canonicalUrl: string; +} + +const { + sidecar, + image, + prevHref, + nextHref, + backHref, + backLabel, + canonicalUrl, +} = Astro.props; +--- + +
    + + + +
    + ← {backLabel} +
    + +
    +
    +
    + { + prevHref ? ( + + ) : ( + + ) + } +
    + {sidecar.alt} + +
    +
    +

    {sidecar.title[0]}

    + +
    +
    + ƒ{sidecar.exif.aperture} · {sidecar.exif.shutter_speed}s · ISO {sidecar.exif.iso} · {sidecar.exif.focal_length}mm + { + sidecar.tags.length > 0 && ( +
    + {sidecar.tags.map((tag) => ( + + {tag} + + ))} +
    + ) + } +
    +
    +
    + { + nextHref ? ( + + ) : ( + + ) + } +
    +
    +
    + + +
    + + + + diff --git a/src/components/PhotosSubNav.astro b/src/components/PhotosSubNav.astro new file mode 100644 index 0000000..a5037b4 --- /dev/null +++ b/src/components/PhotosSubNav.astro @@ -0,0 +1,97 @@ +--- +const pathname = Astro.url.pathname; + +const links = [ + { + href: "/photos", + label: "Stream", + active: pathname === "/photos" || pathname === "/photos/", + }, + { + href: "/photos/collections", + label: "Collections", + active: pathname.startsWith("/photos/collections"), + }, + { + href: "/photos/tags", + label: "Tags", + active: pathname.startsWith("/photos/tags"), + }, + { + href: "/photos/map", + label: "Map", + active: pathname.startsWith("/photos/map"), + }, + { + href: "/photos/stats", + label: "Stats", + active: pathname.startsWith("/photos/stats"), + }, +]; +--- + +
    +
    + ← Back home +

    Photos

    +
    + + +
    + + diff --git a/src/components/TableOfContents.astro b/src/components/TableOfContents.astro new file mode 100644 index 0000000..ee8f914 --- /dev/null +++ b/src/components/TableOfContents.astro @@ -0,0 +1,113 @@ +--- +interface Heading { + depth: number; + slug: string; + text: string; +} + +interface Props { + headings: Heading[]; +} + +const { headings } = Astro.props; +--- + + + + + + diff --git a/src/components/WebMentions.astro b/src/components/WebMentions.astro new file mode 100644 index 0000000..1d68f05 --- /dev/null +++ b/src/components/WebMentions.astro @@ -0,0 +1,241 @@ +--- +import { getFilteredWebmentions, type Mention } from "@/lib/webmentions"; + +interface Props { + url: string; +} + +const { url } = Astro.props; + +// Build-time fetch for instant initial render +let mentions: Mention[] = []; +try { + mentions = await getFilteredWebmentions(url); +} catch { + // Unavailable at build time — client-side will handle it +} + +const likes = mentions.filter((m) => m["wm-property"] === "like-of"); +const reposts = mentions.filter((m) => m["wm-property"] === "repost-of"); +const replies = mentions.filter( + (m) => + m["wm-property"] === "in-reply-to" || m["wm-property"] === "mention-of", +); +--- + +{mentions.length > 0 && ( +
    +

    Webmentions

    + + {(likes.length > 0 || reposts.length > 0) && ( +
    + {likes.length > 0 && ( +
    + {likes.length} {likes.length === 1 ? "Like" : "Likes"} + +
    + )} + {reposts.length > 0 && ( +
    + {reposts.length} {reposts.length === 1 ? "Repost" : "Reposts"} + +
    + )} +
    + )} + + {replies.length > 0 && ( +
    + {replies.length} {replies.length === 1 ? "Reply" : "Replies"} + {replies.map((m) => ( +
    +
    + {m.author?.photo && ( + {m.author.name + )} +
    + {m.author?.name ?? "Anonymous"} + {m.published && ( + + )} +
    +
    + {m.content?.text &&

    {m.content.text}

    } + View original +
    + ))} +
    + )} +
    +)} + +{mentions.length === 0 && ( +
    +)} + + diff --git a/src/components/links/LinkCard.astro b/src/components/links/LinkCard.astro new file mode 100644 index 0000000..3008f01 --- /dev/null +++ b/src/components/links/LinkCard.astro @@ -0,0 +1,167 @@ +--- +import type { AnyLink } from "@/lib/collections"; +import { + collectionColor, + formatDate, + getDomain, + getFaviconUrl, +} from "@/lib/links"; + +export interface Props { + link: AnyLink; +} + +const { link } = Astro.props; +const { title, url, description, publishDate, tags, collection } = link.data; +const domain = getDomain(url); +const faviconUrl = getFaviconUrl(url); +--- + + + + diff --git a/src/components/links/LinksLayout.astro b/src/components/links/LinksLayout.astro new file mode 100644 index 0000000..5e066c0 --- /dev/null +++ b/src/components/links/LinksLayout.astro @@ -0,0 +1,397 @@ +--- +import BaseLayout from "@/layouts/BaseLayout.astro"; +import type { AnyLink } from "@/lib/collections"; +import { + collectionColor, + filterByCollection, + filterByTag, + getUniqueCollections, + getUniqueTags, +} from "@/lib/links"; + +export interface Props { + title: string; + description: string; + links: AnyLink[]; +} + +const { title, description, links } = Astro.props; + +const collections = getUniqueCollections(links); +const tags = getUniqueTags(links); +const pathname = Astro.url.pathname.replace(/\/$/, ""); + +function isActive(href: string): boolean { + return pathname === href.replace(/\/$/, ""); +} +--- + + + + + + diff --git a/src/content.config.ts b/src/content.config.ts new file mode 100644 index 0000000..4b24d5d --- /dev/null +++ b/src/content.config.ts @@ -0,0 +1,148 @@ +import { defineCollection, reference } from "astro:content"; +import { file, glob } from "astro/loaders"; +import { z } from "astro/zod"; + +const blog = defineCollection({ + loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog/posts" }), + schema: ({ image }) => + z.object({ + title: z.string(), + description: z.string(), + publishDate: z.coerce.date(), + updatedDate: z.coerce.date().optional(), + tags: z.array(z.string()).default([]), + category: reference("categories").default({ + collection: "categories", + id: "general", + }), + draft: z.boolean().default(false), + seriesParent: z.string().optional(), + seriesOrder: z.number().optional(), + cover: image().optional(), + coverAlt: z.string().optional(), + }), +}); + +const categories = defineCollection({ + loader: glob({ + pattern: "**/*.{md,mdx}", + base: "./src/content/blog/categories", + }), + schema: ({ image }) => + z.object({ + title: z.string(), + description: z.string(), + order: z.number().int().default(99), + cover: image().optional(), + coverAlt: z.string().optional(), + }), +}); + +const notes = defineCollection({ + loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/notes" }), + schema: ({ image }) => + z.object({ + title: z.string(), + publishDate: z.coerce.date(), + description: z.string().optional(), + cover: image().optional(), + coverAlt: z.string().optional(), + draft: z.boolean().default(false), + tags: z.array(z.string()).default([]), + }), +}); + +const links_json = defineCollection({ + loader: file("./src/content/links/links.json", { + parser: (text) => { + const entries = JSON.parse(text) as Array>; + return entries.map((entry) => { + if (!entry.id && !entry.slug) { + const date = ( + String(entry.publishDate ?? "undated").split("T")[0] ?? "undated" + ).replace(/-/g, "/"); + const slug = String(entry.title ?? "link") + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, ""); + return { ...entry, id: `json/${date}/${slug}` }; + } + return entry; + }); + }, + }), + schema: z.object({ + title: z.string(), + url: z.url(), + description: z.string().optional(), + publishDate: z.coerce.date(), + via: z.string().optional(), + tags: z.array(z.string()).default([]), + collection: z.string().optional(), + }), +}); + +const projects_categories = defineCollection({ + loader: glob({ + pattern: "**/*.{md,mdx}", + base: "./src/content/projects/categories", + }), + schema: ({ image }) => + z.object({ + title: z.string(), + description: z.string(), + order: z.number().int().default(99), + cover: image().optional(), + coverAlt: z.string().optional(), + }), +}); + +const projects = defineCollection({ + loader: glob({ + pattern: "**/*.{md,mdx}", + base: "./src/content/projects/project", + }), + schema: ({ image }) => + z.object({ + title: z.string(), + description: z.string(), + publishDate: z.coerce.date(), + draft: z.boolean().default(false), + url: z.url().optional(), + github: z.url().optional(), + tags: z.array(z.string()).default([]), + category: reference("projects_categories").default({ + collection: "projects_categories", + id: "general", + }), + cover: image().optional(), + coverAlt: z.string().optional(), + }), +}); + +const collections_photos = defineCollection({ + loader: glob({ + pattern: "**/index.{md,mdx}", + base: "./src/content/photos/collections", + }), + schema: z.object({ + title: z.string(), + description: z.string(), + location: z.string().optional(), + publishDate: z.coerce.date().optional(), + draft: z.boolean().default(false), + coverImage: z.string().optional(), + order: z.number().int().default(0), + set: z.boolean().default(false), + }), +}); + +export const collections = { + blog, + categories, + notes, + links_json, + collections_photos, + projects, + projects_categories, +}; diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro new file mode 100644 index 0000000..02c8511 --- /dev/null +++ b/src/layouts/BaseLayout.astro @@ -0,0 +1,129 @@ +--- +import "@/styles/reset.css"; +import "@/styles/global.css"; + +import smartypants from "smartypants"; +import Footer from "@/components/Footer.astro"; +import JsonLd from "@/components/JsonLd.astro"; +import Nav from "@/components/Nav.astro"; + +const siteName = "Adrian Altner"; +const siteUrl = "https://adrian-altner.com"; + +export interface Props { + title: string; + description: string; + image?: + | { + src: string; + alt: string; + } + | undefined; + navDesktopHidden?: boolean; + navHidden?: boolean; + footerHidden?: boolean; + footerDark?: boolean; +} + +const { + title, + description, + image, + navDesktopHidden, + navHidden, + footerHidden, + footerDark, +} = Astro.props; + +function smartypantsText(str: string): string { + return smartypants(str, 1).replace(/&#(\d+);/g, (_, code) => + String.fromCharCode(Number(code)), + ); +} + +const canonicalURL = new URL(Astro.url.pathname, Astro.site); +const pageTitle = Astro.url.pathname === "/" ? title : `${title} · ${siteName}`; +const isHomepage = Astro.url.pathname === "/"; + +const websiteSchema = { + "@context": "https://schema.org", + "@type": "WebSite", + name: siteName, + url: siteUrl, +}; + +const personSchema = { + "@context": "https://schema.org", + "@type": "Person", + name: siteName, + url: siteUrl, + jobTitle: "Photographer & Software Developer", + sameAs: [ + "https://www.instagram.com/adrian.altner/", + "https://www.linkedin.com/in/adrian-altner/", + "https://github.com/adrian-altner", + ], +}; +--- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <meta name="description" content={smartypantsText(description)} /> + <meta property="og:title" content={smartypantsText(title)} /> + <meta property="og:type" content="website" /> + <meta property="og:description" content={smartypantsText(description)} /> + <meta property="og:url" content={canonicalURL} /> + <meta property="og:locale" content="en_US" /> + <meta property="og:site_name" content={siteName} /> + { + image ? ( + <> + <meta property="og:image" content={image.src} /> + <meta property="og:image:alt" content={image.alt} /> + <meta name="twitter:card" content="summary_large_image" /> + <meta name="twitter:image" content={image.src} /> + </> + ) : ( + <meta name="twitter:card" content="summary" /> + ) + } + <JsonLd schema={websiteSchema} /> + {isHomepage && <JsonLd schema={personSchema} />} + <slot name="head" /> + </head> + <body> + {navHidden + ? <Nav hidden={true} desktopHidden={true} /> + : navDesktopHidden + ? <Nav desktopHidden={true} /> + : <Nav title={siteName} /> + } + <slot /> + {!footerHidden && <Footer dark={footerDark} />} + </body> +</html> diff --git a/src/layouts/ProseLayout.astro b/src/layouts/ProseLayout.astro new file mode 100644 index 0000000..6a4344b --- /dev/null +++ b/src/layouts/ProseLayout.astro @@ -0,0 +1,118 @@ +--- +import BaseLayout from "@/layouts/BaseLayout.astro"; + +const { frontmatter } = Astro.props; +--- + +<BaseLayout title={frontmatter.title} description={frontmatter.description}> + <main class="main"> + <a href="/" class="back">← Home</a> + <div class="content"> + <slot /> + <script> + document.querySelectorAll('[data-obf]').forEach(el => { + el.textContent = atob((el as HTMLElement).dataset.obf!); + }); + document.querySelectorAll('[data-obf-href]').forEach(el => { + (el as HTMLAnchorElement).href = atob((el as HTMLElement).dataset.obfHref!); + }); + </script> + </div> + </main> +</BaseLayout> + +<style> + .main { + max-width: var(--width-prose); + margin: 0 auto; + padding: 2rem 2rem 4rem; + width: 100%; + } + + .back { + display: inline-block; + margin-bottom: 1.5rem; + font-size: 0.9rem; + color: #555; + text-decoration: none; + } + + .back:hover { + color: var(--accent); + } + + .content { + font-size: 1rem; + line-height: 1.75; + color: #333; + } + + .content :global(h1) { + font-size: 1.75rem; + font-weight: 400; + letter-spacing: -0.02em; + margin-bottom: 2rem; + color: #111; + } + + .content :global(h2) { + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #888; + margin: 2rem 0 0.75rem; + padding-bottom: 0.4rem; + border-bottom: 2px solid #111; + } + + .content :global(h3) { + font-size: 1rem; + font-weight: 600; + margin: 1.5rem 0 0.5rem; + color: #111; + } + + .content :global(p) { + margin-bottom: 1.25rem; + } + + .content :global(ul), + .content :global(ol) { + margin-bottom: 1.25rem; + padding-left: 1.5rem; + } + + .content :global(li) { + margin-bottom: 0.4rem; + } + + .content :global(strong) { + font-weight: 600; + color: #111; + } + + .content :global(a) { + color: var(--accent); + text-underline-offset: 3px; + } + + .content :global(code) { + font-family: monospace; + font-size: 0.9rem; + background: #eeeeed; + padding: 0.25rem 0.5rem; + border-radius: 4px; + } + + .content :global(.r) { + direction: rtl; + unicode-bidi: bidi-override; + } + + .content :global(hr) { + border: none; + border-top: 1px solid #ddd; + margin: 2rem 0; + } +</style> diff --git a/src/lib/collections.ts b/src/lib/collections.ts new file mode 100644 index 0000000..bf47451 --- /dev/null +++ b/src/lib/collections.ts @@ -0,0 +1,137 @@ +import { type CollectionEntry, getCollection } from "astro:content"; +import type { ImageMetadata } from "astro"; + +export type AnyLink = CollectionEntry<"links_json">; + +export async function getLinks(): Promise<AnyLink[]> { + const links = await getCollection("links_json"); + return links.sort( + (a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf(), + ); +} + +export type PhotoSidecar = { + id: string; + title: string[]; + alt: string; + location: string; + date: string; + tags: string[]; + exif: { + camera: string; + lens: string; + aperture: string; + iso: string; + focal_length: string; + shutter_speed: string; + }; +}; + +export type LoadedPhoto = { + sidecar: PhotoSidecar; + image: ImageMetadata; +}; + +export type Breadcrumb = { + label: string; + href: string; +}; + +/** + * Extract the clean slug from a collection entry id. + * e.g. "travels/asia/chiang-mai/index.md" → "travels/asia/chiang-mai" + * "index.md" → "" + */ +export function collectionSlug( + entry: CollectionEntry<"collections_photos">, +): string { + return entry.id.replace(/\/index\.mdx?$/, "").replace(/^index\.mdx?$/, ""); +} + +/** + * Load and sort photos from a specific collection's img/ directory. + * Only loads photos directly in `collections/<slug>/img/` (not recursive). + */ +export function buildCollectionPhotos( + sidecars: Record<string, PhotoSidecar>, + imageModules: Record<string, { default: ImageMetadata }>, + slug: string, +): LoadedPhoto[] { + const prefix = slug + ? `/src/content/photos/collections/${slug}/img/` + : "/src/content/photos/collections/img/"; + + return Object.entries(sidecars) + .filter(([p]) => { + // Must be in exactly this collection's img/ dir (no deeper nesting) + if (!p.startsWith(prefix)) return false; + const remainder = p.slice(prefix.length); + return !remainder.includes("/"); + }) + .map(([jsonPath, sidecar]) => { + const imgPath = jsonPath.replace(/\.json$/, ".jpg"); + const image = imageModules[imgPath]?.default; + if (!image) return null; + return { sidecar, image }; + }) + .filter((p): p is LoadedPhoto => p !== null) + .sort( + (a, b) => + new Date(a.sidecar.date).getTime() - new Date(b.sidecar.date).getTime(), + ); +} + +/** + * Build breadcrumb trail from root to the given slug. + * e.g. slug "travels/asia/chiang-mai" → [ + * { label: "Collections", href: "/photos/collections" }, + * { label: "Travels", href: "/photos/collections/travels" }, + * { label: "Asia", href: "/photos/collections/travels/asia" }, + * { label: "Chiang Mai", href: "/photos/collections/travels/asia/chiang-mai" }, + * ] + */ +export function buildBreadcrumbs( + slug: string, + allCollections: CollectionEntry<"collections_photos">[], +): Breadcrumb[] { + const crumbs: Breadcrumb[] = [ + { label: "Collections", href: "/photos/collections" }, + ]; + + if (!slug) return crumbs; + + const segments = slug.split("/"); + for (let i = 0; i < segments.length; i++) { + const partialSlug = segments.slice(0, i + 1).join("/"); + const entry = allCollections.find((c) => collectionSlug(c) === partialSlug); + crumbs.push({ + label: entry?.data.title ?? segments[i] ?? partialSlug, + href: `/photos/collections/${partialSlug}`, + }); + } + + return crumbs; +} + +/** + * Get immediate child collections of a given parent slug. + */ +export function getChildCollections( + parentSlug: string, + allCollections: CollectionEntry<"collections_photos">[], +): CollectionEntry<"collections_photos">[] { + const depth = parentSlug ? parentSlug.split("/").length : 0; + + return allCollections + .filter((c) => { + const cs = collectionSlug(c); + if (!cs) return false; + if (parentSlug && !cs.startsWith(`${parentSlug}/`)) return false; + if (!parentSlug && cs.includes("/")) return false; + return cs.split("/").length === depth + 1; + }) + .sort( + (a, b) => + a.data.order - b.data.order || a.data.title.localeCompare(b.data.title), + ); +} diff --git a/src/lib/links.ts b/src/lib/links.ts new file mode 100644 index 0000000..eaeba3f --- /dev/null +++ b/src/lib/links.ts @@ -0,0 +1,68 @@ +import type { AnyLink } from "@/lib/collections"; + +const COLLECTION_COLORS = [ + "#e45c5c", + "#e4885c", + "#d4c24a", + "#5cb85c", + "#5ca8e4", + "#8e5ce4", + "#e45ca8", + "#5ce4d4", +]; + +export function collectionColor(name: string): string { + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = (hash * 31 + name.charCodeAt(i)) >>> 0; + } + return COLLECTION_COLORS[hash % COLLECTION_COLORS.length] as string; +} + +export function getDomain(url: string): string { + try { + return new URL(url).hostname.replace(/^www\./, ""); + } catch { + return url; + } +} + +export function getFaviconUrl(url: string): string { + const domain = getDomain(url); + return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`; +} + +export function getUniqueCollections(links: AnyLink[]): string[] { + const seen = new Set<string>(); + for (const link of links) { + const col = link.data.collection; + if (col) seen.add(col); + } + return Array.from(seen).sort(); +} + +export function getUniqueTags(links: AnyLink[]): string[] { + const seen = new Set<string>(); + for (const link of links) { + for (const tag of link.data.tags) { + seen.add(tag); + } + } + return Array.from(seen).sort(); +} + +export function filterByCollection(links: AnyLink[], col: string): AnyLink[] { + return links.filter((l) => l.data.collection === col); +} + +export function filterByTag(links: AnyLink[], tag: string): AnyLink[] { + return links.filter((l) => l.data.tags.includes(tag)); +} + +export function formatDate(date: Date): string { + return date.toLocaleDateString("de-DE", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); +} diff --git a/src/lib/og.ts b/src/lib/og.ts new file mode 100644 index 0000000..65528e9 --- /dev/null +++ b/src/lib/og.ts @@ -0,0 +1,610 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import satori from "satori"; +import sharp from "sharp"; + +const fontRegular = readFileSync( + join( + process.cwd(), + "node_modules/@fontsource/exo-2/files/exo-2-latin-400-normal.woff", + ), +); +const fontBold = readFileSync( + join( + process.cwd(), + "node_modules/@fontsource/exo-2/files/exo-2-latin-700-normal.woff", + ), +); + +export async function renderOgImage( + vNode: Parameters<typeof satori>[0], +): Promise<ArrayBuffer> { + const svg = await satori(vNode, { + width: 1200, + height: 630, + fonts: [ + { + name: "Exo 2", + data: fontRegular.buffer, + weight: 400, + style: "normal", + }, + { + name: "Exo 2", + data: fontBold.buffer, + weight: 700, + style: "normal", + }, + ], + }); + const buf = await sharp(Buffer.from(svg)).png().toBuffer(); + return buf.buffer.slice( + buf.byteOffset, + buf.byteOffset + buf.byteLength, + ) as ArrayBuffer; +} + +/** + * Crop + resize a source image file to exactly 1200×630 (centered) and + * return it as a base64 JPEG data URI ready for Satori backgroundImage. + */ +export async function imageToOgDataUri(sourcePath: string): Promise<string> { + const buf = await sharp(readFileSync(sourcePath)) + .resize(1200, 630, { fit: "cover", position: "centre" }) + .jpeg({ quality: 85 }) + .toBuffer(); + return `data:image/jpeg;base64,${buf.toString("base64")}`; +} + +// Colors +const BG = "#111111"; +const FG = "#f5f5f3"; +const ACCENT = "#e8587a"; + +function truncate(str: string, max: number): string { + return str.length > max ? `${str.slice(0, max - 1)}…` : str; +} + +export type ArticleOgProps = { + title: string; + description: string; + tags?: string[]; +}; + +export function buildArticleVNode( + props: ArticleOgProps, +): Parameters<typeof satori>[0] { + const title = truncate(props.title, 70); + const description = truncate(props.description, 120); + + return { + type: "div", + props: { + style: { + display: "flex", + flexDirection: "column", + width: "1200px", + height: "630px", + backgroundColor: BG, + color: FG, + fontFamily: "Exo 2", + }, + children: [ + // Top accent bar + { + type: "div", + props: { + style: { + display: "flex", + width: "1200px", + height: "6px", + backgroundColor: ACCENT, + }, + }, + }, + // Content + { + type: "div", + props: { + style: { + display: "flex", + flexDirection: "column", + flex: "1", + paddingTop: "64px", + paddingRight: "80px", + paddingBottom: "48px", + paddingLeft: "80px", + justifyContent: "space-between", + }, + children: [ + // Title + description + { + type: "div", + props: { + style: { display: "flex", flexDirection: "column" }, + children: [ + { + type: "div", + props: { + style: { + display: "flex", + fontSize: "18px", + color: ACCENT, + letterSpacing: "2px", + marginBottom: "20px", + textTransform: "uppercase", + }, + children: "Article", + }, + }, + { + type: "div", + props: { + style: { + display: "flex", + fontSize: "56px", + fontWeight: 700, + color: FG, + lineHeight: "1.1", + marginBottom: "20px", + }, + children: title, + }, + }, + { + type: "div", + props: { + style: { + display: "flex", + fontSize: "24px", + color: "rgba(245,245,243,0.65)", + lineHeight: "1.4", + }, + children: description, + }, + }, + ], + }, + }, + // Footer + { + type: "div", + props: { + style: { + display: "flex", + justifyContent: "flex-end", + borderTop: "1px solid #333333", + paddingTop: "20px", + }, + children: [ + { + type: "div", + props: { + style: { + display: "flex", + fontSize: "22px", + fontWeight: 700, + color: FG, + }, + children: "Adrian Altner", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }; +} + +export type CollectionOgProps = { + title: string; + description: string; + location?: string | undefined; + coverDataUri?: string | undefined; +}; + +export function buildCollectionVNode( + props: CollectionOgProps, +): Parameters<typeof satori>[0] { + const title = truncate(props.title, 60); + const hasImage = !!props.coverDataUri; + + if (hasImage) { + return { + type: "div", + props: { + style: { + display: "flex", + width: "1200px", + height: "630px", + backgroundImage: `url(${props.coverDataUri})`, + backgroundSize: "cover", + backgroundPosition: "center", + fontFamily: "Exo 2", + position: "relative", + }, + children: [ + // Gradient overlay (full card, darker at bottom) + { + type: "div", + props: { + style: { + display: "flex", + position: "absolute", + top: "0px", + left: "0px", + width: "1200px", + height: "630px", + backgroundImage: + "linear-gradient(to top, rgba(0,0,0,0.92) 0%, rgba(0,0,0,0.4) 50%, rgba(0,0,0,0.1) 100%)", + }, + }, + }, + // Top accent bar + { + type: "div", + props: { + style: { + display: "flex", + position: "absolute", + top: "0px", + left: "0px", + width: "1200px", + height: "6px", + backgroundColor: ACCENT, + }, + }, + }, + // Bottom content + { + type: "div", + props: { + style: { + display: "flex", + position: "absolute", + bottom: "0px", + left: "0px", + right: "0px", + paddingTop: "32px", + paddingRight: "64px", + paddingBottom: "48px", + paddingLeft: "64px", + flexDirection: "column", + }, + children: [ + // Location label + ...(props.location + ? [ + { + type: "div", + props: { + style: { + display: "flex", + fontSize: "18px", + color: ACCENT, + letterSpacing: "2px", + textTransform: "uppercase", + marginBottom: "12px", + }, + children: truncate(props.location, 50), + }, + }, + ] + : []), + // Title + { + type: "div", + props: { + style: { + display: "flex", + justifyContent: "space-between", + alignItems: "flex-end", + }, + children: [ + { + type: "div", + props: { + style: { + display: "flex", + fontSize: "64px", + fontWeight: 700, + color: "#ffffff", + lineHeight: "1.05", + }, + children: title, + }, + }, + { + type: "div", + props: { + style: { + display: "flex", + fontSize: "20px", + fontWeight: 700, + color: "rgba(255,255,255,0.7)", + letterSpacing: "1px", + paddingBottom: "8px", + }, + children: "Adrian Altner", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }; + } + + // Text-only fallback (no cover photo) + return { + type: "div", + props: { + style: { + display: "flex", + flexDirection: "column", + width: "1200px", + height: "630px", + backgroundColor: BG, + color: FG, + fontFamily: "Exo 2", + }, + children: [ + { + type: "div", + props: { + style: { + display: "flex", + width: "1200px", + height: "6px", + backgroundColor: ACCENT, + }, + }, + }, + { + type: "div", + props: { + style: { + display: "flex", + flexDirection: "column", + flex: "1", + paddingTop: "64px", + paddingRight: "80px", + paddingBottom: "48px", + paddingLeft: "80px", + justifyContent: "space-between", + }, + children: [ + { + type: "div", + props: { + style: { display: "flex", flexDirection: "column" }, + children: [ + { + type: "div", + props: { + style: { + display: "flex", + fontSize: "18px", + color: ACCENT, + letterSpacing: "2px", + marginBottom: "20px", + textTransform: "uppercase", + }, + children: "Collection", + }, + }, + { + type: "div", + props: { + style: { + display: "flex", + fontSize: "64px", + fontWeight: 700, + color: FG, + }, + children: title, + }, + }, + ], + }, + }, + { + type: "div", + props: { + style: { + display: "flex", + justifyContent: "flex-end", + borderTop: "1px solid #333333", + paddingTop: "20px", + }, + children: [ + { + type: "div", + props: { + style: { + display: "flex", + fontSize: "22px", + fontWeight: 700, + color: FG, + }, + children: "Adrian Altner", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }; +} + +export type NoteOgProps = { + coverDataUri?: string | undefined; +}; + +export function buildNoteVNode( + props: NoteOgProps, +): Parameters<typeof satori>[0] { + if (props.coverDataUri) { + return { + type: "div", + props: { + style: { + display: "flex", + width: "1200px", + height: "630px", + backgroundImage: `url(${props.coverDataUri})`, + backgroundSize: "cover", + backgroundPosition: "center", + }, + }, + }; + } + + return { + type: "div", + props: { + style: { + display: "flex", + width: "1200px", + height: "630px", + backgroundColor: BG, + }, + }, + }; +} + +export type PhotoOgProps = { + title: string; + alt: string; + date: string; + location?: string | undefined; + photoDataUri: string; +}; + +export function buildPhotoVNode( + props: PhotoOgProps, +): Parameters<typeof satori>[0] { + const title = truncate(props.title, 70); + const dateFormatted = new Date(props.date).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + + return { + type: "div", + props: { + style: { + display: "flex", + width: "1200px", + height: "630px", + backgroundImage: `url(${props.photoDataUri})`, + backgroundSize: "cover", + fontFamily: "Exo 2", + position: "relative", + }, + children: [ + // Gradient overlay (darker at bottom for text legibility) + { + type: "div", + props: { + style: { + display: "flex", + position: "absolute", + top: "0px", + left: "0px", + width: "1200px", + height: "630px", + backgroundImage: + "linear-gradient(to top, rgba(0,0,0,0.88) 0%, rgba(0,0,0,0.3) 50%, rgba(0,0,0,0.05) 100%)", + }, + }, + }, + // Bottom content + { + type: "div", + props: { + style: { + display: "flex", + position: "absolute", + bottom: "0px", + left: "0px", + right: "0px", + paddingTop: "32px", + paddingRight: "64px", + paddingBottom: "48px", + paddingLeft: "64px", + flexDirection: "column", + }, + children: [ + // Title + { + type: "div", + props: { + style: { + display: "flex", + fontSize: "52px", + fontWeight: 700, + color: "#ffffff", + lineHeight: "1.05", + marginBottom: "16px", + }, + children: title, + }, + }, + // Date left, site name right + { + type: "div", + props: { + style: { + display: "flex", + justifyContent: "space-between", + alignItems: "flex-end", + }, + children: [ + { + type: "div", + props: { + style: { + display: "flex", + fontSize: "18px", + color: "rgba(255,255,255,0.6)", + }, + children: dateFormatted, + }, + }, + { + type: "div", + props: { + style: { + display: "flex", + fontSize: "20px", + fontWeight: 700, + color: "rgba(255,255,255,0.7)", + letterSpacing: "1px", + }, + children: "Adrian Altner", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }; +} diff --git a/src/lib/photo-albums.ts b/src/lib/photo-albums.ts new file mode 100644 index 0000000..69b9bee --- /dev/null +++ b/src/lib/photo-albums.ts @@ -0,0 +1,77 @@ +import { access, readdir } from "node:fs/promises"; +import { dirname, join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export const PHOTOS_DIRECTORY = join(__dirname, "../content/photos/albums"); + +export interface PhotoDirectory { + name: string; + imagePaths: string[]; +} + +async function findJpgFiles(dir: string, baseDir: string): Promise<string[]> { + const results: string[] = []; + const entries = await readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + const nested = await findJpgFiles(fullPath, baseDir); + results.push(...nested); + } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".jpg")) { + results.push(relative(baseDir, fullPath)); + } + } + return results; +} + +export async function getPhotoDirectories( + photosDirectory: string, +): Promise<PhotoDirectory[]> { + const entries = await readdir(photosDirectory, { withFileTypes: true }); + const dirs = entries.filter((e) => e.isDirectory()); + + const result: PhotoDirectory[] = []; + for (const dir of dirs) { + const dirPath = join(photosDirectory, dir.name); + const imagePaths = await findJpgFiles(dirPath, photosDirectory); + if (imagePaths.length > 0) { + result.push({ name: dir.name, imagePaths }); + } + } + return result; +} + +export async function getImagesMissingMetadata( + photosDirectory: string, +): Promise<string[]> { + const allImagePaths = await findJpgFiles(photosDirectory, photosDirectory); + const missing: string[] = []; + + for (const imagePath of allImagePaths) { + const jsonPath = getMetadataPathForImage(imagePath, photosDirectory); + try { + await access(jsonPath); + } catch { + missing.push(imagePath); + } + } + return missing; +} + +export function getMetadataPathForImage( + relativeImagePath: string, + photosDirectory: string, +): string { + const jsonRelativePath = relativeImagePath.replace(/\.jpg$/i, ".json"); + return join(photosDirectory, jsonRelativePath); +} + +export function getPhotoAbsolutePath( + relativeImagePath: string, + photosDirectory: string, +): string { + return join(photosDirectory, relativeImagePath); +} diff --git a/src/lib/remark-obsidian-links.mjs b/src/lib/remark-obsidian-links.mjs new file mode 100644 index 0000000..a6d227b --- /dev/null +++ b/src/lib/remark-obsidian-links.mjs @@ -0,0 +1,180 @@ +/** + * Remark plugin that converts Obsidian-style [[wiki-links]] to standard + * Markdown links by resolving filenames against the content directory. + * + * Supported syntax: + * [[filename]] → [filename](/url/to/filename/) + * [[filename|display text]] → [display text](/url/to/filename/) + * [[filename#heading]] → heading fragment is preserved in the URL + * ![[image.jpg]] → ![image.jpg](./relative/path/image.jpg) + * ![[image.jpg|alt text]] → ![alt text](./relative/path/image.jpg) + * + * Content sources and their URL prefixes: + * src/content/blog/posts → /blog/{slug}/ + * src/content/notes → /notes/{slug}/ + * src/content/projects/project → /projects/{slug}/ + */ + +import { readdirSync } from "node:fs"; +import { dirname, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { SKIP, visit } from "unist-util-visit"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const SOURCES = [ + { + base: resolve(__dirname, "../content/blog/posts"), + urlPrefix: "/blog", + }, + { + base: resolve(__dirname, "../content/notes"), + urlPrefix: "/notes", + }, + { + base: resolve(__dirname, "../content/projects/project"), + urlPrefix: "/projects", + }, +]; + +const IMAGE_EXT_RE = /\.(jpe?g|png|gif|webp|svg|avif)$/i; + +function buildFileIndex() { + const map = new Map(); + for (const { base, urlPrefix } of SOURCES) { + let files; + try { + files = readdirSync(base, { recursive: true }); + } catch { + continue; // directory may not exist yet + } + for (const file of files) { + if (!/\.(md|mdx)$/.test(file)) continue; + const slug = file.replace(/\.(md|mdx)$/, "").replace(/\\/g, "/"); + const filename = slug.split("/").pop(); + // First match wins — filename must be unique across collections + if (!map.has(filename)) { + map.set(filename, `${urlPrefix}/${slug}/`); + } + } + } + return map; +} + +function buildImageIndex() { + const map = new Map(); + for (const { base } of SOURCES) { + let files; + try { + files = readdirSync(base, { recursive: true }); + } catch { + continue; + } + for (const file of files) { + if (!IMAGE_EXT_RE.test(file)) continue; + const filename = file.replace(/\\/g, "/").split("/").pop(); + if (!map.has(filename)) { + map.set(filename, resolve(base, file)); + } + } + } + return map; +} + +// Build once per process, not per file +const fileIndex = buildFileIndex(); +const imageIndex = buildImageIndex(); + +// Matches optional leading ! and [[...]] content +const WIKI_LINK_RE = /(!?)\[\[([^\]]+)\]\]/g; + +export function remarkObsidianLinks() { + return (tree, file) => { + visit(tree, "text", (node, index, parent) => { + if (!WIKI_LINK_RE.test(node.value)) return; + WIKI_LINK_RE.lastIndex = 0; + + const nodes = []; + let last = 0; + let match = WIKI_LINK_RE.exec(node.value); + + while (match !== null) { + if (match.index > last) { + nodes.push({ + type: "text", + value: node.value.slice(last, match.index), + }); + } + + const embed = match[1] === "!"; + const inner = match[2]; + const pipeIdx = inner.indexOf("|"); + const ref = pipeIdx === -1 ? inner : inner.slice(0, pipeIdx); + const label = + pipeIdx === -1 + ? ref.split("#")[0].trim() + : inner.slice(pipeIdx + 1).trim(); + const [filename, heading] = ref.trim().split("#"); + const trimmedFilename = filename.trim(); + + // Image embed: ![[image.ext]] + if (embed && IMAGE_EXT_RE.test(trimmedFilename)) { + const absImagePath = imageIndex.get(trimmedFilename); + if (absImagePath && file?.path) { + const relPath = + "./" + + relative(dirname(file.path), absImagePath).replace(/\\/g, "/"); + nodes.push({ + type: "image", + url: relPath, + alt: label || trimmedFilename, + title: null, + }); + } else { + // Fallback: emit as link + nodes.push({ + type: "link", + url: `#${trimmedFilename}`, + title: null, + children: [{ type: "text", value: label || trimmedFilename }], + }); + } + } else { + // Regular wiki-link + const base = fileIndex.get(trimmedFilename); + const url = base + ? heading + ? `${base}#${heading.trim()}` + : base + : `#${trimmedFilename}`; + + nodes.push({ + type: "link", + url, + title: null, + data: { + hName: "a", + hProperties: { + href: url, + target: "_blank", + rel: "noopener noreferrer", + }, + hChildren: [{ type: "text", value: label }], + }, + children: [{ type: "text", value: label }], + }); + } + + last = match.index + match[0].length; + match = WIKI_LINK_RE.exec(node.value); + } + + if (last < node.value.length) { + nodes.push({ type: "text", value: node.value.slice(last) }); + } + + parent.children.splice(index, 1, ...nodes); + return [SKIP, index + nodes.length]; + }); + }; +} diff --git a/src/lib/webmentions.ts b/src/lib/webmentions.ts new file mode 100644 index 0000000..c9f6553 --- /dev/null +++ b/src/lib/webmentions.ts @@ -0,0 +1,364 @@ +interface Author { + name?: string; + photo?: string; + url?: string; +} + +export interface Mention { + "wm-property": string; + "wm-id": number; + url: string; + author?: Author; + published?: string; + content?: { text?: string; html?: string }; +} + +const WEBMENTION_IO_API = "https://webmention.io/api/mentions.jf2"; +const SOCIAL_PROPERTIES = new Set([ + "like-of", + "repost-of", + "in-reply-to", + "mention-of", +]); + +const mentionsCache = new Map< + string, + { expiresAt: number; mentions: Mention[] } +>(); +const sourceExistsCache = new Map< + string, + { expiresAt: number; exists: boolean } +>(); + +const MENTIONS_CACHE_TTL_MS = 60_000; +const SOURCE_EXISTS_TRUE_CACHE_TTL_MS = 10 * 60_000; +const SOURCE_EXISTS_FALSE_CACHE_TTL_MS = 60_000; +const MAX_CONCURRENCY = 6; +const FETCH_TIMEOUT_MS = 8_000; + +type MastodonInteractionCheck = { + kind: "mastodon"; + origin: string; + statusId: string; + actorId: string; + action: "favourited_by" | "reblogged_by"; +}; + +type MastodonStatusRef = { + kind: "mastodon"; + origin: string; + statusId: string; +}; + +function now() { + return Date.now(); +} + +function cacheSourceExists( + sourceUrl: string, + exists: boolean, + ttlMs = exists + ? SOURCE_EXISTS_TRUE_CACHE_TTL_MS + : SOURCE_EXISTS_FALSE_CACHE_TTL_MS, +) { + sourceExistsCache.set(sourceUrl, { + exists, + expiresAt: now() + ttlMs, + }); +} + +function getHostname(rawUrl: string) { + try { + return new URL(rawUrl).hostname.toLowerCase(); + } catch { + return ""; + } +} + +function isLikelySocialSource(url: string) { + const host = getHostname(url); + if (!host) return false; + if (host.includes("mastodon")) return true; + if (host.endsWith(".social")) return true; + return false; +} + +function shouldValidateSource(mention: Mention) { + if (!mention.url) return false; + if (!SOCIAL_PROPERTIES.has(mention["wm-property"])) return false; + return isLikelySocialSource(mention.url); +} + +function withTimeout(ms: number) { + return AbortSignal.timeout(ms); +} + +function getLinkHeaderNextUrl(linkHeader: string | null) { + if (!linkHeader) return null; + const match = linkHeader.match(/<([^>]+)>;\s*rel="next"/i); + return match?.[1] ?? null; +} + +function parseMastodonInteraction( + sourceUrl: string, +): MastodonInteractionCheck | null { + try { + const parsed = new URL(sourceUrl); + const fragment = decodeURIComponent(parsed.hash.replace(/^#/, "")); + if (!fragment) return null; + + const likeMatch = fragment.match(/^(?:favorited|favourited)-by-(\d+)$/i); + const repostMatch = fragment.match(/^reblogged-by-(\d+)$/i); + if (!likeMatch && !repostMatch) return null; + + const statusIdMatch = parsed.pathname.match(/\/(\d+)\/?$/); + const statusId = statusIdMatch?.[1]; + if (!statusId) return null; + + const likedActorId = likeMatch?.[1]; + const repostActorId = repostMatch?.[1]; + + if (likedActorId) { + return { + kind: "mastodon", + origin: parsed.origin, + statusId, + actorId: likedActorId, + action: "favourited_by", + }; + } + + if (!repostActorId) return null; + return { + kind: "mastodon", + origin: parsed.origin, + statusId, + actorId: repostActorId, + action: "reblogged_by", + }; + } catch { + return null; + } +} + +function parseSocialInteraction(sourceUrl: string) { + return parseMastodonInteraction(sourceUrl); +} + +function parseMastodonStatusRef(sourceUrl: string): MastodonStatusRef | null { + try { + const parsed = new URL(sourceUrl); + const statusIdMatch = parsed.pathname.match(/\/(\d+)\/?$/); + const statusId = statusIdMatch?.[1]; + if (!statusId) return null; + if (parsed.hash && parsed.hash.length > 1) return null; + + const host = parsed.hostname.toLowerCase(); + if (!host.includes("mastodon") && !host.endsWith(".social")) return null; + + return { + kind: "mastodon", + origin: parsed.origin, + statusId, + }; + } catch { + return null; + } +} + +function parseSocialPostRef(sourceUrl: string): MastodonStatusRef | null { + return parseMastodonStatusRef(sourceUrl); +} + +async function checkMastodonInteractionExists( + interaction: MastodonInteractionCheck, +) { + let nextUrl: string | null = + `${interaction.origin}/api/v1/statuses/${interaction.statusId}/${interaction.action}?limit=80`; + + for (let page = 0; page < 20 && nextUrl; page++) { + const res = await fetch(nextUrl, { + signal: withTimeout(FETCH_TIMEOUT_MS), + headers: { + "User-Agent": "adrian-altner.com webmentions verifier", + }, + }); + + if (res.status === 404 || res.status === 410) return false; + if (!res.ok) return true; + + const accounts = await res.json(); + if (!Array.isArray(accounts)) return true; + if ( + accounts.some( + (account) => String(account?.id ?? "") === interaction.actorId, + ) + ) { + return true; + } + + nextUrl = getLinkHeaderNextUrl(res.headers.get("link")); + } + + return !!nextUrl; +} + +async function checkMastodonStatusExists(status: MastodonStatusRef) { + const res = await fetch( + `${status.origin}/api/v1/statuses/${status.statusId}`, + { + signal: withTimeout(FETCH_TIMEOUT_MS), + headers: { + "User-Agent": "adrian-altner.com webmentions verifier", + }, + }, + ); + + if (res.status === 404 || res.status === 410) return false; + if (!res.ok) return true; + return true; +} + +async function socialPostStillExists(postRef: MastodonStatusRef) { + return checkMastodonStatusExists(postRef); +} + +async function socialInteractionStillExists( + interaction: MastodonInteractionCheck, +) { + return checkMastodonInteractionExists(interaction); +} + +async function sourceStillExists(sourceUrl: string) { + const cached = sourceExistsCache.get(sourceUrl); + if (cached && cached.expiresAt > now()) return cached.exists; + + try { + const interaction = parseSocialInteraction(sourceUrl); + if (interaction) { + const exists = await socialInteractionStillExists(interaction); + cacheSourceExists(sourceUrl, exists); + return exists; + } + + const postRef = parseSocialPostRef(sourceUrl); + if (postRef) { + const exists = await socialPostStillExists(postRef); + cacheSourceExists(sourceUrl, exists); + return exists; + } + + const res = await fetch(sourceUrl, { + redirect: "follow", + signal: withTimeout(FETCH_TIMEOUT_MS), + headers: { + "User-Agent": "adrian-altner.com webmentions verifier", + }, + }); + + const statusGone = res.status === 404 || res.status === 410; + if (statusGone) { + cacheSourceExists(sourceUrl, false); + return false; + } + + if (!res.ok) { + // Fail open on transient upstream errors. + cacheSourceExists(sourceUrl, true, 60_000); + return true; + } + + const host = getHostname(sourceUrl); + const contentType = res.headers.get("content-type") ?? ""; + const isHtml = contentType.includes("text/html"); + if (isHtml && host.includes("mastodon")) { + const body = (await res.text()).toLowerCase(); + const isDeletedMastodon = + host.includes("mastodon") && + (body.includes("status not found") || + body.includes("this status does not exist")); + if (isDeletedMastodon) { + cacheSourceExists(sourceUrl, false); + return false; + } + } + + cacheSourceExists(sourceUrl, true); + return true; + } catch { + // Fail open on network/runtime problems. + cacheSourceExists(sourceUrl, true, 60_000); + return true; + } +} + +async function runWithConcurrencyLimit<T, R>( + items: T[], + limit: number, + worker: (item: T) => Promise<R>, +) { + const output = new Array<R | undefined>(items.length); + let nextIndex = 0; + + async function runWorker() { + while (nextIndex < items.length) { + const index = nextIndex++; + const item = items[index]; + if (item === undefined) continue; + output[index] = await worker(item); + } + } + + const workers = Array.from( + { length: Math.max(1, Math.min(limit, items.length)) }, + () => runWorker(), + ); + await Promise.all(workers); + return output as R[]; +} + +async function fetchRawWebmentions(target: string) { + const apiUrl = `${WEBMENTION_IO_API}?target=${encodeURIComponent(target)}&per-page=100`; + const res = await fetch(apiUrl, { + signal: withTimeout(FETCH_TIMEOUT_MS), + headers: { + "User-Agent": "adrian-altner.com webmentions fetcher", + }, + }); + + if (!res.ok) return []; + const data = await res.json(); + return (data.children ?? []) as Mention[]; +} + +export async function getFilteredWebmentions(target: string) { + const cached = mentionsCache.get(target); + if (cached && cached.expiresAt > now()) { + return cached.mentions; + } + + const mentions = await fetchRawWebmentions(target); + if (mentions.length === 0) { + mentionsCache.set(target, { + mentions: [], + expiresAt: now() + MENTIONS_CACHE_TTL_MS, + }); + return []; + } + + const checks = await runWithConcurrencyLimit( + mentions, + MAX_CONCURRENCY, + async (mention) => { + if (!shouldValidateSource(mention)) return true; + return sourceStillExists(mention.url); + }, + ); + + const filtered = mentions.filter((_, idx) => checks[idx]); + mentionsCache.set(target, { + mentions: filtered, + expiresAt: now() + MENTIONS_CACHE_TTL_MS, + }); + return filtered; +} diff --git a/src/pages/about.astro b/src/pages/about.astro new file mode 100644 index 0000000..d9cd851 --- /dev/null +++ b/src/pages/about.astro @@ -0,0 +1,249 @@ +--- +import { Image } from "astro:assets"; +import { getCollection } from "astro:content"; +import meBangkok from "@/assets/images/me-bangkok.jpg"; +import JsonLd from "@/components/JsonLd.astro"; +import BaseLayout from "@/layouts/BaseLayout.astro"; + +const recentProjects = ( + await getCollection("projects", ({ data }) => !data.draft) +) + .sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()) + .slice(0, 3); + +const profilePageSchema = { + "@context": "https://schema.org", + "@type": "ProfilePage", + url: "https://adrian-altner.com/about", + mainEntity: { + "@type": "Person", + name: "Adrian Altner", + url: "https://adrian-altner.com", + jobTitle: "Key Account Manager & Amateur Photographer", + sameAs: [ + "https://www.instagram.com/adrian.altner/", + "https://www.linkedin.com/in/adrian-altner/", + "https://github.com/adrian-altner", + ], + }, +}; +--- + +<BaseLayout title="About" description="About Adrian Altner, his passion for the web and photography, and how to get in touch."> + <JsonLd schema={profilePageSchema} slot="head" /> + <div class="page"> + <main class="main"> + <a href="/" class="back">← Home</a> + <div class="about"> + <div class="about__photo-col"> + <Image + src={meBangkok} + alt="Adrian Altner in Bangkok" + class="about__photo" + width={480} + height={640} + /> + </div> + + <div class="about__content"> + <h1 class="about__heading">About me</h1> + <p class="about__tagline">Adrian Altner</p> + + <p class="about__bio"> + I've been passionate about the web since 1997. By day, I work as a Key Account + Manager in IT distribution. I'm self-taught by nature, and in my spare time I + build things for fun and teach myself whatever sparks my curiosity. + </p> + <p class="about__bio"> + Photography is one of my main hobbies. I usually shoot while traveling and on + photowalks, drawn to street scenes, architecture, and the kind of light that never + waits. This site is where those interests come together. + </p> + + </div> + </div> + + <section class="projects"> + <h2 class="projects__heading"><a href="/projects">Projects</a></h2> + <ul class="projects__list"> + {recentProjects.map((p) => ( + <li class="projects__item"> + <a href={`/projects/${p.id}`} class="projects__title">{p.data.title}</a> + <p class="projects__desc">{p.data.description}</p> + </li> + ))} + </ul> + </section> + + </main> + </div> +</BaseLayout> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #f5f5f3; + color: #111; + } + + .main { + max-width: var(--width-content); + margin: 0 auto; + padding: 2rem 2rem 4rem; + width: 100%; + } + + .back { + display: inline-block; + margin-bottom: 1.5rem; + font-size: 0.9rem; + color: #555; + text-decoration: none; + } + + .back:hover { + color: var(--accent); + } + + /* ── Two-column layout ── */ + .about { + display: grid; + grid-template-columns: 48% 52%; + gap: 0; + } + + /* ── Photo column ── */ + .about__photo-col { + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + } + + .about__photo { + width: 100%; + height: 100%; + object-fit: contain; + object-position: center; + display: block; + } + + /* ── Content column ── */ + .about__content { + padding: 3rem 2.5rem; + display: flex; + flex-direction: column; + justify-content: center; + max-width: 560px; + } + + .about__heading { + font-size: clamp(1.75rem, 3vw, 2.25rem); + font-weight: 700; + letter-spacing: -0.03em; + line-height: 1; + text-transform: uppercase; + color: #1a1a2e; + margin-bottom: 0.75rem; + } + + .about__tagline { + font-size: 1.1rem; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--accent); + margin-bottom: 2rem; + } + + .about__bio { + font-size: 0.975rem; + line-height: 1.8; + color: #444; + margin-bottom: 1rem; + } + + /* ── Projects ── */ + .projects { + margin-top: 3rem; + padding-top: 2rem; + border-top: 1px solid #e0e0de; + } + + .projects__heading { + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #888; + margin-bottom: 1rem; + } + + .projects__heading a { + color: inherit; + text-decoration: none; + } + + .projects__heading a:hover { + color: #111; + } + + .projects__list { + list-style: none; + padding: 0; + display: flex; + flex-direction: column; + } + + .projects__item { + display: flex; + flex-direction: column; + gap: 0.2rem; + padding: 0.75rem 0; + border-bottom: 1px solid #e0e0de; + } + + .projects__item:first-child { + border-top: 1px solid #e0e0de; + } + + .projects__title { + font-size: 0.875rem; + font-weight: 500; + color: #111; + text-decoration: none; + line-height: 1.3; + } + + .projects__title:hover { + color: var(--accent); + } + + .projects__desc { + font-size: 0.875rem; + color: #666; + line-height: 1.4; + margin: 0; + } + + /* ── Mobile ── */ + @media (max-width: 600px) { + .about { + grid-template-columns: 1fr; + } + + .about__photo-col { + height: 520px; + } + + .about__photo { + object-fit: cover; + object-position: center 85%; + } + + .about__content { + padding: 2rem 1.5rem 3rem; + } + } +</style> diff --git a/src/pages/archives/index.astro b/src/pages/archives/index.astro new file mode 100644 index 0000000..b4b5c8f --- /dev/null +++ b/src/pages/archives/index.astro @@ -0,0 +1,222 @@ +--- +import { getCollection } from "astro:content"; +import BaseLayout from "@/layouts/BaseLayout.astro"; +import { collectionSlug, getLinks } from "@/lib/collections"; + +const posts = (await getCollection("blog", ({ data }) => !data.draft)).map( + (p) => ({ + title: p.data.title, + date: p.data.publishDate, + url: `/blog/${p.id}`, + type: "post" as const, + }), +); + +const notes = (await getCollection("notes", ({ data }) => !data.draft)).map( + (n) => ({ + title: n.data.title, + date: n.data.publishDate, + url: `/notes/${n.id}`, + type: "note" as const, + }), +); + +const photoSets = ( + await getCollection( + "collections_photos", + ({ data }) => data.set && !data.draft && !!data.publishDate, + ) +).map((c) => ({ + title: c.data.title, + date: c.data.publishDate as Date, + url: `/photos/collections/${collectionSlug(c)}`, + type: "photos" as const, +})); + +const links = (await getLinks()).map((l) => ({ + title: l.data.title, + date: l.data.publishDate, + url: `/links/${l.id}`, + type: "link" as const, +})); + +const all = [...posts, ...notes, ...photoSets, ...links].sort( + (a, b) => b.date.valueOf() - a.date.valueOf(), +); + +// Group by year +const byYear = new Map<number, typeof all>(); +for (const entry of all) { + const year = entry.date.getFullYear(); + if (!byYear.has(year)) byYear.set(year, []); + byYear.get(year)?.push(entry); +} +const years = [...byYear.keys()].sort((a, b) => b - a); + +function formatDate(date: Date) { + return date.toLocaleDateString("de-DE", { month: "2-digit", day: "2-digit" }); +} + +const typeLabel: Record<string, string> = { + post: "Article", + note: "Note", + photos: "Photo Collection", + link: "Link", +}; +--- + +<BaseLayout + title="Archives" + description="Chronologisches Archiv aller Posts und Notizen von Adrian Altner." +> + <div class="page"> + <main class="main"> + <a href="/" class="back">← Home</a> + <h1 class="heading">Archives</h1> + { + years.length === 0 ? ( + <p class="empty">Noch keine Inhalte vorhanden.</p> + ) : ( + years.map((year) => ( + <section class="year"> + <h2 class="year__heading">{year}</h2> + <ul class="list"> + {byYear.get(year)!.map((entry) => ( + <li class="item"> + <time class="item__date" datetime={entry.date.toISOString()}> + {formatDate(entry.date)} + </time> + <a href={entry.url} class="item__title"> + {entry.title} + </a> + <span class={`item__type item__type--${entry.type}`}> + {typeLabel[entry.type]} + </span> + </li> + ))} + </ul> + </section> + )) + ) + } + </main> + </div> +</BaseLayout> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #f5f5f3; + color: #111; + } + + .main { + max-width: var(--width-prose); + margin: 0 auto; + padding: 2rem 2rem 4rem; + width: 100%; + } + + .back { + display: inline-block; + margin-bottom: 1.5rem; + font-size: 0.9rem; + color: #555; + text-decoration: none; + } + + .back:hover { + color: var(--accent); + } + + .heading { + font-size: 1.75rem; + font-weight: 400; + letter-spacing: -0.02em; + margin-bottom: 2.5rem; + } + + .empty { + color: #888; + font-size: 0.95rem; + } + + .year { + margin-bottom: 2.5rem; + } + + .year__heading { + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #888; + margin-bottom: 0.5rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid #111; + } + + .list { + list-style: none; + padding: 0; + } + + .item { + display: flex; + align-items: baseline; + gap: 0.75rem; + padding: 0.55rem 0; + border-bottom: 1px solid #e8e8e6; + } + + .item__date { + font-size: 0.8rem; + color: #888; + white-space: nowrap; + flex-shrink: 0; + width: 3.5rem; + } + + .item__title { + font-size: 0.95rem; + color: #111; + text-decoration: none; + flex: 1; + } + + .item__title:hover { + color: var(--accent); + } + + .item__type { + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + padding: 0.15rem 0.4rem; + border-radius: 3px; + flex-shrink: 0; + } + + .item__type--post { + background: #e8e8e6; + color: #555; + } + + .item__type--note { + background: #e8f0e8; + color: #4a7a4a; + } + + .item__type--photos { + background: #e8eef5; + color: #3a5f8a; + } + + .item__type--link { + background: #f5ece8; + color: #8a4a3a; + } +</style> diff --git a/src/pages/blog/[...slug].astro b/src/pages/blog/[...slug].astro new file mode 100644 index 0000000..76569b6 --- /dev/null +++ b/src/pages/blog/[...slug].astro @@ -0,0 +1,477 @@ +--- +import { Image } from "astro:assets"; +import { getCollection, render } from "astro:content"; +import BlogPostSeries from "@/components/BlogPostSeries.astro"; +import JsonLd from "@/components/JsonLd.astro"; +import TableOfContents from "@/components/TableOfContents.astro"; +import WebMentions from "@/components/WebMentions.astro"; +import BaseLayout from "@/layouts/BaseLayout.astro"; +export async function getStaticPaths() { + const posts = (await getCollection("blog", ({ data }) => !data.draft)).sort( + (a, b) => a.data.publishDate.valueOf() - b.data.publishDate.valueOf(), + ); + return posts.map((post, i) => { + const prev = posts[i - 1]; + const next = posts[i + 1]; + return { + params: { slug: post.id }, + props: { + post, + prevPost: prev ? { id: prev.id, title: prev.data.title } : null, + nextPost: next ? { id: next.id, title: next.data.title } : null, + }, + }; + }); +} + +const { post, prevPost, nextPost } = Astro.props; +const { Content, headings } = await render(post); +const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3); +function capitalize(s: string) { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +const allPosts = await getCollection("blog"); +const seriesChildren = allPosts + .filter((a) => a.data.seriesParent === post.id && !a.data.draft) + .sort((a, b) => (a.data.seriesOrder ?? 99) - (b.data.seriesOrder ?? 99)); + +// For child posts: find parent + siblings +const seriesParentPost = post.data.seriesParent + ? allPosts.find((a) => a.id === post.data.seriesParent) + : undefined; +const seriesSiblings = seriesParentPost + ? allPosts + .filter( + (a) => a.data.seriesParent === seriesParentPost.id && !a.data.draft, + ) + .sort((a, b) => (a.data.seriesOrder ?? 99) - (b.data.seriesOrder ?? 99)) + : []; + +// Series data for sidebar + bottom block +const hasSeries = seriesChildren.length > 0 || seriesSiblings.length > 0; + +function formatDate(date: Date) { + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); +} + +const ogImageUrl = new URL(`/og/blog/${post.id}.png`, Astro.site).toString(); +const postUrl = new URL(`/blog/${post.id}/`, Astro.site).toString(); +const postSchema = { + "@context": "https://schema.org", + "@type": "Article", + headline: post.data.title, + description: post.data.description, + datePublished: post.data.publishDate.toISOString(), + image: ogImageUrl, + url: postUrl, + author: { + "@type": "Person", + name: "Adrian Altner", + url: "https://adrian-altner.com", + }, +}; +--- + +<BaseLayout + title={post.data.title} + description={post.data.description} + image={{ src: ogImageUrl, alt: post.data.title }} +> + <JsonLd schema={postSchema} slot="head" /> + <div class="page"> + <div class="main"> + <a href="/blog" class="back">← Back to Blog</a> + <div class="content"> + <article class="h-entry"> + <a + href={postUrl} + class="u-url" + aria-hidden="true" + tabindex="-1" + style="display:none">Permalink</a + > + <span class="p-author h-card" style="display:none" + ><img + class="u-photo" + src="https://adrian-altner.com/avatar.jpg" + alt="Adrian Altner" + /><span class="p-name">Adrian Altner</span><a + class="u-url" + href="https://adrian-altner.com">adrian-altner.com</a + ></span + > + <header class="post-header"> + <h1 class="post-title p-name">{post.data.title}</h1> + <div class="post-meta"> + <time + class="dt-published" + datetime={post.data.publishDate.toISOString()} + > + {formatDate(post.data.publishDate)} + </time> + { + post.data.tags.length > 0 && ( + <div class="post-tags"> + {post.data.tags.map((tag) => ( + <a href={`/tags/${tag}`} class="tag"> + {capitalize(tag)} + </a> + ))} + </div> + ) + } + </div> + </header> + {post.data.cover && ( + <Image class="post-cover" src={post.data.cover} alt={post.data.coverAlt ?? ""} /> + )} + <div class="prose e-content"> + <Content /> + </div> + { + seriesChildren.length > 0 && ( + <BlogPostSeries + children={seriesChildren} + currentId={post.id} + parent={post} + /> + ) + } + { + seriesParentPost && seriesSiblings.length > 0 && ( + <BlogPostSeries + children={seriesSiblings} + currentId={post.id} + parent={seriesParentPost} + /> + ) + } + <WebMentions url={postUrl} /> + </article> + { + (tocHeadings.length > 1 || hasSeries) && ( + <aside class="toc-sidebar"> + {tocHeadings.length > 1 && ( + <TableOfContents headings={tocHeadings} /> + )} + {seriesChildren.length > 0 && ( + <BlogPostSeries + children={seriesChildren} + currentId={post.id} + parent={post} + sidebar={true} + /> + )} + {seriesParentPost && seriesSiblings.length > 0 && ( + <BlogPostSeries + children={seriesSiblings} + currentId={post.id} + parent={seriesParentPost} + sidebar={true} + /> + )} + </aside> + ) + } + </div> + { + (prevPost || nextPost) && ( + <nav class="post-pagination" aria-label="Post navigation"> + <div class="pagination-prev"> + {nextPost && ( + <a href={`/blog/${nextPost.id}`} class="pagination-link"> + <span class="pagination-label">← Newer</span> + <span class="pagination-title">{nextPost.title}</span> + </a> + )} + </div> + <div class="pagination-next"> + {prevPost && ( + <a + href={`/blog/${prevPost.id}`} + class="pagination-link pagination-link--next" + > + <span class="pagination-label">Older →</span> + <span class="pagination-title">{prevPost.title}</span> + </a> + )} + </div> + </nav> + ) + } + <a href="/blog" class="back">← Back to Blog</a> + </div> + </div> +</BaseLayout> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #f5f5f3; + color: #111; + } + + .main { + max-width: var(--width-content); + margin: 0 auto; + padding: 2rem 2rem 4rem; + width: 100%; + } + + .content { + display: grid; + grid-template-columns: 1fr var(--width-sidebar-toc); + gap: 3rem; + align-items: start; + } + + .toc-sidebar { + min-width: 0; + position: sticky; + top: 2rem; + max-height: calc(100vh - 4rem); + overflow-y: auto; + } + + @media (max-width: 900px) { + .content { + grid-template-columns: 1fr; + } + + .toc-sidebar { + display: none; + } + } + + article { + min-width: 0; + max-width: var(--width-prose); + } + + .post-header { + margin-bottom: 2.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid #e0e0de; + } + + .post-title { + font-weight: 400; + letter-spacing: -0.02em; + margin-bottom: 0.75rem; + } + + .post-meta { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + } + + .post-meta time { + font-size: 0.875rem; + color: #888; + } + + .post-tags { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + } + + .tag { + font-size: 0.75rem; + color: var(--accent); + text-decoration: none; + border: 1px solid var(--accent); + border-radius: 3px; + padding: 0.15rem 0.5rem; + opacity: 0.8; + } + + .tag:hover { + opacity: 1; + } + + .post-cover { + width: 100%; + height: auto; + border-radius: 6px; + display: block; + margin-bottom: 2rem; + } + + .prose { + font-size: 1rem; + line-height: 1.7; + color: #333; + } + + .prose :global(h2) { + font-size: var(--text-md); + font-weight: 500; + margin: 2rem 0 0.75rem; + color: #111; + } + + .prose :global(h3) { + font-size: var(--text-sm); + font-weight: 500; + margin: 1.5rem 0 0.5rem; + color: #111; + } + + .prose :global(p) { + margin-bottom: 1.25rem; + } + + .prose :global(a) { + color: var(--accent); + text-underline-offset: 3px; + } + + .prose :global(blockquote) { + border-left: 3px solid var(--accent); + margin: 1.5rem 0; + padding: 0.25rem 0 0.25rem 1.25rem; + color: #555; + font-style: italic; + } + + .prose :global(code) { + font-family: ui-monospace, monospace; + font-size: 0.875em; + background: #e8e8e6; + padding: 0.1em 0.35em; + border-radius: 3px; + } + + .prose :global(pre) { + background: #2b2b2b; + color: #e0e0e0; + padding: 1.25rem; + border-radius: 6px; + overflow-x: auto; + margin: 1.5rem 0; + } + + .prose :global(pre code) { + background: none; + padding: 0; + font-size: 0.875rem; + } + + .prose :global(table) { + width: 100%; + border-collapse: collapse; + margin: 1.75rem 0; + font-size: 0.95rem; + line-height: 1.6; + } + + .prose :global(thead) { + border-bottom: 2px solid #d7d7d2; + } + + .prose :global(tbody tr) { + border-bottom: 1px solid #e0e0de; + } + + .prose :global(tbody tr:last-child) { + border-bottom: none; + } + + .prose :global(th), + .prose :global(td) { + padding: 0.8rem 1rem 0.8rem 0; + text-align: left; + vertical-align: top; + overflow-wrap: anywhere; + } + + .prose :global(th:last-child), + .prose :global(td:last-child) { + padding-right: 0; + } + + .prose :global(th) { + font-weight: 600; + color: #111; + } + + .prose :global(ul), + .prose :global(ol) { + padding-left: 1.5rem; + margin-bottom: 1.25rem; + } + + .prose :global(li) { + margin-bottom: 0.4rem; + } + + .back { + display: inline-block; + font-size: 0.9rem; + color: #555; + text-decoration: none; + border-bottom: 1px solid #ccc; + padding-bottom: 1px; + margin-bottom: 1.5rem; + } + + + .back:hover { + color: var(--accent); + border-color: var(--accent); + } + + .post-pagination { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + max-width: var(--width-prose); + margin: 1rem 0 2rem; + padding-top: 2rem; + border-top: 1px solid #e0e0de; + } + + .pagination-next { + text-align: right; + } + + .pagination-link { + display: flex; + flex-direction: column; + gap: 0.25rem; + text-decoration: none; + color: inherit; + } + + .pagination-link--next { + align-items: flex-end; + } + + .pagination-label { + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--accent); + } + + .pagination-title { + font-size: 0.9rem; + color: #333; + line-height: 1.4; + } + + .pagination-link:hover .pagination-title { + color: var(--accent); + } +</style> diff --git a/src/pages/blog/category/[slug].astro b/src/pages/blog/category/[slug].astro new file mode 100644 index 0000000..64770f1 --- /dev/null +++ b/src/pages/blog/category/[slug].astro @@ -0,0 +1,142 @@ +--- +import { Image } from "astro:assets"; +import { getCollection, render } from "astro:content"; +import BlogPostItem from "@/components/BlogPostItem.astro"; +import BaseLayout from "@/layouts/BaseLayout.astro"; + +export async function getStaticPaths() { + const categories = await getCollection("categories"); + const allPosts = await getCollection("blog", ({ data }) => !data.draft); + + return categories.map((category) => ({ + params: { slug: category.id }, + props: { + category, + posts: allPosts + .filter((p) => p.data.category?.id === category.id) + .sort( + (a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf(), + ), + }, + })); +} + +const { category, posts } = Astro.props; +const { Content } = await render(category); + +const allPosts = await getCollection("blog", ({ data }) => !data.draft); +const seriesCountMap = new Map<string, number>(); +for (const post of allPosts) { + if (post.data.seriesParent) { + seriesCountMap.set( + post.data.seriesParent, + (seriesCountMap.get(post.data.seriesParent) ?? 1) + 1, + ); + } +} +--- + +<BaseLayout title={category.data.title} description={category.data.description}> + <div class="page"> + <main class="main"> + <a href="/blog" class="back">← Blog</a> + <p class="eyebrow">Category</p> + <h1 class="heading">{category.data.title}</h1> + {category.data.cover && ( + <Image class="category-cover" src={category.data.cover} alt={category.data.coverAlt ?? ""} /> + )} + <div class="category-intro"> + <Content /> + </div> + <p class="count">{posts.length} article{posts.length !== 1 ? "s" : ""}</p> + <ul class="list"> + {posts.map((post) => ( + <BlogPostItem post={post} seriesCountMap={seriesCountMap} /> + ))} + </ul> + </main> + </div> +</BaseLayout> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #f5f5f3; + color: #111; + } + + .main { + max-width: var(--width-prose); + margin: 0 auto; + padding: 2rem 2rem 4rem; + width: 100%; + } + + .back { + display: inline-block; + font-size: 0.9rem; + color: #555; + text-decoration: none; + border-bottom: 1px solid #ccc; + padding-bottom: 1px; + margin-bottom: 1.5rem; + } + + .back:hover { + color: var(--accent); + border-color: var(--accent); + } + + .eyebrow { + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--accent); + margin-bottom: 0.4rem; + } + + .heading { + font-weight: 400; + letter-spacing: -0.02em; + margin-bottom: 1rem; + } + + .category-cover { + width: 100%; + height: auto; + border-radius: 6px; + display: block; + margin-bottom: 1.5rem; + } + + .category-intro { + font-size: 0.95rem; + line-height: 1.6; + color: #555; + margin-bottom: 0.5rem; + } + + .count { + font-size: 0.875rem; + color: #888; + margin-bottom: 2.5rem; + } + + .list { + list-style: none; + padding: 0; + display: flex; + flex-direction: column; + gap: 1.5rem; + } + + @media (max-width: 640px) { + .item__link { + flex-direction: column; + gap: 0.2rem; + } + } +</style> diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro new file mode 100644 index 0000000..e166780 --- /dev/null +++ b/src/pages/blog/index.astro @@ -0,0 +1,283 @@ +--- +import { getCollection } from "astro:content"; +import BlogPostItem from "@/components/BlogPostItem.astro"; +import BaseLayout from "@/layouts/BaseLayout.astro"; + +const articles = (await getCollection("blog", ({ data }) => !data.draft)).sort( + (a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf(), +); + +const allCategories = await getCollection("categories"); +const categoryCounts = new Map<string, number>(); +for (const post of articles) { + if (post.data.category) { + categoryCounts.set( + post.data.category.id, + (categoryCounts.get(post.data.category.id) ?? 0) + 1, + ); + } +} +const categories = allCategories + .map((cat) => ({ ...cat, count: categoryCounts.get(cat.id) ?? 0 })) + .sort( + (a, b) => + a.data.order - b.data.order || a.data.title.localeCompare(b.data.title), + ); +const categoryTitleMap = new Map( + allCategories.map((c) => [c.id, c.data.title]), +); + +const seriesCountMap = new Map<string, number>(); +for (const post of articles) { + if (post.data.seriesParent) { + seriesCountMap.set( + post.data.seriesParent, + (seriesCountMap.get(post.data.seriesParent) ?? 1) + 1, + ); + } +} + +// Aggregate tags with counts +function capitalize(s: string) { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +const tagCounts = new Map<string, number>(); +for (const post of articles) { + for (const tag of post.data.tags) { + tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1); + } +} +const tags = [...tagCounts.entries()] + .sort((a, b) => b[1] - a[1]) + .map(([id, count]) => ({ id, title: capitalize(id), count })); +--- + +<BaseLayout title="Blog" description="Blog by Adrian Altner."> + <div class="page"> + <div class="main"> + <a href="/" class="back">← Home</a> + <div class="content"> + <main> + <h1 class="heading">Blog</h1> + <ul class="list"> + { + articles.map((post) => ( + <BlogPostItem + post={post} + categoryTitleMap={categoryTitleMap} + seriesCountMap={seriesCountMap} + /> + )) + } + </ul> + </main> + + <aside class="sidebar"> + { + categories.length > 0 && ( + <section class="sidebar__section"> + <h2 class="sidebar__heading">Categories</h2> + <ul class="sidebar__list"> + {categories.map((cat) => ( + <li> + <a + href={`/blog/category/${cat.id}`} + class="sidebar__link" + > + {cat.data.title} + <span class="sidebar__tag-count">{cat.count}</span> + </a> + </li> + ))} + </ul> + </section> + ) + } + + { + tags.length > 0 && ( + <section class="sidebar__section"> + <h2 class="sidebar__heading">Tags</h2> + <div class="sidebar__tags"> + {tags.map(({ id, title, count }) => ( + <a href={`/tags/${id}`} class="sidebar__tag"> + {title} + <span class="sidebar__tag-count">{count}</span> + </a> + ))} + </div> + <a href="/tags" class="sidebar__link-all"> + View All + </a> + </section> + ) + } + </aside> + </div> + </div> + </div> +</BaseLayout> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #f5f5f3; + color: #111; + } + + .main { + max-width: var(--width-content); + margin: 0 auto; + padding: 2rem 2rem 4rem; + width: 100%; + } + + .content { + display: grid; + grid-template-columns: 1fr var(--width-sidebar); + gap: 4rem; + align-items: start; + } + + .back { + display: inline-block; + font-size: 0.9rem; + color: #555; + text-decoration: none; + border-bottom: 1px solid #ccc; + padding-bottom: 1px; + margin-bottom: 1.5rem; + } + + .back:hover { + color: var(--accent); + border-color: var(--accent); + } + + /* ── Post list ── */ + .heading { + font-weight: 400; + letter-spacing: -0.02em; + margin-bottom: 2.5rem; + } + + .list { + list-style: none; + padding: 0; + display: flex; + flex-direction: column; + gap: 2rem; + } + + /* ── Sidebar ── */ + .sidebar { + position: sticky; + top: 2rem; + display: flex; + flex-direction: column; + gap: 2rem; + } + + .sidebar__section { + border-top: 2px solid #111; + padding-top: 0.75rem; + } + + .sidebar__heading { + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #888; + margin-bottom: 0.75rem; + } + + .sidebar__list { + list-style: none; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.4rem; + } + + .sidebar__link { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.9rem; + color: #333; + text-decoration: none; + } + + .sidebar__link-all { + display: block; + font-size: 0.9rem; + color: #333; + text-decoration: none; + margin-top: 1rem; + } + + .sidebar__tags { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + } + + .sidebar__tag { + display: inline-flex; + align-items: center; + gap: 0.3rem; + font-size: 0.8rem; + color: #444; + text-decoration: none; + background: #e8e8e6; + border-radius: 3px; + padding: 0.2rem 0.5rem; + transition: + background 0.15s, + color 0.15s; + } + + .sidebar__tag:hover { + background: var(--accent); + color: #fff; + } + + .sidebar__tag-count { + font-size: 0.7rem; + color: #888; + } + + .sidebar__tag:hover .sidebar__tag-count { + color: rgba(255, 255, 255, 0.75); + } + + /* ── Mobile ── */ + @media (max-width: 768px) { + .content { + grid-template-columns: 1fr; + } + + .sidebar { + position: static; + flex-direction: row; + flex-wrap: wrap; + gap: 1.5rem; + border-top: 1px solid #e0e0de; + padding-top: 2rem; + } + + .sidebar__section { + flex: 1; + min-width: 140px; + } + + .item__link { + flex-direction: column; + gap: 0.2rem; + } + } +</style> diff --git a/src/pages/contact.astro b/src/pages/contact.astro new file mode 100644 index 0000000..bdb3f75 --- /dev/null +++ b/src/pages/contact.astro @@ -0,0 +1,221 @@ +--- +import BaseLayout from "@/layouts/BaseLayout.astro"; +--- + +<BaseLayout title="Contact" description="Get in touch with Adrian Altner."> + <div class="page"> + <main class="main"> + <a href="/" class="back">← Home</a> + <div class="content"> + <h1>Contact</h1> + + <h3>Notes & Feedback</h3> + <p>Feedback and corrections are always welcome. I read everything, but rarely reply — please don't be discouraged if you don't hear back.</p> + + <hr /> + + <h2>How to reach me</h2> + + <div class="cards"> + <div class="card"> + <div class="card__icon" aria-hidden="true"> + <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> + <rect x="2" y="4" width="20" height="16" rx="2"></rect> + <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path> + </svg> + </div> + <div class="card__body"> + <p class="card__label">Email</p> + <a class="card__value r" data-obf="bW9je3RvZH1yZW50bGEtbmFpcmRhe3RhfXllaA==" data-obf-href="bWFpbHRvOmhleUBhZHJpYW4tYWx0bmVyLmNvbQ=="></a> + </div> + </div> + + <div class="card card--placeholder"> + <div class="card__icon" aria-hidden="true"> + <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> + <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.15 12a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.05 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 21 17z"></path> + </svg> + </div> + <div class="card__body"> + <p class="card__label">Signal</p> + <p class="card__value card__value--muted">Coming soon.</p> + </div> + </div> + </div> + + <hr /> + + <div class="note"> + <strong>Please note:</strong> Unsolicited press releases, advertising, and SEO pitches will be deleted without a response. + </div> + </div> + </main> + </div> +</BaseLayout> + +<script> + document.querySelectorAll('[data-obf]').forEach(el => { + el.textContent = atob((el as HTMLElement).dataset.obf!); + }); + document.querySelectorAll('[data-obf-href]').forEach(el => { + (el as HTMLAnchorElement).href = atob((el as HTMLElement).dataset.obfHref!); + }); +</script> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #f5f5f3; + color: #111; + } + + .main { + max-width: var(--width-prose); + margin: 0 auto; + padding: 2rem 2rem 4rem; + width: 100%; + } + + .back { + display: inline-block; + margin-bottom: 1.5rem; + font-size: 0.9rem; + color: #555; + text-decoration: none; + } + + .back:hover { + color: var(--accent); + } + + .content { + font-size: 1rem; + line-height: 1.75; + color: #333; + } + + h1 { + font-size: 1.75rem; + font-weight: 400; + letter-spacing: -0.02em; + margin-bottom: 2rem; + color: #111; + } + + h2 { + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #888; + margin: 2rem 0 1rem; + padding-bottom: 0.4rem; + border-bottom: 2px solid #111; + } + + h3 { + font-size: 1rem; + font-weight: 600; + margin: 0 0 0.5rem; + color: #111; + } + + p { + margin-bottom: 1.25rem; + } + + hr { + border: none; + border-top: 1px solid #ddd; + margin: 2rem 0; + } + + .cards { + display: flex; + flex-direction: column; + gap: 0.75rem; + } + + .card { + display: flex; + align-items: flex-start; + gap: 1rem; + padding: 1rem 1.25rem; + background: #fff; + border: 1px solid #e5e5e3; + border-radius: 8px; + } + + .card--placeholder { + opacity: 0.5; + } + + .card__icon { + flex-shrink: 0; + margin-top: 0.2rem; + color: #555; + } + + .card__body { + display: flex; + flex-direction: column; + gap: 0.1rem; + min-width: 0; + } + + .card__label { + font-size: 0.8rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: #888; + margin: 0; + display: flex; + align-items: center; + gap: 0.5rem; + } + + .card__value { + font-size: clamp(0.8rem, 3.5vw, 1rem); + color: var(--accent); + text-underline-offset: 3px; + margin: 0; + white-space: nowrap; + } + + .card__value--muted { + color: #888; + font-style: italic; + } + + .badge { + font-size: 0.65rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + background: #eeeeed; + color: #888; + padding: 0.15rem 0.45rem; + border-radius: 999px; + } + + .r { + direction: rtl; + unicode-bidi: bidi-override; + } + + .note { + font-size: 0.9rem; + color: #555; + background: #eeeeed; + padding: 0.9rem 1.25rem; + border-radius: 6px; + line-height: 1.6; + } + + .note strong { + color: #111; + } +</style> diff --git a/src/pages/imprint.md b/src/pages/imprint.md new file mode 100644 index 0000000..476488f --- /dev/null +++ b/src/pages/imprint.md @@ -0,0 +1,38 @@ +--- +layout: ../layouts/ProseLayout.astro +title: Imprint +description: Legal notice for adrian-altner.com. +--- + +# Imprint + +## Information pursuant to Section 5 DDG + +<span class="r" data-obf="cmVudGxBIG5haXJkQQ=="></span><br> +<span class="r" data-obf="MTMgLnJ0Uy1kcmFobm9lTC1mbG9kdVI="></span><br> +<span class="r" data-obf="bmVkc2VyRCA3OTAxMA=="></span><br> +<span class="r" data-obf="eW5hbXJlRw=="></span> + +**Phone:** <a class="r" data-obf="MDI0MDM1ODcgNjUxIDk0Kw==" data-obf-href="dGVsOis0OTE1Njc4NTMwNDIw"></a> +**Email:** <a class="r" data-obf="bW9je3RvZH1yZW50bGEtbmFpcmRhe3RhfXllaA==" data-obf-href="bWFpbHRvOmhleUBhZHJpYW4tYWx0bmVyLmNvbQ=="></a> + +--- + +## Responsible for Editorial Content pursuant to Section 18 para. 2 MStV + +<span class="r" data-obf="cmVudGxBIG5haXJkQQ=="></span><br> +<span class="r" data-obf="MTMgLnJ0Uy1kcmFobm9lTC1mbG9kdVI="></span><br> +<span class="r" data-obf="bmVkc2VyRCA3OTAxMA=="></span><br> +<span class="r" data-obf="eW5hbXJlRw=="></span> + +--- + +## Content License + +Unless otherwise noted, content published on this website is licensed under the [Creative Commons Attribution-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-sa/4.0/) license. + +--- + +## Privacy + +Information on the processing of personal data can be found in the [Privacy Policy](/privacy-policy). diff --git a/src/pages/index.astro b/src/pages/index.astro new file mode 100644 index 0000000..2fbdca3 --- /dev/null +++ b/src/pages/index.astro @@ -0,0 +1,447 @@ +--- +import { Image } from "astro:assets"; +import { getCollection } from "astro:content"; +import type { ImageMetadata } from "astro"; +import profilePicture from "@/assets/images/intro.jpg"; +import BaseLayout from "@/layouts/BaseLayout.astro"; +import { + buildCollectionPhotos, + collectionSlug, + type PhotoSidecar, +} from "@/lib/collections"; + +const articles = (await getCollection("blog", ({ data }) => !data.draft)) + .sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()) + .slice(0, 3); + +const notes = (await getCollection("notes", ({ data }) => !data.draft)) + .sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()) + .slice(0, 3); + +const allCollections = await getCollection( + "collections_photos", + ({ data }) => !data.draft, +); +const sidecars = import.meta.glob<PhotoSidecar>( + "/src/content/photos/collections/**/*.json", + { eager: true }, +); +const imageModules = import.meta.glob<{ default: ImageMetadata }>( + "/src/content/photos/collections/**/*.jpg", + { eager: true }, +); + +const photoEntries = allCollections + .flatMap((col) => { + const slug = collectionSlug(col); + return buildCollectionPhotos(sidecars, imageModules, slug).map((p) => ({ + image: p.image, + href: `/photos/collections/${slug}/${p.sidecar.id}`, + date: p.sidecar.date, + })); + }) + .sort((a, b) => a.date.localeCompare(b.date)) + .slice(-6); + +function formatDate(date: Date) { + return date.toLocaleDateString("de-DE", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); +} +--- + +<BaseLayout + title="Adrian Altner" + description="Longtime web enthusiast with a strong interest in technology who enjoys photography, travel, and learning new things in his spare time." + navDesktopHidden={true} +> + <div class="page"> + <!-- Hero --> + <main class="hero"> + <div class="hero__card h-card"> + <div class="hero__photo-wrap"> + <Image + src={profilePicture} + alt="Adrian Altner" + class="hero__photo u-photo" + width={200} + height={200} + /> + </div> + <div class="hero__text"> + <h1 class="hero__name"> + <a href="https://adrian-altner.com" class="u-url p-name" + >Adrian Altner</a> + </h1> + <p class="hero__intro p-note"> + I'm a longtime <strong>web enthusiast</strong> with a strong interest + in + <strong>technology</strong>. In my spare time, I enjoy photography, + traveling, and learning new things on my own. + </p> + </div> + </div> + <div class="hero__divider" aria-hidden="true"></div> + <nav class="hero__nav" aria-label="Main navigation"> + <a href="/blog">Blog</a> + <a href="/photos">Photos</a> + <a href="/notes">Notes</a> + <a href="/links">Links</a> + <a href="/archives">Archives</a> + <a href="/about">About</a> + </nav> + </main> + + <!-- Content columns --> + <section class="content-grid"> + <div class="col"> + <h2 class="col__heading"><a href="/blog">Blog</a></h2> + <ul class="preview-list"> + { + articles.map((a) => ( + <li class="preview-item"> + <time + class="preview-item__date" + datetime={a.data.publishDate.toISOString()} + > + {formatDate(a.data.publishDate)} + </time> + <a href={`/blog/${a.id}`} class="preview-item__title"> + {a.data.title} + </a> + <p class="preview-item__desc">{a.data.description}</p> + </li> + )) + } + </ul> + </div> + + <div class="col"> + <h2 class="col__heading"><a href="/notes">Notes</a></h2> + <ul class="preview-list"> + { + notes.map((n) => ( + <li class="preview-item"> + <time + class="preview-item__date" + datetime={n.data.publishDate.toISOString()} + > + {formatDate(n.data.publishDate)} + </time> + <a href={`/notes/${n.id}`} class="preview-item__title"> + {n.data.title} + </a> + {n.data.description && ( + <p class="preview-item__desc">{n.data.description}</p> + )} + </li> + )) + } + </ul> + </div> + </section> + + <!-- Photo stream --> + { + photoEntries.length > 0 && ( + <section class="photo-stream"> + <h2 class="photo-stream__heading"> + Last photos from <a href="/photos/collections">my collections</a> + </h2> + {photoEntries.map((entry) => ( + <a href={entry.href} class="photo-stream__item"> + <Image + src={entry.image} + alt="" + class="photo-stream__img" + width={400} + height={300} + /> + </a> + ))} + </section> + ) + } + </div> +</BaseLayout> + +<style> + .page { + display: grid; + grid-template-columns: minmax(0, 1fr); + min-height: 100dvh; + color: #111; + } + + /* ── Hero ── */ + .hero { + max-width: var(--width-hero); + margin: 0 auto; + width: 100%; + padding: 5rem 2rem 6rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0; + } + + .hero__divider { + width: 1px; + align-self: stretch; + background: #e0e0de; + margin: 0 2rem; + flex-shrink: 0; + } + + .hero__card { + display: flex; + align-items: center; + max-width: 60ch; + gap: 1.5rem; + } + + .hero__nav { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.4rem 0; + flex-shrink: 0; + } + + .hero__nav a { + font-size: var(--text-xs); + font-weight: 500; + color: #111; + text-decoration: none; + padding: 0.3rem 0.75rem; + border-radius: 4px; + text-align: left; + transition: background 0.15s; + } + + .hero__nav a:hover { + background: #e8e8e6; + } + + .hero__photo-wrap { + width: 160px; + height: 160px; + border-radius: 50%; + overflow: hidden; + flex-shrink: 0; + } + + .hero__photo { + width: 156px; + height: 156px; + object-fit: cover; + transform: scale(1.25); + } + + .hero__name a { + color: inherit; + text-decoration: none; + } + + .hero__name { + font-size: var(--text-xl); + font-weight: 400; + letter-spacing: -0.02em; + line-height: 1.1; + margin-bottom: 0.5rem; + } + + .hero__intro { + font-size: var(--text-xs); + line-height: 1.35; + color: #444; + max-width: 46ch; + } + + .hero__intro strong { + font-weight: 700; + color: #111; + } + + /* ── Content grid ── */ + .content-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + max-width: var(--width-hero); + margin: 0 auto; + padding: 0 2rem 3rem; + width: 100%; + } + + .col__heading { + font-size: var(--text-xs); + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #888; + margin-bottom: 1rem; + } + + .col__heading a { + color: inherit; + text-decoration: none; + } + + .col__heading a:hover { + color: #111; + } + + .preview-list { + list-style: none; + padding: 0; + display: flex; + flex-direction: column; + gap: 0; + } + + .preview-item { + display: flex; + flex-direction: column; + gap: 0.2rem; + padding: 0.75rem 0; + border-bottom: 1px solid #e0e0de; + } + + .preview-item:first-child { + border-top: 1px solid #e0e0de; + } + + .preview-item__date { + color: #999; + } + + .preview-item__title { + font-size: var(--text-xs); + font-weight: 500; + color: #111; + text-decoration: none; + line-height: 1.3; + } + + .preview-item__title:hover { + color: var(--accent); + } + + .preview-item__desc { + color: #666; + line-height: 1.4; + margin: 0; + } + + /* ── Photo stream ── */ + .photo-stream { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.5rem; + max-width: var(--width-hero); + margin: 0 auto; + padding: 0 2rem 4rem; + width: 100%; + } + + .photo-stream__heading { + grid-column: 1 / -1; + font-size: var(--text-xs); + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #888; + margin-bottom: 0.25rem; + } + + .photo-stream__heading a { + color: inherit; + text-decoration: underline; + } + + .photo-stream__heading a:hover { + color: var(--accent); + } + + .photo-stream__item { + display: block; + aspect-ratio: 4 / 3; + overflow: hidden; + cursor: pointer; + } + + .photo-stream__item:hover .photo-stream__img { + transform: scale(1.04); + transition: transform 0.3s ease; + } + + .photo-stream__img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + transition: transform 0.3s ease; + } + + /* ── Mobile breakpoint ── */ + @media (max-width: 640px) { + .hero { + flex-direction: column; + align-items: flex-start; + padding-top: 2rem; + padding-bottom: 3rem; + } + + /* flex-wrap: photo + name centered in row 1, intro wraps to row 2 */ + .hero__card { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 0.25rem 1rem; + width: 100%; + } + + .hero__text { + display: contents; + } + + .hero__photo-wrap { + width: 96px; + height: 96px; + flex-shrink: 0; + } + + .hero__photo { + width: 92px; + height: 92px; + } + + .hero__name { + min-width: 0; + overflow-wrap: break-word; + margin-bottom: 0; + } + + .hero__intro { + flex-basis: 100%; + margin-top: 0.75rem; + text-align: center; + } + + .hero__nav { + display: none; + } + + .content-grid { + grid-template-columns: 1fr; + gap: 2rem; + } + + .photo-stream { + grid-template-columns: repeat(2, 1fr); + } + } +</style> diff --git a/src/pages/links/collection/[collection].astro b/src/pages/links/collection/[collection].astro new file mode 100644 index 0000000..01dbc0d --- /dev/null +++ b/src/pages/links/collection/[collection].astro @@ -0,0 +1,90 @@ +--- +import LinkCard from "@/components/links/LinkCard.astro"; +import LinksLayout from "@/components/links/LinksLayout.astro"; +import { getLinks } from "@/lib/collections"; +import { + collectionColor, + filterByCollection, + getUniqueCollections, +} from "@/lib/links"; + +export async function getStaticPaths() { + const links = await getLinks(); + const collections = getUniqueCollections(links); + return collections.map((col) => ({ + params: { collection: col.toLowerCase() }, + props: { col, links }, + })); +} + +const { col, links } = Astro.props; +const colLinks = filterByCollection(links, col); +const color = collectionColor(col); +--- + +<LinksLayout title={col} description={`Links in der Collection "${col}".`} {links}> + <div class="page"> + <header class="page__header"> + <span class="page__folder" style={`background: ${color}`} aria-hidden="true"> + <svg width="12" height="12" viewBox="0 0 24 24" fill="white" stroke="none"> + <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" /> + </svg> + </span> + <h1 class="page__title">{col}</h1> + <span class="page__count">{colLinks.length}</span> + </header> + {colLinks.length === 0 ? ( + <p class="empty">Keine Links in dieser Collection.</p> + ) : ( + <ul class="link-list"> + {colLinks.map((link) => <LinkCard {link} />)} + </ul> + )} + </div> +</LinksLayout> + +<style> + .page { + display: flex; + flex-direction: column; + gap: 1.5rem; + } + + .page__header { + display: flex; + align-items: center; + gap: 0.75rem; + } + + .page__folder { + width: 28px; + height: 28px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + + .page__title { + font-size: 1.4rem; + font-weight: 600; + letter-spacing: -0.02em; + color: #111; + } + + .page__count { + font-size: 0.875rem; + color: #999; + } + + .empty { + color: #888; + font-size: 0.95rem; + } + + .link-list { + margin: 0; + padding: 0; + } +</style> diff --git a/src/pages/links/collection/index.astro b/src/pages/links/collection/index.astro new file mode 100644 index 0000000..dd32d60 --- /dev/null +++ b/src/pages/links/collection/index.astro @@ -0,0 +1,118 @@ +--- +import LinksLayout from "@/components/links/LinksLayout.astro"; +import { getLinks } from "@/lib/collections"; +import { + collectionColor, + filterByCollection, + getUniqueCollections, +} from "@/lib/links"; + +const links = await getLinks(); +const collections = getUniqueCollections(links).sort((a, b) => + a.localeCompare(b), +); +--- + +<LinksLayout title="Collections" description="Alle Link-Collections." {links}> + <div class="page"> + <header class="page__header"> + <h1 class="page__title">Collections</h1> + <span class="page__count">{collections.length}</span> + </header> + <ul class="col-list"> + {collections.map((col) => ( + <li> + <a href={`/links/collection/${encodeURIComponent(col.toLowerCase())}`} class="col-item"> + <div class="col-item__left"> + <span class="col-item__folder" style={`background: ${collectionColor(col)}`} aria-hidden="true"> + <svg width="10" height="10" viewBox="0 0 24 24" fill="white" stroke="none"> + <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" /> + </svg> + </span> + <span class="col-item__name">{col}</span> + </div> + <span class="col-item__count">{filterByCollection(links, col).length}</span> + </a> + </li> + ))} + </ul> + </div> +</LinksLayout> + +<style> + .page { + display: flex; + flex-direction: column; + gap: 1.5rem; + } + + .page__header { + display: flex; + align-items: baseline; + gap: 0.75rem; + } + + .page__title { + font-size: 1.4rem; + font-weight: 600; + letter-spacing: -0.02em; + color: #111; + } + + .page__count { + font-size: 0.875rem; + color: #999; + } + + .col-list { + list-style: none; + margin: 0; + padding: 0; + } + + .col-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.6rem 0; + border-bottom: 1px solid #e8e8e6; + text-decoration: none; + color: inherit; + } + + .col-list li:last-child .col-item { + border-bottom: none; + } + + .col-item:hover .col-item__name { + color: var(--accent); + } + + .col-item__left { + display: flex; + align-items: center; + gap: 0.6rem; + } + + .col-item__folder { + width: 20px; + height: 20px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + + .col-item__name { + font-size: 0.9rem; + color: #444; + transition: color 0.1s; + } + + .col-item__count { + font-size: 0.75rem; + color: #bbb; + font-variant-numeric: tabular-nums; + } +</style> diff --git a/src/pages/links/index.astro b/src/pages/links/index.astro new file mode 100644 index 0000000..5c20f81 --- /dev/null +++ b/src/pages/links/index.astro @@ -0,0 +1,50 @@ +--- +import LinkCard from "@/components/links/LinkCard.astro"; +import LinksLayout from "@/components/links/LinksLayout.astro"; +import { getLinks } from "@/lib/collections"; + +const links = await getLinks(); +--- + +<LinksLayout title="All Links" description="Alle kuratierten Links von Adrian Altner." {links}> + <div class="page"> + <header class="page__header"> + <h1 class="page__title">All Links</h1> + <span class="page__count">{links.length}</span> + </header> + <ul class="link-list"> + {links.map((link) => <LinkCard {link} />)} + </ul> + </div> +</LinksLayout> + +<style> + .page { + display: flex; + flex-direction: column; + gap: 1.5rem; + } + + .page__header { + display: flex; + align-items: baseline; + gap: 0.75rem; + } + + .page__title { + font-size: 1.4rem; + font-weight: 600; + letter-spacing: -0.02em; + color: #111; + } + + .page__count { + font-size: 0.875rem; + color: #999; + } + + .link-list { + margin: 0; + padding: 0; + } +</style> diff --git a/src/pages/links/tag/[tag].astro b/src/pages/links/tag/[tag].astro new file mode 100644 index 0000000..8d558cd --- /dev/null +++ b/src/pages/links/tag/[tag].astro @@ -0,0 +1,78 @@ +--- +import LinkCard from "@/components/links/LinkCard.astro"; +import LinksLayout from "@/components/links/LinksLayout.astro"; +import { getLinks } from "@/lib/collections"; +import { filterByTag, getUniqueTags } from "@/lib/links"; + +export async function getStaticPaths() { + const links = await getLinks(); + const tags = getUniqueTags(links); + return tags.map((tag) => ({ + params: { tag: tag.toLowerCase() }, + props: { tag, links }, + })); +} + +const { tag, links } = Astro.props; +const tagLinks = filterByTag(links, tag); +--- + +<LinksLayout title={`#${tag}`} description={`Links mit Tag "${tag}".`} {links}> + <div class="page"> + <header class="page__header"> + <span class="page__hash" aria-hidden="true">#</span> + <h1 class="page__title">{tag}</h1> + <span class="page__count">{tagLinks.length}</span> + </header> + {tagLinks.length === 0 ? ( + <p class="empty">Keine Links mit diesem Tag.</p> + ) : ( + <ul class="link-list"> + {tagLinks.map((link) => <LinkCard {link} />)} + </ul> + )} + </div> +</LinksLayout> + +<style> + .page { + display: flex; + flex-direction: column; + gap: 1.5rem; + } + + .page__header { + display: flex; + align-items: baseline; + gap: 0.35rem; + } + + .page__hash { + font-size: 1.4rem; + font-weight: 600; + color: #aaa; + } + + .page__title { + font-size: 1.4rem; + font-weight: 600; + letter-spacing: -0.02em; + color: #111; + } + + .page__count { + font-size: 0.875rem; + color: #999; + margin-left: 0.4rem; + } + + .empty { + color: #888; + font-size: 0.95rem; + } + + .link-list { + margin: 0; + padding: 0; + } +</style> diff --git a/src/pages/links/tag/index.astro b/src/pages/links/tag/index.astro new file mode 100644 index 0000000..6f98d59 --- /dev/null +++ b/src/pages/links/tag/index.astro @@ -0,0 +1,89 @@ +--- +import LinksLayout from "@/components/links/LinksLayout.astro"; +import { getLinks } from "@/lib/collections"; +import { filterByTag, getUniqueTags } from "@/lib/links"; + +const links = await getLinks(); +const tags = getUniqueTags(links).sort((a, b) => a.localeCompare(b)); +--- + +<LinksLayout title="Tags" description="Alle Link-Tags." {links}> + <div class="page"> + <header class="page__header"> + <h1 class="page__title">Tags</h1> + <span class="page__count">{tags.length}</span> + </header> + <ul class="tag-list"> + {tags.map((tag) => ( + <li> + <a href={`/links/tag/${encodeURIComponent(tag.toLowerCase())}`} class="tag-item"> + <span class="tag-item__name">#{tag}</span> + <span class="tag-item__count">{filterByTag(links, tag).length}</span> + </a> + </li> + ))} + </ul> + </div> +</LinksLayout> + +<style> + .page { + display: flex; + flex-direction: column; + gap: 1.5rem; + } + + .page__header { + display: flex; + align-items: baseline; + gap: 0.75rem; + } + + .page__title { + font-size: 1.4rem; + font-weight: 600; + letter-spacing: -0.02em; + color: #111; + } + + .page__count { + font-size: 0.875rem; + color: #999; + } + + .tag-list { + list-style: none; + margin: 0; + padding: 0; + } + + .tag-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.6rem 0; + border-bottom: 1px solid #e8e8e6; + text-decoration: none; + color: inherit; + } + + .tag-list li:last-child .tag-item { + border-bottom: none; + } + + .tag-item:hover .tag-item__name { + color: var(--accent); + } + + .tag-item__name { + font-size: 0.9rem; + color: #444; + transition: color 0.1s; + } + + .tag-item__count { + font-size: 0.75rem; + color: #bbb; + font-variant-numeric: tabular-nums; + } +</style> diff --git a/src/pages/notes/[...slug].astro b/src/pages/notes/[...slug].astro new file mode 100644 index 0000000..7fffedb --- /dev/null +++ b/src/pages/notes/[...slug].astro @@ -0,0 +1,229 @@ +--- +import { Image } from "astro:assets"; +import { getCollection, render } from "astro:content"; +import WebMentions from "@/components/WebMentions.astro"; +import BaseLayout from "@/layouts/BaseLayout.astro"; +export async function getStaticPaths() { + const notes = await getCollection("notes", ({ data }) => !data.draft); + return notes.map((note) => ({ + params: { slug: note.id }, + props: { note }, + })); +} + +const { note } = Astro.props; +const { Content } = await render(note); +const noteUrl = new URL(`/notes/${note.id}/`, Astro.site).toString(); +const ogImageUrl = new URL(`/og/notes/${note.id}.png`, Astro.site).toString(); + +function capitalize(s: string) { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +function formatDate(date: Date) { + return date.toLocaleDateString("de-DE", { + year: "numeric", + month: "long", + day: "numeric", + }); +} +--- + +<BaseLayout + title={note.data.title} + description={note.data.description ?? note.data.title} + image={{ src: ogImageUrl, alt: note.data.title }} +> + <div class="page"> + <main class="main"> + <a href="/notes" class="back">← Notes</a> + <article class="h-entry"> + <a href={noteUrl} class="u-url" aria-hidden="true" tabindex="-1" style="display:none">Permalink</a> + <span class="p-author h-card" style="display:none"><img class="u-photo" src="https://adrian-altner.com/avatar.jpg" alt="Adrian Altner" /><span class="p-name">Adrian Altner</span><a class="u-url" href="https://adrian-altner.com">adrian-altner.com</a></span> + <header class="note-header"> + <h1 class="note-title p-name">{note.data.title}</h1> + <time + class="note-date dt-published" + datetime={note.data.publishDate.toISOString()} + > + {formatDate(note.data.publishDate)} + </time> + { + note.data.tags.length > 0 && ( + <div class="note-tags"> + {note.data.tags.map((tag) => ( + <a href={`/tags/${tag}`} class="tag"> + {capitalize(tag)} + </a> + ))} + </div> + ) + } + </header> + {note.data.cover && ( + <Image class="note-cover" src={note.data.cover} alt={note.data.coverAlt ?? ""} /> + )} + <div class="prose e-content"> + <Content /> + </div> + <WebMentions url={noteUrl} /> + </article> + <a href="/notes" class="back">← Notes</a> + </main> + </div> +</BaseLayout> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #f5f5f3; + color: #111; + } + + .main { + max-width: var(--width-prose); + margin: 0 auto; + padding: 2rem 2rem 4rem; + width: 100%; + } + + .note-header { + margin-bottom: 2rem; + padding-bottom: 1.25rem; + border-bottom: 1px solid #e0e0de; + } + + .note-title { + font-size: clamp(1.5rem, 4vw, 2.25rem); + font-weight: 400; + letter-spacing: -0.02em; + line-height: 1.2; + margin-bottom: 0.5rem; + } + + .note-date { + font-size: 0.875rem; + color: #888; + } + + .note-tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 0.75rem; + } + + .tag { + font-size: 0.75rem; + color: #666; + background: #e8e8e6; + padding: 0.15em 0.55em; + border-radius: 3px; + text-decoration: none; + } + + .tag:hover { + background: var(--accent); + color: #fff; + } + + .note-cover { + width: 100%; + height: auto; + border-radius: 6px; + display: block; + margin-bottom: 2rem; + } + + .prose { + font-size: 1rem; + line-height: 1.7; + color: #333; + } + + .prose :global(h2) { + font-size: 1.4rem; + font-weight: 500; + margin: 2rem 0 0.75rem; + color: #111; + } + + .prose :global(h3) { + font-size: 1.15rem; + font-weight: 500; + margin: 1.5rem 0 0.5rem; + color: #111; + } + + .prose :global(p) { + margin-bottom: 1.25rem; + } + + .prose :global(a) { + color: var(--accent); + text-underline-offset: 3px; + } + + .prose :global(blockquote) { + border-left: 3px solid var(--accent); + margin: 1.5rem 0; + padding: 0.25rem 0 0.25rem 1.25rem; + color: #555; + font-style: italic; + } + + .prose :global(code) { + font-family: ui-monospace, monospace; + font-size: 0.875em; + background: #e8e8e6; + padding: 0.1em 0.35em; + border-radius: 3px; + } + + .prose :global(pre) { + background: #2b2b2b; + color: #e0e0e0; + padding: 1.25rem; + border-radius: 6px; + overflow-x: auto; + margin: 1.5rem 0; + } + + .prose :global(pre code) { + background: none; + padding: 0; + font-size: 0.875rem; + } + + .prose :global(ul), + .prose :global(ol) { + padding-left: 1.5rem; + margin-bottom: 1.25rem; + } + + .prose :global(li) { + margin-bottom: 0.4rem; + } + + .back { + display: inline-block; + font-size: 0.9rem; + color: #555; + text-decoration: none; + border-bottom: 1px solid #ccc; + padding-bottom: 1px; + margin-bottom: 1.5rem; + } + + article + .back { + margin-bottom: 0; + margin-top: 1rem; + } + + .back:hover { + color: var(--accent); + border-color: var(--accent); + } +</style> diff --git a/src/pages/notes/index.astro b/src/pages/notes/index.astro new file mode 100644 index 0000000..fadb8f2 --- /dev/null +++ b/src/pages/notes/index.astro @@ -0,0 +1,275 @@ +--- +import { Image } from "astro:assets"; +import { getCollection, render } from "astro:content"; +import BaseLayout from "@/layouts/BaseLayout.astro"; + +const notes = (await getCollection("notes", ({ data }) => !data.draft)).sort( + (a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf(), +); + +function capitalize(s: string) { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +const renderedNotes = await Promise.all( + notes.map(async (note) => ({ note, Content: (await render(note)).Content })), +); + +function formatDate(date: Date) { + return date.toLocaleDateString("de-DE", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); +} +--- + +<BaseLayout title="Notes" description="Kurze Notizen von Adrian Altner."> + <div class="page"> + <main class="main"> + <a href="/" class="back">← Home</a> + <h1 class="heading">Notes</h1> + { + renderedNotes.length === 0 ? ( + <p class="empty">Noch keine Notizen vorhanden.</p> + ) : ( + <ul class="list"> + {renderedNotes.map(({ note, Content }) => ( + <li class="item"> + <time + class="item__date" + datetime={note.data.publishDate.toISOString()} + > + {formatDate(note.data.publishDate)} + </time> + <a href={`/notes/${note.id}`} class="item__title"> + {note.data.title} + </a> + {note.data.cover && ( + <a href={`/notes/${note.id}`} class="item__cover"> + <Image + src={note.data.cover} + alt={note.data.coverAlt ?? ""} + /> + </a> + )} + <div class="item__body prose"> + <Content /> + </div> + {note.data.tags.length > 0 && ( + <div class="item__tags"> + {note.data.tags.map((tag) => ( + <a href={`/tags/${tag}`} class="tag"> + {capitalize(tag)} + </a> + ))} + </div> + )} + </li> + ))} + </ul> + ) + } + </main> + </div> +</BaseLayout> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #f5f5f3; + color: #111; + } + + .main { + max-width: var(--width-prose); + margin: 0 auto; + padding: 2rem 2rem 4rem; + width: 100%; + } + + .heading { + font-size: 1.75rem; + font-weight: 400; + letter-spacing: -0.02em; + margin-bottom: 2.5rem; + } + + .empty { + color: #888; + font-size: 0.95rem; + } + + .back { + display: inline-block; + margin-bottom: 1.5rem; + font-size: 0.9rem; + color: #555; + text-decoration: none; + } + + .back:hover { + color: var(--accent); + } + + .list { + list-style: none; + padding: 0; + display: flex; + flex-direction: column; + } + + .item { + display: grid; + grid-template-columns: 7rem 1fr; + gap: 0.25rem 1.25rem; + padding: 1rem 0; + border-bottom: 1px solid #e0e0de; + align-items: baseline; + } + + .item:first-child { + border-top: 1px solid #e0e0de; + } + + .item__date { + font-size: 0.8rem; + color: #888; + white-space: nowrap; + padding-top: 0.15rem; + } + + .item__title { + font-size: 1rem; + font-weight: 500; + color: #111; + text-decoration: none; + } + + .item__title:hover { + color: var(--accent); + } + + .item__cover { + grid-column: 1 / -1; + margin-top: 0.75rem; + display: block; + } + + .item__cover img { + width: 100%; + height: auto; + border-radius: 4px; + display: block; + } + + .item__body { + grid-column: 1 / -1; + margin-top: 0.75rem; + } + + .prose { + font-size: 1rem; + line-height: 1.7; + color: #333; + } + + .prose :global(h2) { + font-size: 1.4rem; + font-weight: 500; + margin: 2rem 0 0.75rem; + color: #111; + } + + .prose :global(h3) { + font-size: 1.15rem; + font-weight: 500; + margin: 1.5rem 0 0.5rem; + color: #111; + } + + .prose :global(p) { + margin-bottom: 1.25rem; + } + + .prose :global(p:last-child) { + margin-bottom: 0; + } + + .prose :global(a) { + color: var(--accent); + text-underline-offset: 3px; + } + + .prose :global(blockquote) { + border-left: 3px solid var(--accent); + margin: 1.5rem 0; + padding: 0.25rem 0 0.25rem 1.25rem; + color: #555; + font-style: italic; + } + + .prose :global(code) { + font-family: ui-monospace, monospace; + font-size: 0.875em; + background: #e8e8e6; + padding: 0.1em 0.35em; + border-radius: 3px; + } + + .prose :global(pre) { + background: #2b2b2b; + color: #e0e0e0; + padding: 1.25rem; + border-radius: 6px; + overflow-x: auto; + margin: 1.5rem 0; + } + + .prose :global(pre code) { + background: none; + padding: 0; + font-size: 0.875rem; + } + + .prose :global(ul), + .prose :global(ol) { + padding-left: 1.5rem; + margin-bottom: 1.25rem; + } + + .prose :global(li) { + margin-bottom: 0.4rem; + } + + .item__tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 2rem; + grid-column: 1 / -1; + } + + .tag { + font-size: 0.75rem; + color: #666; + background: #e8e8e6; + padding: 0.15em 0.55em; + border-radius: 3px; + text-decoration: none; + } + + .tag:hover { + background: var(--accent); + color: #fff; + } + + @media (max-width: 480px) { + .item { + grid-template-columns: 1fr; + gap: 0.2rem; + } + } +</style> diff --git a/src/pages/og/blog/[...slug].png.ts b/src/pages/og/blog/[...slug].png.ts new file mode 100644 index 0000000..441ea20 --- /dev/null +++ b/src/pages/og/blog/[...slug].png.ts @@ -0,0 +1,23 @@ +import { getCollection } from "astro:content"; +import { buildArticleVNode, renderOgImage } from "@/lib/og"; + +export async function getStaticPaths() { + const posts = await getCollection("blog", ({ data }) => !data.draft); + return posts.map((post) => ({ + params: { slug: post.id }, + props: { title: post.data.title, description: post.data.description }, + })); +} + +export async function GET({ + props, +}: { + props: { title: string; description: string }; +}) { + const buffer = await renderOgImage( + buildArticleVNode({ title: props.title, description: props.description }), + ); + return new Response(buffer, { + headers: { "Content-Type": "image/png" }, + }); +} diff --git a/src/pages/og/notes/[...slug].png.ts b/src/pages/og/notes/[...slug].png.ts new file mode 100644 index 0000000..43cf2a2 --- /dev/null +++ b/src/pages/og/notes/[...slug].png.ts @@ -0,0 +1,32 @@ +import { getCollection } from "astro:content"; +import { join } from "node:path"; +import type { APIContext, ImageMetadata } from "astro"; +import { buildNoteVNode, imageToOgDataUri, renderOgImage } from "@/lib/og"; + +export async function getStaticPaths() { + const notes = await getCollection("notes", ({ data }) => !data.draft); + + const imageModules = import.meta.glob<{ default: ImageMetadata }>( + "/src/content/notes/**/*.{jpg,jpeg,png,webp}", + { eager: true }, + ); + + return notes.map((note) => { + let coverPath: string | null = null; + if (note.data.cover) { + const entry = Object.entries(imageModules).find( + ([, mod]) => mod.default.src === note.data.cover?.src, + ); + coverPath = entry?.[0] ?? null; + } + return { params: { slug: note.id }, props: { coverPath } }; + }); +} + +export async function GET({ props }: APIContext) { + const coverDataUri = props.coverPath + ? await imageToOgDataUri(join(process.cwd(), props.coverPath)) + : undefined; + const buffer = await renderOgImage(buildNoteVNode({ coverDataUri })); + return new Response(buffer, { headers: { "Content-Type": "image/png" } }); +} diff --git a/src/pages/og/photos/[...slug].png.ts b/src/pages/og/photos/[...slug].png.ts new file mode 100644 index 0000000..ad83e04 --- /dev/null +++ b/src/pages/og/photos/[...slug].png.ts @@ -0,0 +1,133 @@ +import { getCollection } from "astro:content"; +import { join } from "node:path"; +import type { APIContext, ImageMetadata } from "astro"; +import { + buildCollectionPhotos, + collectionSlug, + type PhotoSidecar, +} from "@/lib/collections"; +import { + buildCollectionVNode, + buildPhotoVNode, + imageToOgDataUri, + renderOgImage, +} from "@/lib/og"; + +type CollectionPathProps = { + type: "collection"; + title: string; + description: string; + location?: string; + coverPath: string | null; +}; + +type PhotoPathProps = { + type: "photo"; + title: string; + alt: string; + date: string; + location: string; + photoPath: string; +}; + +type PathProps = CollectionPathProps | PhotoPathProps; + +export async function getStaticPaths() { + const allCollections = await getCollection( + "collections_photos", + ({ data }) => !data.draft, + ); + + const sidecars = import.meta.glob<PhotoSidecar>( + "/src/content/photos/collections/**/*.json", + { eager: true }, + ); + const imageModules = import.meta.glob<{ default: ImageMetadata }>( + "/src/content/photos/collections/**/*.jpg", + { eager: true }, + ); + + const paths: { params: { slug: string }; props: PathProps }[] = []; + + for (const col of allCollections) { + const slug = collectionSlug(col); + if (!slug) continue; // skip root collection index + + // Cover: first JPG in this collection's own img/ dir (sorted alphabetically) + const imgPrefix = `/src/content/photos/collections/${slug}/img/`; + const coverEntry = Object.entries(imageModules) + .filter( + ([p]) => + p.startsWith(imgPrefix) && !p.slice(imgPrefix.length).includes("/"), + ) + .sort(([a], [b]) => a.localeCompare(b))[0]; + const coverPath = coverEntry?.[0] ?? null; + + paths.push({ + params: { slug }, + props: { + type: "collection", + title: col.data.title, + description: col.data.description, + ...(col.data.location !== undefined + ? { location: col.data.location } + : {}), + coverPath, + } satisfies CollectionPathProps, + }); + + // Individual photo paths + const photos = buildCollectionPhotos(sidecars, imageModules, slug); + for (const photo of photos) { + const photoPath = `/src/content/photos/collections/${slug}/img/${photo.sidecar.id}.jpg`; + paths.push({ + params: { slug: `${slug}/${photo.sidecar.id}` }, + props: { + type: "photo", + title: photo.sidecar.title[0] ?? photo.sidecar.id, + alt: photo.sidecar.alt, + date: photo.sidecar.date, + location: photo.sidecar.location, + photoPath, + } satisfies PhotoPathProps, + }); + } + } + + return paths; +} + +export async function GET({ props }: APIContext) { + const typedProps = props as PathProps; + const cwd = process.cwd(); + + if (typedProps.type === "collection") { + const coverDataUri = typedProps.coverPath + ? await imageToOgDataUri(join(cwd, typedProps.coverPath)) + : undefined; + const buffer = await renderOgImage( + buildCollectionVNode({ + title: typedProps.title, + description: typedProps.description, + ...(typedProps.location !== undefined + ? { location: typedProps.location } + : {}), + coverDataUri, + }), + ); + return new Response(buffer, { headers: { "Content-Type": "image/png" } }); + } + + // type === "photo" + const photoDataUri = await imageToOgDataUri(join(cwd, typedProps.photoPath)); + const buffer = await renderOgImage( + buildPhotoVNode({ + title: typedProps.title, + alt: typedProps.alt, + date: typedProps.date, + location: typedProps.location, + photoDataUri, + }), + ); + return new Response(buffer, { headers: { "Content-Type": "image/png" } }); +} diff --git a/src/pages/photos/collections/[...slug].astro b/src/pages/photos/collections/[...slug].astro new file mode 100644 index 0000000..58ffbd5 --- /dev/null +++ b/src/pages/photos/collections/[...slug].astro @@ -0,0 +1,653 @@ +--- +import { Image } from "astro:assets"; +import { getCollection } from "astro:content"; +import type { ImageMetadata } from "astro"; +import PhotoDetail from "@/components/PhotoDetail.astro"; +import PhotosSubNav from "@/components/PhotosSubNav.astro"; +import BaseLayout from "@/layouts/BaseLayout.astro"; +import type { PhotoSidecar } from "@/lib/collections"; +import { + buildBreadcrumbs, + buildCollectionPhotos, + collectionSlug, + getChildCollections, +} from "@/lib/collections"; + +type CollectionProps = { + type: "collection"; + slug: string; + title: string; + description: string; + location?: string; + hasContent: boolean; + childSlugs: string[]; + childTitles: Record<string, string>; + childLocations: Record<string, string | undefined>; + photoCount: number; + breadcrumbs: { label: string; href: string }[]; +}; + +type PhotoProps = { + type: "photo"; + sidecar: PhotoSidecar; + image: ImageMetadata; + collectionSlug: string; + collectionTitle: string; + prevHref: string | null; + nextHref: string | null; + breadcrumbs: { label: string; href: string }[]; +}; + +export async function getStaticPaths() { + const allCollections = await getCollection( + "collections_photos", + ({ data }) => !data.draft, + ); + + const sidecars = import.meta.glob<PhotoSidecar>( + "/src/content/photos/collections/**/*.json", + { eager: true }, + ); + const imageModules = import.meta.glob<{ default: ImageMetadata }>( + "/src/content/photos/collections/**/*.jpg", + { eager: true }, + ); + + const paths: { + params: { slug: string | undefined }; + props: CollectionProps | PhotoProps; + }[] = []; + + // Build a global flat photo list (sorted by collection slug) for cross-collection nav + const sortedCollections = [...allCollections].sort((a, b) => + collectionSlug(a).localeCompare(collectionSlug(b)), + ); + const globalPhotos: { cSlug: string; photoId: string }[] = []; + const collectionPhotosMap = new Map< + string, + { sidecar: PhotoSidecar; image: ImageMetadata }[] + >(); + for (const col of sortedCollections) { + const slug = collectionSlug(col); + const photos = buildCollectionPhotos(sidecars, imageModules, slug); + collectionPhotosMap.set(slug, photos); + for (const photo of photos) { + globalPhotos.push({ cSlug: slug, photoId: photo.sidecar.id }); + } + } + + for (const col of allCollections) { + const slug = collectionSlug(col); + const children = getChildCollections(slug, allCollections); + const photos = collectionPhotosMap.get(slug) ?? []; + const breadcrumbs = buildBreadcrumbs(slug, allCollections); + + // Collection index path + paths.push({ + params: { slug: slug || undefined }, + props: { + type: "collection", + slug, + title: col.data.title, + description: col.data.description, + ...(col.data.location !== undefined + ? { location: col.data.location } + : {}), + hasContent: false, // render() called at page level + childSlugs: children.map((c) => collectionSlug(c)), + childTitles: Object.fromEntries( + children.map((c) => [collectionSlug(c), c.data.title]), + ), + childLocations: Object.fromEntries( + children.map((c) => [collectionSlug(c), c.data.location]), + ), + photoCount: photos.length, + breadcrumbs, + } satisfies CollectionProps, + }); + + // Photo detail paths — use global prev/next for cross-collection navigation + photos.forEach((photo) => { + const gi = globalPhotos.findIndex( + (p) => p.cSlug === slug && p.photoId === photo.sidecar.id, + ); + const prevG = globalPhotos[gi - 1]; + const nextG = globalPhotos[gi + 1]; + + paths.push({ + params: { slug: `${slug}/${photo.sidecar.id}` }, + props: { + type: "photo", + sidecar: photo.sidecar, + image: photo.image, + collectionSlug: slug, + collectionTitle: col.data.title, + prevHref: prevG + ? `/photos/collections/${prevG.cSlug}/${prevG.photoId}` + : null, + nextHref: nextG + ? `/photos/collections/${nextG.cSlug}/${nextG.photoId}` + : null, + breadcrumbs: [ + ...breadcrumbs, + { + label: photo.sidecar.title[0] ?? photo.sidecar.id, + href: `/photos/collections/${slug}/${photo.sidecar.id}`, + }, + ], + } satisfies PhotoProps, + }); + }); + } + + return paths; +} + +const props = Astro.props as CollectionProps | PhotoProps; + +// For collection pages: load photos + child covers at render time +const sidecars = import.meta.glob<PhotoSidecar>( + "/src/content/photos/collections/**/*.json", + { eager: true }, +); +const imageModules = import.meta.glob<{ default: ImageMetadata }>( + "/src/content/photos/collections/**/*.jpg", + { eager: true }, +); + +let photos: { sidecar: PhotoSidecar; image: ImageMetadata }[] = []; +let childCovers: Record<string, ImageMetadata | null> = {}; + +if (props.type === "collection") { + photos = buildCollectionPhotos(sidecars, imageModules, props.slug); + + // Pick first image per child collection as cover + for (const childSlug of props.childSlugs) { + const prefix = `/src/content/photos/collections/${childSlug}/`; + const cover = Object.entries(imageModules) + .filter(([p]) => p.startsWith(prefix)) + .sort(([a], [b]) => a.localeCompare(b))[0]; + childCovers[childSlug] = cover?.[1]?.default ?? null; + } +} + +const BATCH_SIZE = 15; +--- + +{ + props.type === "photo" ? ( + <BaseLayout + title={props.sidecar.title[0] ?? props.sidecar.id} + description={props.sidecar.alt} + image={{ + src: new URL( + `/og/photos/${props.collectionSlug}/${props.sidecar.id}.png`, + Astro.site, + ).toString(), + alt: props.sidecar.alt, + }} + navHidden={true} + footerHidden={true} + > + <PhotoDetail + sidecar={props.sidecar} + image={props.image} + prevHref={props.prevHref} + nextHref={props.nextHref} + backHref={`/photos/collections/${props.collectionSlug}`} + backLabel={props.collectionTitle} + canonicalUrl={new URL( + `/photos/collections/${props.collectionSlug}/${props.sidecar.id}`, + Astro.site, + ).toString()} + /> + </BaseLayout> + ) : ( + <BaseLayout + title={props.title} + description={props.description} + navDesktopHidden={true} + footerDark={true} + image={ + props.slug + ? { + src: new URL( + `/og/photos/${props.slug}.png`, + Astro.site, + ).toString(), + alt: props.title, + } + : undefined + } + > + <div class="page"> + <main class="main"> + <PhotosSubNav /> + + <nav class="breadcrumb" aria-label="Breadcrumb"> + {props.breadcrumbs.map((crumb, i) => ( + <> + {i > 0 && <span aria-hidden="true">/</span>} + {i < props.breadcrumbs.length - 1 ? ( + <a href={crumb.href}>{crumb.label}</a> + ) : ( + <span aria-current="page">{crumb.label}</span> + )} + </> + ))} + </nav> + + <header class="collection-header"> + <h1 class="collection-title">{props.title}</h1> + {props.location && ( + <p class="collection-location">{props.location}</p> + )} + {props.description && ( + <p class="collection-description">{props.description}</p> + )} + </header> + + {props.childSlugs.length > 0 && ( + <section class="sub-collections"> + <ul class="collection-grid" role="list"> + {props.childSlugs.map((childSlug) => { + const cover = childCovers[childSlug]; + const title = props.childTitles[childSlug] ?? childSlug; + const location = props.childLocations[childSlug]; + return ( + <li class="collection-card"> + <a + href={`/photos/collections/${childSlug}`} + class="collection-link" + > + {cover && ( + <div class="collection-cover"> + <Image + src={cover} + alt={title} + width={600} + height={400} + class="collection-img" + /> + </div> + )} + <div class="collection-meta"> + <span class="collection-name">{title}</span> + {location && ( + <span class="collection-loc">{location}</span> + )} + </div> + </a> + </li> + ); + })} + </ul> + </section> + )} + + {photos.length > 0 && ( + <> + <div class="photo-grid h-feed" id="photo-grid"> + {photos.map((photo, i) => { + const ar = photo.image.width / photo.image.height; + return ( + <div + class="photo-item h-entry" + data-ar={ar} + data-batch={Math.floor(i / BATCH_SIZE)} + data-hidden={i >= BATCH_SIZE ? "true" : undefined} + > + <a + href={`/photos/collections/${props.slug}/${photo.sidecar.id}`} + class="photo-link u-url" + > + <Image + src={photo.image} + alt={photo.sidecar.alt} + width={800} + height={Math.round(800 / ar)} + loading="lazy" + class="photo-img u-photo" + /> + </a> + <time + class="dt-published" + datetime={new Date(photo.sidecar.date).toISOString()} + style="display:none" + > + {photo.sidecar.date} + </time> + </div> + ); + })} + </div> + <div id="sentinel" aria-hidden="true" /> + <div id="load-status" class="load-status" aria-live="polite" /> + </> + )} + </main> + </div> + </BaseLayout> + ) +} + +<script> + import justifiedLayout from "justified-layout"; + + const grid = document.getElementById("photo-grid"); + if (grid) { + const BATCH_SIZE = 15; + const TARGET_ROW_HEIGHT = 280; + const BOX_SPACING = 8; + + const allItems = Array.from( + grid.querySelectorAll<HTMLElement>(".photo-item"), + ); + const sentinel = document.getElementById("sentinel"); + const status = document.getElementById("load-status"); + + let currentBatch = 0; + const totalBatches = + Math.ceil( + allItems.filter((el) => el.hasAttribute("data-hidden")).length / + BATCH_SIZE, + ) + 1; + + function applyLayout() { + if (window.innerWidth <= 640) { + grid!.style.height = ""; + grid!.style.position = ""; + for (const item of allItems) { + if (!item.hasAttribute("data-hidden")) item.style.cssText = ""; + } + return; + } + + const visibleItems = allItems.filter( + (el) => !el.hasAttribute("data-hidden"), + ); + const ratios = visibleItems.map((el) => Number(el.dataset.ar)); + + const result = justifiedLayout(ratios, { + containerWidth: grid!.offsetWidth, + targetRowHeight: TARGET_ROW_HEIGHT, + boxSpacing: BOX_SPACING, + containerPadding: 0, + }); + + grid!.style.position = "relative"; + grid!.style.height = `${result.containerHeight}px`; + + for (let i = 0; i < visibleItems.length; i++) { + const box = result.boxes[i]; + const item = visibleItems[i]; + if (!box || !item) continue; + item.style.position = "absolute"; + item.style.top = `${box.top}px`; + item.style.left = `${box.left}px`; + item.style.width = `${box.width}px`; + item.style.height = `${box.height}px`; + } + } + + function revealNextBatch() { + currentBatch++; + const toReveal = allItems.filter( + (el) => + el.hasAttribute("data-hidden") && + Number(el.dataset.batch) === currentBatch, + ); + if (toReveal.length === 0) { + sentinel?.remove(); + return; + } + if (status) status.textContent = "Loading more photos…"; + for (const el of toReveal) { + el.removeAttribute("data-hidden"); + el.classList.add("is-visible"); + } + applyLayout(); + if (currentBatch >= totalBatches) sentinel?.remove(); + if (status) status.textContent = ""; + } + + let resizeTimer: ReturnType<typeof setTimeout>; + window.addEventListener("resize", () => { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(applyLayout, 150); + }); + + applyLayout(); + + if (sentinel) { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0]?.isIntersecting) revealNextBatch(); + }, + { rootMargin: "400px" }, + ); + observer.observe(sentinel); + } + } +</script> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #111; + color: #f5f5f3; + } + + .page.dark { + display: flex; + flex-direction: column; + } + + .main { + max-width: var(--width-wide); + margin: 0 auto; + padding: 1.5rem; + width: 100%; + } + + /* Breadcrumb */ + .breadcrumb { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8rem; + color: #666; + margin-bottom: 1.5rem; + flex-wrap: wrap; + } + + .breadcrumb a { + color: #888; + text-decoration: none; + } + + .breadcrumb a:hover { + color: #f5f5f3; + } + + /* Collection header */ + .collection-header { + margin-bottom: 1.5rem; + } + + .collection-title { + font-size: 1.5rem; + font-weight: 500; + margin-bottom: 0.25rem; + } + + .collection-location { + font-size: 0.85rem; + color: #888; + margin: 0; + } + + .collection-description { + margin-top: 0.5rem; + font-size: 0.9rem; + color: #aaa; + line-height: 1.6; + max-width: 60ch; + } + + /* Sub-collections grid */ + .sub-collections { + margin-bottom: 2rem; + } + + .collection-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; + list-style: none; + padding: 0; + margin: 0; + } + + .collection-card { + border-radius: 2px; + overflow: hidden; + } + + .collection-link { + display: block; + text-decoration: none; + color: inherit; + } + + .collection-cover { + aspect-ratio: 3 / 2; + overflow: hidden; + } + + .collection-img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.4s ease; + } + + .collection-link:hover .collection-img { + transform: scale(1.03); + } + + .collection-meta { + padding: 0.6rem 0; + display: flex; + flex-direction: column; + gap: 0.2rem; + } + + .collection-name { + font-size: 0.95rem; + font-weight: 500; + } + + .collection-loc { + font-size: 0.8rem; + color: #888; + } + + /* Photo grid */ + .photo-grid { + position: relative; + } + + .photo-item { + border-radius: 2px; + overflow: hidden; + } + + .photo-item[data-hidden] { + display: none; + } + + .photo-item.is-visible { + animation: fadeIn 0.4s ease forwards; + } + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + .photo-link { + display: block; + width: 100%; + height: 100%; + overflow: hidden; + } + + .photo-img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.4s ease; + } + + .photo-link:hover .photo-img { + transform: scale(1.03); + } + + #sentinel { + height: 1px; + margin-top: 2rem; + } + + .load-status { + text-align: center; + padding: 1rem; + font-size: 0.85rem; + color: #666; + min-height: 2rem; + } + + .page :global(.footer a) { + color: #f5f5f3; + } + + @media (max-width: 640px) { + .main { + padding: 0.25rem 0.75rem; + } + + .collection-grid { + grid-template-columns: repeat(2, 1fr); + gap: 0.5rem; + } + + .photo-grid { + display: flex; + flex-direction: column; + gap: 0.5rem; + height: auto !important; + position: static !important; + } + + .photo-item { + position: static !important; + width: 100% !important; + height: auto !important; + } + + .photo-img { + height: auto; + object-fit: initial; + } + } +</style> diff --git a/src/pages/photos/collections/index.astro b/src/pages/photos/collections/index.astro new file mode 100644 index 0000000..090d5f7 --- /dev/null +++ b/src/pages/photos/collections/index.astro @@ -0,0 +1,165 @@ +--- +import { Image } from "astro:assets"; +import { getCollection } from "astro:content"; +import type { ImageMetadata } from "astro"; +import PhotosSubNav from "@/components/PhotosSubNav.astro"; +import BaseLayout from "@/layouts/BaseLayout.astro"; +import { collectionSlug, getChildCollections } from "@/lib/collections"; + +const allCollections = await getCollection( + "collections_photos", + ({ data }) => !data.draft, +); + +// Top-level collections only (no slash in slug) +const topLevel = getChildCollections("", allCollections); + +const imageModules = import.meta.glob<{ default: ImageMetadata }>( + "/src/content/photos/collections/**/*.jpg", + { eager: true }, +); + +// Pick first image per top-level collection as cover +const covers = Object.fromEntries( + topLevel.map((col) => { + const slug = collectionSlug(col); + const prefix = `/src/content/photos/collections/${slug}/`; + const cover = Object.entries(imageModules) + .filter(([p]) => p.startsWith(prefix)) + .sort(([a], [b]) => a.localeCompare(b))[0]; + return [slug, cover?.[1]?.default ?? null]; + }), +); +--- + +<BaseLayout + title="Collections" + description="Photo collections by Adrian Altner — travel and street photography." + navDesktopHidden={true} + footerDark={true} +> + <div class="page"> + <main class="main"> + <PhotosSubNav /> + <ul class="collection-grid" role="list"> + { + topLevel.map((col) => { + const slug = collectionSlug(col); + const cover = covers[slug]; + return ( + <li class="collection-card"> + <a href={`/photos/collections/${slug}`} class="collection-link"> + {cover && ( + <div class="collection-cover"> + <Image + src={cover} + alt={col.data.title} + width={600} + height={400} + class="collection-img" + /> + </div> + )} + <div class="collection-meta"> + <span class="collection-title">{col.data.title}</span> + {col.data.location && ( + <span class="collection-location"> + {col.data.location} + </span> + )} + </div> + </a> + </li> + ); + }) + } + </ul> + </main> + </div> +</BaseLayout> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #111; + color: #f5f5f3; + } + + .main { + max-width: var(--width-wide); + margin: 0 auto; + padding: 1.5rem; + width: 100%; + } + + .collection-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; + list-style: none; + padding: 0; + margin: 0; + } + + .collection-card { + border-radius: 2px; + overflow: hidden; + } + + .collection-link { + display: block; + text-decoration: none; + color: inherit; + } + + .collection-cover { + aspect-ratio: 3 / 2; + overflow: hidden; + } + + .collection-img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.4s ease; + } + + .collection-link:hover .collection-img { + transform: scale(1.03); + } + + .collection-meta { + padding: 0.6rem 0; + display: flex; + flex-direction: column; + gap: 0.2rem; + } + + .collection-title { + font-size: 0.95rem; + font-weight: 500; + } + + .collection-location { + font-size: 0.8rem; + color: #888; + } + + .page :global(.footer a) { + color: #f5f5f3; + } + + @media (max-width: 640px) { + .main { + padding: 0.75rem; + } + + .collection-grid { + grid-template-columns: repeat(2, 1fr); + gap: 0.5rem; + } + } +</style> diff --git a/src/pages/photos/index.astro b/src/pages/photos/index.astro new file mode 100644 index 0000000..a838a97 --- /dev/null +++ b/src/pages/photos/index.astro @@ -0,0 +1,352 @@ +--- +import { Image } from "astro:assets"; +import type { ImageMetadata } from "astro"; +import JsonLd from "@/components/JsonLd.astro"; +import PhotosSubNav from "@/components/PhotosSubNav.astro"; +import BaseLayout from "@/layouts/BaseLayout.astro"; + +const collectionPageSchema = { + "@context": "https://schema.org", + "@type": "CollectionPage", + name: "Photography by Adrian Altner", + description: + "A photostream of travel and street photography by Adrian Altner.", + url: "https://adrian-altner.com/photos", + author: { + "@type": "Person", + name: "Adrian Altner", + url: "https://adrian-altner.com", + }, +}; + +type PhotoSidecar = { + id: string; + title: string[]; + alt: string; + location: string; + date: string; + tags: string[]; + exif: { + camera: string; + lens: string; + aperture: string; + iso: string; + focal_length: string; + shutter_speed: string; + }; +}; + +const sidecars = import.meta.glob<PhotoSidecar>( + "/src/content/photos/collections/**/*.json", + { eager: true }, +); + +const imageModules = import.meta.glob<{ default: ImageMetadata }>( + "/src/content/photos/collections/**/*.jpg", + { eager: true }, +); + +const photos = Object.entries(sidecars) + .map(([jsonPath, sidecar]) => { + const imgPath = jsonPath.replace(".json", ".jpg"); + const imageModule = imageModules[imgPath]; + const collectionSlug = jsonPath + .replace("/src/content/photos/collections/", "") + .replace(/\/img\/[^/]+\.json$/, ""); + return { sidecar, image: imageModule?.default, collectionSlug }; + }) + .filter( + ( + p, + ): p is { + sidecar: PhotoSidecar; + image: ImageMetadata; + collectionSlug: string; + } => !!p.image, + ) + .sort( + (a, b) => + new Date(b.sidecar.date).getTime() - new Date(a.sidecar.date).getTime(), + ); + +const BATCH_SIZE = 15; +--- + +<BaseLayout + title="Photography" + description="A photostream of travel and street photography by Adrian Altner." + navDesktopHidden={true} + footerDark={true} +> + <JsonLd schema={collectionPageSchema} slot="head" /> + <div class="page"> + + + <main class="main"> + <PhotosSubNav /> + <div class="photo-grid" id="photo-grid"> + { + photos.map((photo, i) => { + const ar = photo.image.width / photo.image.height; + return ( + <div + class="photo-item" + data-ar={ar} + data-batch={Math.floor(i / BATCH_SIZE)} + data-hidden={i >= BATCH_SIZE ? "true" : undefined} + > + <a href={`/photos/collections/${photo.collectionSlug}/${photo.sidecar.id}`} class="photo-link"> + <Image + src={photo.image} + alt={photo.sidecar.alt} + width={800} + height={Math.round(800 / ar)} + loading="lazy" + class="photo-img" + /> + </a> + </div> + ); + }) + } + </div> + + <div id="sentinel" aria-hidden="true"></div> + <div id="load-status" class="load-status" aria-live="polite"></div> + </main> + </div> +</BaseLayout> + +<script> + import justifiedLayout from "justified-layout"; + + const BATCH_SIZE = 15; + const TARGET_ROW_HEIGHT = 280; + const BOX_SPACING = 8; + + const grid = document.getElementById("photo-grid")!; + const allItems = Array.from( + grid.querySelectorAll<HTMLElement>(".photo-item"), + ); + const sentinel = document.getElementById("sentinel"); + const status = document.getElementById("load-status"); + + let currentBatch = 0; + const totalBatches = + Math.ceil( + allItems.filter((el) => el.hasAttribute("data-hidden")).length / + BATCH_SIZE, + ) + 1; + + function applyLayout() { + if (window.innerWidth <= 640) { + grid.style.height = ""; + grid.style.position = ""; + for (const item of allItems) { + if (!item.hasAttribute("data-hidden")) { + item.style.cssText = ""; + } + } + return; + } + + const visibleItems = allItems.filter( + (el) => !el.hasAttribute("data-hidden"), + ); + const ratios = visibleItems.map((el) => Number(el.dataset.ar)); + + const result = justifiedLayout(ratios, { + containerWidth: grid.offsetWidth, + targetRowHeight: TARGET_ROW_HEIGHT, + boxSpacing: BOX_SPACING, + containerPadding: 0, + }); + + grid.style.position = "relative"; + grid.style.height = `${result.containerHeight}px`; + + for (let i = 0; i < visibleItems.length; i++) { + const box = result.boxes[i]; + const item = visibleItems[i]; + if (!box || !item) continue; + item.style.position = "absolute"; + item.style.top = `${box.top}px`; + item.style.left = `${box.left}px`; + item.style.width = `${box.width}px`; + item.style.height = `${box.height}px`; + } + } + + function revealNextBatch() { + currentBatch++; + const toReveal = allItems.filter( + (el) => + el.hasAttribute("data-hidden") && + Number(el.dataset.batch) === currentBatch, + ); + + if (toReveal.length === 0) { + sentinel?.remove(); + return; + } + + if (status) status.textContent = "Loading more photos…"; + + for (const el of toReveal) { + el.removeAttribute("data-hidden"); + el.classList.add("is-visible"); + } + + applyLayout(); + + if (currentBatch >= totalBatches) { + sentinel?.remove(); + } + if (status) status.textContent = ""; + } + + let resizeTimer: ReturnType<typeof setTimeout>; + window.addEventListener("resize", () => { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(applyLayout, 150); + }); + + applyLayout(); + + if (sentinel) { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0]?.isIntersecting) { + revealNextBatch(); + } + }, + { rootMargin: "400px" }, + ); + observer.observe(sentinel); + } +</script> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #111; + color: #f5f5f3; + } + + .main { + max-width: var(--width-wide); + margin: 0 auto; + padding: 1.5rem; + width: 100%; + } + + /* ── Justified layout (positioned by JS) ── */ + .photo-grid { + position: relative; + } + + .photo-item { + border-radius: 2px; + overflow: hidden; + transition: opacity 0.4s ease; + } + + .photo-item[data-hidden] { + display: none; + } + + .photo-item.is-visible { + animation: fadeIn 0.4s ease forwards; + } + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + /* ── Photo card ── */ + .photo-link { + display: block; + width: 100%; + height: 100%; + overflow: hidden; + } + + .photo-img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.4s ease; + } + + .photo-link:hover .photo-img { + transform: scale(1.03); + } + + /* ── Sentinel / status ── */ + #sentinel { + height: 1px; + margin-top: 2rem; + } + + .load-status { + text-align: center; + padding: 1rem; + font-size: 0.85rem; + color: #666; + min-height: 2rem; + } + + /* ── Nav / Footer override for dark bg ── */ + .page :global(.nav), + .page :global(.mobile-bar) { + background: #111; + color: #000; + } + + .page :global(.nav a), + .page :global(.nav__icon-btn) { + color: #000; + } + + .page :global(.footer a) { + color: #f5f5f3; + } + + /* ── Mobile: simple column, natural aspect ratios ── */ + @media (max-width: 640px) { + .main { + padding: 0.25rem; + } + + .photo-grid { + display: flex; + flex-direction: column; + gap: 0.5rem; + height: auto !important; + position: static !important; + } + + .photo-item { + position: static !important; + width: 100% !important; + height: auto !important; + top: auto !important; + left: auto !important; + } + + .photo-img { + height: auto; + object-fit: initial; + } + } +</style> diff --git a/src/pages/photos/map.astro b/src/pages/photos/map.astro new file mode 100644 index 0000000..b4729a0 --- /dev/null +++ b/src/pages/photos/map.astro @@ -0,0 +1,353 @@ +--- +import { getImage } from "astro:assets"; +import type { ImageMetadata } from "astro"; +import PhotosSubNav from "@/components/PhotosSubNav.astro"; +import BaseLayout from "@/layouts/BaseLayout.astro"; + +type PhotoSidecar = { + id: string; + title: string[]; + image: string; + alt: string; + location: string; + date: string; + tags: string[]; + exif: { + camera: string; + lens: string; + aperture: string; + iso: string; + focal_length: string; + shutter_speed: string; + }; +}; + +type MarkerData = { + id: string; + collectionSlug: string; + lat: number; + lng: number; + thumbSrc: string; + thumbWidth: number; + thumbHeight: number; + alt: string; + title: string; +}; + +function parseDMS(dms: string): { lat: number; lng: number } | null { + const re = + /(\d+)\s*deg\s*(\d+)'\s*([\d.]+)"\s*([NS])\s*,\s*(\d+)\s*deg\s*(\d+)'\s*([\d.]+)"\s*([EW])/; + const m = dms.match(re); + if (!m) return null; + const [, d1, mi1, s1, dir1, d2, mi2, s2, dir2] = m; + if (!d1 || !mi1 || !s1 || !dir1 || !d2 || !mi2 || !s2 || !dir2) return null; + const toDD = (d: string, min: string, sec: string, dir: string): number => { + const dd = Number(d) + Number(min) / 60 + Number(sec) / 3600; + return dir === "S" || dir === "W" ? -dd : dd; + }; + return { + lat: toDD(d1, mi1, s1, dir1), + lng: toDD(d2, mi2, s2, dir2), + }; +} + +const sidecars = import.meta.glob<PhotoSidecar>( + "/src/content/photos/collections/**/*.json", + { eager: true }, +); + +const imageModules = import.meta.glob<{ default: ImageMetadata }>( + "/src/content/photos/collections/**/*.jpg", + { eager: true }, +); + +const photos = Object.entries(sidecars) + .map(([jsonPath, sidecar]) => { + const imgPath = jsonPath.replace(".json", ".jpg"); + const imageModule = imageModules[imgPath]; + const collectionSlug = jsonPath + .replace("/src/content/photos/collections/", "") + .replace(/\/img\/.*$/, ""); + return { sidecar, image: imageModule?.default, collectionSlug }; + }) + .filter( + ( + p, + ): p is { + sidecar: PhotoSidecar; + image: ImageMetadata; + collectionSlug: string; + } => !!p.image, + ); + +const markers: MarkerData[] = ( + await Promise.all( + photos.map(async ({ sidecar, image, collectionSlug }) => { + const coords = parseDMS(sidecar.location); + if (!coords) return null; + const thumb = await getImage({ src: image, width: 240, format: "jpeg" }); + return { + id: sidecar.id, + collectionSlug, + lat: coords.lat, + lng: coords.lng, + thumbSrc: thumb.src, + thumbWidth: thumb.attributes.width as number, + thumbHeight: thumb.attributes.height as number, + alt: sidecar.alt, + title: sidecar.title[0] ?? sidecar.id, + } satisfies MarkerData; + }), + ) +).filter((m): m is MarkerData => m !== null); + +const markersJson = JSON.stringify(markers); +--- + +<BaseLayout + title="Map" + description="All photo locations mapped — travel and street photography by Adrian Altner." + navDesktopHidden={true} + footerDark={true} +> + <div class="page"> + <main class="main"> + <div class="nav-area"> + <PhotosSubNav /> + </div> + <div id="map" class="map-container"></div> + </main> + </div> + <script + is:inline + type="application/json" + id="map-data" + set:html={markersJson} + /> +</BaseLayout> + +<script> + import "leaflet/dist/leaflet.css"; + import "leaflet.markercluster/dist/MarkerCluster.css"; + import "leaflet.markercluster/dist/MarkerCluster.Default.css"; + import L from "leaflet"; + import "leaflet.markercluster"; + + type MarkerData = { + id: string; + collectionSlug: string; + lat: number; + lng: number; + thumbSrc: string; + thumbWidth: number; + thumbHeight: number; + alt: string; + title: string; + }; + + const raw = document.getElementById("map-data")?.textContent ?? "[]"; + const markers: MarkerData[] = JSON.parse(raw); + + const map = L.map("map", { zoomControl: true }); + + L.tileLayer("https://tiles-eu.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png", { + attribution: + '© <a href="https://stadiamaps.com/" target="_blank">Stadia Maps</a> © <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> © <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>', + maxZoom: 20, + opacity: 0.7, + }).addTo(map); + + const cluster = L.markerClusterGroup({ + iconCreateFunction: (c) => + L.divIcon({ + html: `<div class="cluster-icon">${c.getChildCount()}</div>`, + className: "", + iconSize: [36, 36], + }), + showCoverageOnHover: false, + maxClusterRadius: 60, + }); + + for (const m of markers) { + const popup = `<div class="map-popup"> + <a href="/photos/collections/${m.collectionSlug}/${m.id}" class="map-popup__link"> + <img src="${m.thumbSrc}" width="${m.thumbWidth}" height="${m.thumbHeight}" alt="${m.alt.replace(/"/g, """)}" loading="lazy" class="map-popup__img" /> + <p class="map-popup__title">${m.title}</p> + </a> + </div>`; + + L.circleMarker([m.lat, m.lng], { + radius: 7, + color: "#e8587a", + fillColor: "#e8587a", + fillOpacity: 0.85, + weight: 2, + }) + .bindPopup(popup, { maxWidth: 240 }) + .addTo(cluster); + } + + cluster.addTo(map); + + if (markers.length > 0) { + const bounds = L.latLngBounds(markers.map((m) => [m.lat, m.lng])); + map.fitBounds(bounds, { padding: [40, 40] }); + } else { + map.setView([10, 105], 5); + } +</script> + +<style> + .page { + display: flex; + flex-direction: column; + height: 100dvh; + background: #111; + color: #f5f5f3; + } + + .main { + flex: 1; + width: 100%; + min-height: 0; + display: flex; + flex-direction: column; + } + + .nav-area { + padding: 1.5rem 1.5rem 0; + } + + /* ── Map ── */ + .map-container { + flex: 1; + min-height: 0; + border-radius: 2px; + overflow: hidden; + } + + /* ── Leaflet dark overrides ── */ + :global(.leaflet-popup-content-wrapper) { + background: #1e1e1e !important; + color: #f5f5f3 !important; + border-radius: 4px !important; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6) !important; + padding: 0 !important; + } + + :global(.leaflet-popup-content) { + margin: 0 !important; + width: auto !important; + } + + :global(.leaflet-popup-tip) { + background: #1e1e1e !important; + } + + :global(.leaflet-popup-close-button) { + color: #888 !important; + top: 6px !important; + right: 8px !important; + } + + :global(.leaflet-popup-close-button:hover) { + color: #f5f5f3 !important; + } + + :global(.leaflet-control-zoom a) { + background: #1e1e1e; + color: #f5f5f3; + border-color: #333; + } + + :global(.leaflet-control-zoom a:hover) { + background: #2a2a2a; + color: #fff; + } + + :global(.leaflet-bar) { + border: none; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); + } + + :global(.leaflet-control-attribution) { + background: rgba(17, 17, 17, 0.7); + color: #555; + font-size: 0.6rem; + padding: 1px 4px; + } + + :global(.leaflet-control-attribution a) { + color: #666; + } + + /* ── Cluster icon ── */ + :global(.cluster-icon) { + width: 36px; + height: 36px; + border-radius: 50%; + background: #e8587a; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; + font-weight: 600; + font-family: "Exo 2", sans-serif; + box-shadow: 0 2px 8px rgba(232, 88, 122, 0.4); + } + + /* ── Popup content ── */ + :global(.map-popup) { + width: 220px; + } + + :global(.map-popup__link) { + display: block; + text-decoration: none; + color: inherit; + } + + :global(.map-popup__img) { + display: block; + width: 100%; + height: 148px; + object-fit: cover; + border-radius: 4px 4px 0 0; + } + + :global(.map-popup__title) { + font-size: 0.78rem; + line-height: 1.4; + padding: 0.5rem 0.6rem 0.6rem; + color: #f5f5f3 !important; + margin: 0; + } + + :global(.map-popup__link:hover .map-popup__title) { + color: #f5f5f3; + } + + /* ── Nav / Footer override for dark bg ── */ + .page :global(.nav), + .page :global(.mobile-bar) { + background: #111; + color: #000; + } + + .page :global(.nav a), + .page :global(.nav__icon-btn) { + color: #000; + } + + .page :global(.footer a) { + color: #f5f5f3; + } + + /* ── Mobile ── */ + @media (max-width: 640px) { + .nav-area { + padding: 0.5rem 0.5rem 0; + } + } +</style> diff --git a/src/pages/photos/stats.astro b/src/pages/photos/stats.astro new file mode 100644 index 0000000..b8c262d --- /dev/null +++ b/src/pages/photos/stats.astro @@ -0,0 +1,343 @@ +--- +import PhotosSubNav from "@/components/PhotosSubNav.astro"; +import BaseLayout from "@/layouts/BaseLayout.astro"; + +type PhotoSidecar = { + id: string; + exif: { + camera: string; + lens: string; + aperture: string; + iso: string; + focal_length: string; + shutter_speed: string; + }; +}; + +const sidecars = import.meta.glob<PhotoSidecar>( + "/src/content/photos/collections/**/*.json", + { eager: true }, +); + +function countBy(values: string[]): { labels: string[]; data: number[] } { + const counts: Record<string, number> = {}; + for (const v of values) { + counts[v] = (counts[v] ?? 0) + 1; + } + const labels = Object.keys(counts).sort( + (a, b) => (counts[b] ?? 0) - (counts[a] ?? 0), + ); + return { labels, data: labels.map((l) => counts[l] ?? 0) }; +} + +function bucket( + values: number[], + buckets: { label: string; min: number; max: number }[], +): { labels: string[]; data: number[] } { + const counts: Record<string, number> = {}; + for (const b of buckets) counts[b.label] = 0; + for (const v of values) { + const b = buckets.find((b) => v >= b.min && v < b.max); + if (b) counts[b.label] = (counts[b.label] ?? 0) + 1; + } + const labels = buckets.map((b) => b.label); + return { labels, data: labels.map((l) => counts[l] ?? 0) }; +} + +function parseShutter(s: string): number { + if (s.includes("/")) { + const [num, den] = s.split("/").map(Number); + return (num ?? 1) / (den ?? 1); + } + return Number(s) || 0; +} + +const exifs = Object.values(sidecars) + .map((s) => s.exif) + .filter(Boolean); +const totalPhotos = exifs.length; + +const camera = countBy(exifs.map((e) => e.camera ?? "Unknown")); +function normalizeLens(lens: string): string { + return lens.replace(/^Fujifilm Fujinon /, "").trim(); +} +const lens = countBy(exifs.map((e) => normalizeLens(e.lens ?? "Unknown"))); + +const apertures = exifs + .map((e) => parseFloat(e.aperture)) + .filter((n) => !isNaN(n)); +const apertureUnique = [...new Set(apertures)].sort((a, b) => a - b); +const apertureCounts: Record<string, number> = {}; +for (const v of apertures) { + const k = `f/${v}`; + apertureCounts[k] = (apertureCounts[k] ?? 0) + 1; +} +const aperture = { + labels: apertureUnique.map((v) => `f/${v}`), + data: apertureUnique.map((v) => apertureCounts[`f/${v}`] ?? 0), +}; + +const isoBuckets = [ + { label: "≤ 200", min: 0, max: 201 }, + { label: "400", min: 201, max: 600 }, + { label: "800", min: 600, max: 1200 }, + { label: "1600", min: 1200, max: 2400 }, + { label: "3200", min: 2400, max: 4800 }, + { label: "≥ 6400", min: 4800, max: Infinity }, +]; +const iso = bucket( + exifs.map((e) => parseInt(e.iso)).filter((n) => !isNaN(n)), + isoBuckets, +); + +const focalBuckets = [ + { label: "≤ 24mm", min: 0, max: 25 }, + { label: "25–35mm", min: 25, max: 36 }, + { label: "36–50mm", min: 36, max: 51 }, + { label: "51–85mm", min: 51, max: 86 }, + { label: "86–135mm", min: 86, max: 136 }, + { label: "> 135mm", min: 136, max: Infinity }, +]; +const focal = bucket( + exifs.map((e) => parseFloat(e.focal_length)).filter((n) => !isNaN(n)), + focalBuckets, +); + +const shutterBuckets = [ + { label: "≥ 1s", min: 1, max: Infinity }, + { label: "1/2 – 1s", min: 0.5, max: 1 }, + { label: "1/30 – 1/2", min: 1 / 30, max: 0.5 }, + { label: "1/125 – 1/30", min: 1 / 125, max: 1 / 30 }, + { label: "1/500 – 1/125", min: 1 / 500, max: 1 / 125 }, + { label: "< 1/500", min: 0, max: 1 / 500 }, +]; +const shutter = bucket( + exifs.map((e) => parseShutter(e.shutter_speed)).filter((n) => n > 0), + shutterBuckets, +); + +const metrics = JSON.stringify({ camera, lens, aperture, iso, focal, shutter }); +--- + +<BaseLayout + title="Photo Stats" + description="Metrics and statistics from my photo collection." + navDesktopHidden={true} + footerDark={true} +> + <div class="page"> + <main class="main"> + <PhotosSubNav /> + + <div class="stats-content" data-metrics={metrics}> + <p class="total">{totalPhotos} photos total</p> + + <div class="chart-grid chart-grid--top"> + <section class="chart-section"> + <h2 class="section-title">Camera</h2> + <div class="chart-wrap chart-wrap--tall"> + <canvas id="chart-camera" aria-label="Photos per camera"></canvas> + </div> + </section> + + <section class="chart-section"> + <h2 class="section-title">Lens</h2> + <div class="chart-wrap chart-wrap--tall"> + <canvas id="chart-lens" aria-label="Photos per lens"></canvas> + </div> + </section> + </div> + + <div class="chart-grid"> + <section class="chart-section"> + <h2 class="section-title">Aperture</h2> + <div class="chart-wrap"> + <canvas id="chart-aperture" aria-label="Aperture distribution"></canvas> + </div> + </section> + + <section class="chart-section"> + <h2 class="section-title">ISO</h2> + <div class="chart-wrap"> + <canvas id="chart-iso" aria-label="ISO distribution"></canvas> + </div> + </section> + + <section class="chart-section"> + <h2 class="section-title">Focal Length</h2> + <div class="chart-wrap"> + <canvas id="chart-focal" aria-label="Focal length distribution"></canvas> + </div> + </section> + + <section class="chart-section"> + <h2 class="section-title">Shutter Speed</h2> + <div class="chart-wrap"> + <canvas id="chart-shutter" aria-label="Shutter speed distribution"></canvas> + </div> + </section> + </div> + </div> + </main> + </div> +</BaseLayout> + +<script> + import Chart from "chart.js/auto"; + + const container = document.querySelector<HTMLElement>(".stats-content"); + if (!container) throw new Error("No stats container"); + + const metrics = JSON.parse(container.dataset.metrics ?? "{}"); + + function neon(hex: string, alpha = 0.25) { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return `rgba(${r},${g},${b},${alpha})`; + } + + const COLORS = ["#39ff14", "#ff2d78", "#00e5ff", "#ffaa00", "#bf00ff", "#ff6b00"]; + + function makeChart( + id: string, + labels: string[], + data: number[], + horizontal = false, + colorIndex = 0, + ) { + const hex = COLORS[colorIndex % COLORS.length] ?? "#ffffff"; + const canvas = document.getElementById(id) as HTMLCanvasElement | null; + if (!canvas) return; + if (horizontal) { + const wrap = canvas.parentElement as HTMLElement | null; + if (wrap) wrap.style.height = `${labels.length * 44 + 40}px`; + } + new Chart(canvas, { + type: "bar", + data: { + labels, + datasets: [ + { + label: "Photos", + data, + backgroundColor: neon(hex, 0.2), + borderColor: neon(hex, 0.9), + borderWidth: 1, + borderRadius: 2, + }, + ], + }, + options: { + indexAxis: horizontal ? "y" : "x", + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false }, + tooltip: { + callbacks: { label: (ctx) => ` ${horizontal ? ctx.parsed.x : ctx.parsed.y} photos` }, + }, + }, + scales: { + x: { + ticks: { color: "#999", font: { size: 11 } }, + grid: { color: horizontal ? "rgba(255,255,255,0.06)" : "none" }, + border: { color: "rgba(255,255,255,0.1)" }, + }, + y: { + ticks: { color: "#f5f5f3", font: { size: 12 } }, + grid: { color: horizontal ? "none" : "rgba(255,255,255,0.06)" }, + border: { color: "rgba(255,255,255,0.1)" }, + }, + }, + }, + }); + } + + makeChart("chart-camera", metrics.camera.labels, metrics.camera.data, true, 0); + makeChart("chart-lens", metrics.lens.labels, metrics.lens.data, true, 1); + makeChart("chart-aperture",metrics.aperture.labels,metrics.aperture.data,false,2); + makeChart("chart-iso", metrics.iso.labels, metrics.iso.data, false,3); + makeChart("chart-focal", metrics.focal.labels, metrics.focal.data, false,4); + makeChart("chart-shutter", metrics.shutter.labels, metrics.shutter.data, false,5); +</script> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #111; + color: #f5f5f3; + } + + .main { + max-width: var(--width-wide); + margin: 0 auto; + padding: 1.5rem; + width: 100%; + } + + .stats-content { + padding-top: 1rem; + } + + .total { + font-size: 0.85rem; + color: #555; + margin: 0 0 2rem; + } + + .section-title { + font-size: 0.8rem; + font-weight: 600; + letter-spacing: 0.07em; + text-transform: uppercase; + color: #666; + margin: 0 0 0.75rem; + } + + .chart-section { + margin-bottom: 2.5rem; + } + + .chart-wrap { + position: relative; + height: 220px; + } + + .chart-wrap--tall { + height: 140px; + } + + .chart-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem 3rem; + } + + .chart-grid--top { + margin-bottom: 2.5rem; + } + + @media (max-width: 640px) { + .chart-grid { + grid-template-columns: 1fr; + } + } + + .page :global(.nav), + .page :global(.mobile-bar) { + background: #111; + color: #000; + } + + .page :global(.nav a), + .page :global(.nav__icon-btn) { + color: #000; + } + + .page :global(.footer a) { + color: #f5f5f3; + } +</style> diff --git a/src/pages/photos/tags/[slug].astro b/src/pages/photos/tags/[slug].astro new file mode 100644 index 0000000..c9d9a9b --- /dev/null +++ b/src/pages/photos/tags/[slug].astro @@ -0,0 +1,340 @@ +--- +import { Image } from "astro:assets"; +import type { ImageMetadata } from "astro"; +import PhotosSubNav from "@/components/PhotosSubNav.astro"; +import BaseLayout from "@/layouts/BaseLayout.astro"; +import type { PhotoSidecar } from "@/lib/collections"; + +type TagPhoto = { + sidecar: PhotoSidecar; + image: ImageMetadata; + collectionSlug: string; +}; + +export async function getStaticPaths() { + const sidecars = import.meta.glob<PhotoSidecar>( + "/src/content/photos/collections/**/*.json", + { eager: true }, + ); + const imageModules = import.meta.glob<{ default: ImageMetadata }>( + "/src/content/photos/collections/**/*.jpg", + { eager: true }, + ); + + function toSlug(tag: string): string { + return tag + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^a-z0-9-]/g, ""); + } + + const tagMap = new Map<string, { label: string; photos: TagPhoto[] }>(); + + for (const [jsonPath, sidecar] of Object.entries(sidecars)) { + const imgPath = jsonPath.replace(/\.json$/, ".jpg"); + const image = imageModules[imgPath]?.default; + if (!image) continue; + + const collectionSlug = jsonPath + .replace("/src/content/photos/collections/", "") + .replace(/\/img\/[^/]+\.json$/, ""); + + for (const tag of sidecar.tags) { + const slug = toSlug(tag); + if (!tagMap.has(slug)) tagMap.set(slug, { label: tag, photos: [] }); + tagMap.get(slug)?.photos.push({ sidecar, image, collectionSlug }); + } + } + + return [...tagMap.entries()].map(([slug, { label, photos }]) => ({ + params: { slug }, + props: { + label, + photos: photos.sort( + (a, b) => + new Date(b.sidecar.date).getTime() - + new Date(a.sidecar.date).getTime(), + ), + }, + })); +} + +const { label, photos } = Astro.props as { label: string; photos: TagPhoto[] }; +const BATCH_SIZE = 15; +--- + +<BaseLayout + title={`Tag: ${label}`} + description={`Photos tagged with "${label}" by Adrian Altner.`} + navDesktopHidden={true} + footerDark={true} +> + <div class="page"> + <main class="main"> + <PhotosSubNav /> + + <header class="tag-header"> + <h1 class="tag-title">{label}</h1> + <p class="tag-count"> + {photos.length} + {photos.length === 1 ? "photo" : "photos"} + </p> + </header> + + <div class="photo-grid" id="photo-grid"> + { + photos.map((photo, i) => { + const ar = photo.image.width / photo.image.height; + return ( + <div + class="photo-item" + data-ar={ar} + data-batch={Math.floor(i / BATCH_SIZE)} + data-hidden={i >= BATCH_SIZE ? "true" : undefined} + > + <a + href={`/photos/collections/${photo.collectionSlug}/${photo.sidecar.id}`} + class="photo-link" + > + <Image + src={photo.image} + alt={photo.sidecar.alt} + width={800} + height={Math.round(800 / ar)} + loading="lazy" + class="photo-img" + /> + </a> + </div> + ); + }) + } + </div> + + <div id="sentinel" aria-hidden="true"></div> + <div id="load-status" class="load-status" aria-live="polite"></div> + </main> + </div> +</BaseLayout> + +<script> + import justifiedLayout from "justified-layout"; + + const grid = document.getElementById("photo-grid"); + if (grid) { + const BATCH_SIZE = 15; + const TARGET_ROW_HEIGHT = 280; + const BOX_SPACING = 8; + + const allItems = Array.from( + grid.querySelectorAll<HTMLElement>(".photo-item"), + ); + const sentinel = document.getElementById("sentinel"); + const status = document.getElementById("load-status"); + + let currentBatch = 0; + const totalBatches = + Math.ceil( + allItems.filter((el) => el.hasAttribute("data-hidden")).length / + BATCH_SIZE, + ) + 1; + + function applyLayout() { + if (window.innerWidth <= 640) { + grid!.style.height = ""; + grid!.style.position = ""; + for (const item of allItems) { + if (!item.hasAttribute("data-hidden")) item.style.cssText = ""; + } + return; + } + + const visibleItems = allItems.filter( + (el) => !el.hasAttribute("data-hidden"), + ); + const ratios = visibleItems.map((el) => Number(el.dataset.ar)); + + const result = justifiedLayout(ratios, { + containerWidth: grid!.offsetWidth, + targetRowHeight: TARGET_ROW_HEIGHT, + boxSpacing: BOX_SPACING, + containerPadding: 0, + }); + + grid!.style.position = "relative"; + grid!.style.height = `${result.containerHeight}px`; + + for (let i = 0; i < visibleItems.length; i++) { + const box = result.boxes[i]; + const item = visibleItems[i]; + if (!box || !item) continue; + item.style.position = "absolute"; + item.style.top = `${box.top}px`; + item.style.left = `${box.left}px`; + item.style.width = `${box.width}px`; + item.style.height = `${box.height}px`; + } + } + + function revealNextBatch() { + currentBatch++; + const toReveal = allItems.filter( + (el) => + el.hasAttribute("data-hidden") && + Number(el.dataset.batch) === currentBatch, + ); + if (toReveal.length === 0) { + sentinel?.remove(); + return; + } + if (status) status.textContent = "Loading more photos…"; + for (const el of toReveal) { + el.removeAttribute("data-hidden"); + el.classList.add("is-visible"); + } + applyLayout(); + if (currentBatch >= totalBatches) sentinel?.remove(); + if (status) status.textContent = ""; + } + + let resizeTimer: ReturnType<typeof setTimeout>; + window.addEventListener("resize", () => { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(applyLayout, 150); + }); + + applyLayout(); + + if (sentinel) { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0]?.isIntersecting) revealNextBatch(); + }, + { rootMargin: "400px" }, + ); + observer.observe(sentinel); + } + } +</script> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #111; + color: #f5f5f3; + } + + .main { + max-width: var(--width-wide); + margin: 0 auto; + padding: 1.5rem; + width: 100%; + } + + .tag-header { + margin-bottom: 1.5rem; + } + + .tag-title { + font-size: 1.5rem; + font-weight: 500; + margin-bottom: 0.25rem; + } + + .tag-count { + font-size: 0.85rem; + color: #888; + margin: 0; + } + + .photo-grid { + position: relative; + } + + .photo-item { + border-radius: 2px; + overflow: hidden; + } + + .photo-item[data-hidden] { + display: none; + } + + .photo-item.is-visible { + animation: fadeIn 0.4s ease forwards; + } + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + .photo-link { + display: block; + width: 100%; + height: 100%; + overflow: hidden; + } + + .photo-img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.4s ease; + } + + .photo-link:hover .photo-img { + transform: scale(1.03); + } + + #sentinel { + height: 1px; + margin-top: 2rem; + } + + .load-status { + text-align: center; + padding: 1rem; + font-size: 0.85rem; + color: #666; + min-height: 2rem; + } + + .page :global(.footer a) { + color: #f5f5f3; + } + + @media (max-width: 640px) { + .main { + padding: 0.25rem 0.75rem; + } + + .photo-grid { + display: flex; + flex-direction: column; + gap: 0.5rem; + height: auto !important; + position: static !important; + } + + .photo-item { + position: static !important; + width: 100% !important; + height: auto !important; + } + + .photo-img { + height: auto; + object-fit: initial; + } + } +</style> diff --git a/src/pages/photos/tags/index.astro b/src/pages/photos/tags/index.astro new file mode 100644 index 0000000..c603e0f --- /dev/null +++ b/src/pages/photos/tags/index.astro @@ -0,0 +1,142 @@ +--- +import PhotosSubNav from "@/components/PhotosSubNav.astro"; +import BaseLayout from "@/layouts/BaseLayout.astro"; +import type { PhotoSidecar } from "@/lib/collections"; + +function tagToSlug(tag: string): string { + return tag + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^a-z0-9-]/g, ""); +} + +const sidecars = import.meta.glob<PhotoSidecar>( + "/src/content/photos/collections/**/*.json", + { eager: true }, +); + +const tagMap = new Map<string, { label: string; count: number }>(); +for (const sidecar of Object.values(sidecars)) { + for (const tag of sidecar.tags) { + const slug = tagToSlug(tag); + const entry = tagMap.get(slug); + if (entry) { + entry.count++; + } else { + tagMap.set(slug, { label: tag, count: 1 }); + } + } +} + +const tags = [...tagMap.entries()] + .map(([slug, { label, count }]) => ({ slug, label, count })) + .sort((a, b) => a.label.localeCompare(b.label)); +--- + +<BaseLayout + title="Tags" + description="Browse photos by tag — travel and street photography by Adrian Altner." + navDesktopHidden={true} + footerDark={true} +> + <div class="page"> + <main class="main"> + <PhotosSubNav /> + + <header class="page-header"> + <h1 class="page-title">Tags</h1> + <p class="page-meta">{tags.length} tags</p> + </header> + + <ul class="tag-list" role="list"> + { + tags.map(({ slug, label, count }) => ( + <li> + <a href={`/photos/tags/${slug}`} class="tag-pill"> + {label} + <span class="tag-count">{count}</span> + </a> + </li> + )) + } + </ul> + </main> + </div> +</BaseLayout> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #111; + color: #f5f5f3; + } + + .main { + max-width: var(--width-wide); + margin: 0 auto; + padding: 1.5rem; + width: 100%; + } + + .page-header { + margin-bottom: 1.5rem; + } + + .page-title { + font-size: 1.5rem; + font-weight: 500; + margin-bottom: 0.25rem; + } + + .page-meta { + font-size: 0.85rem; + color: #888; + margin: 0; + } + + .tag-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + list-style: none; + padding: 0; + margin: 0; + } + + .tag-pill { + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-size: 0.85rem; + color: #aaa; + border: 1px solid #333; + border-radius: 3px; + padding: 0.3rem 0.65rem; + text-decoration: none; + transition: + color 0.2s, + border-color 0.2s; + } + + .tag-pill:hover { + color: #f5f5f3; + border-color: #555; + } + + .tag-count { + font-size: 0.75rem; + color: #555; + } + + .page :global(.footer a) { + color: #f5f5f3; + } + + @media (max-width: 640px) { + .main { + padding: 0.75rem; + } + } +</style> diff --git a/src/pages/privacy-policy.md b/src/pages/privacy-policy.md new file mode 100644 index 0000000..4433d0c --- /dev/null +++ b/src/pages/privacy-policy.md @@ -0,0 +1,119 @@ +--- +layout: ../layouts/ProseLayout.astro +title: Privacy Policy +description: Privacy policy and data protection information for adrian-altner.com. +--- + +# Privacy Policy + +*Last updated: March 2026 (added: hosting provider, interactive map)* + +--- + +## 1. Controller + +The controller responsible for data processing on this website within the meaning of the General Data Protection Regulation (GDPR) is: + +<strong><span class="r" data-obf="cmVudGxBIG5haXJkQQ=="></span></strong><br> +**Email:** <a class="r" data-obf="bW9je3RvZH1yZW50bGEtbmFpcmRhe3RhfXllaA==" data-obf-href="bWFpbHRvOmhleUBhZHJpYW4tYWx0bmVyLmNvbQ=="></a> + +--- + +## 2. General Information on Data Processing + +We take the protection of your personal data very seriously. We treat your personal data confidentially and in accordance with the statutory data protection regulations and this privacy policy. + +As a rule, it is possible to use this website without providing personal data. If personal data (e.g. name or email address) is collected on this website, this is done on a voluntary basis where possible. This data will not be passed on to third parties without your explicit consent. + +--- + +## 3. Data Collection When Visiting This Website + +**Server Log Files** + +When you visit this website, our hosting provider automatically collects and stores information in server log files that your browser transmits to us. This includes: + +- Your IP address +- Date and time of the request +- Content of the request (specific page) +- Access status / HTTP status code +- Amount of data transferred +- Website from which the request originated (referrer) +- Browser type and version +- Operating system + +This data is not merged with other data sources. The legal basis for this processing is Art. 6(1)(f) GDPR (legitimate interest). Our legitimate interest lies in the technically error-free presentation and optimisation of this website. + +The server log files are stored for a maximum of 7 days and then deleted, unless further retention is required for security purposes. + +--- + +## 4. Hosting + +This website is hosted by **Hetzner Online GmbH**, Industriestr. 25, 91710 Gunzenhausen, Germany (<https://www.hetzner.com>). All data processing takes place on servers within the European Union, ensuring a high level of data protection in accordance with GDPR. + +When you visit this website, Hetzner may process technical connection data (e.g. IP address) as part of server operations. We have concluded a data processing agreement (DPA) with Hetzner in accordance with Art. 28 GDPR. + +--- + +## 5. Interactive Map (Stadia Maps) + +The photo map page (`/photos/map`) uses map tiles provided by **Stadia Maps** (Stadia Maps, LLC, <https://stadiamaps.com>). When you visit this page, your browser loads map tiles directly from Stadia Maps servers located in the European Union (`tiles-eu.stadiamaps.com`). This causes your IP address and browser information to be transmitted to Stadia Maps. + +Stadia Maps does not track users across websites and processes data in accordance with GDPR. For details, see their privacy policy: <https://stadiamaps.com/privacy-policy/>. A Data Processing Addendum (DPA) is available at <https://stadiamaps.com/legal/data-processing-addendum/>. + +The legal basis for this processing is Art. 6(1)(f) GDPR (legitimate interest in providing an interactive map of photo locations). Map tile requests are only made when you actively visit the map page. + +The map data is provided by **OpenStreetMap** contributors (<https://www.openstreetmap.org/copyright>, ODbL license) and processed into vector tiles by **OpenMapTiles** (<https://openmaptiles.org>). + +--- + +## 6. Cookies + +This website does not use cookies, tracking technologies, or any analytics tools. No information is stored on your device when you visit this website. + +--- + +## 7. Contact + +If you contact us by email, the data you provide (your email address and the content of your message) will be stored for the purpose of processing your inquiry and in case of follow-up questions. This data will not be passed on to third parties without your consent. + +The legal basis for processing this data is Art. 6(1)(f) GDPR (legitimate interest in responding to your inquiry) or, where your inquiry aims at concluding a contract, Art. 6(1)(b) GDPR. + +You may object to the storage of your data at any time. In this case, the conversation cannot be continued. All personal data stored in the course of the contact will be deleted. + +--- + +## 8. Your Rights + +You have the following rights regarding your personal data: + +- **Right of access** (Art. 15 GDPR) – you may request information about the personal data we process about you. +- **Right to rectification** (Art. 16 GDPR) – you may request correction of inaccurate or incomplete data. +- **Right to erasure** (Art. 17 GDPR) – you may request deletion of your data under certain conditions. +- **Right to restriction of processing** (Art. 18 GDPR) – you may request that processing of your data be restricted. +- **Right to data portability** (Art. 20 GDPR) – you may request to receive your data in a structured, machine-readable format. +- **Right to object** (Art. 21 GDPR) – you may object to processing based on legitimate interests at any time. + +To exercise any of these rights, please contact us at the address provided above. + +--- + +## 9. Right to Lodge a Complaint + +You have the right to lodge a complaint with a data protection supervisory authority. The supervisory authority competent for you depends on your place of residence or the location of the alleged infringement. + +In Germany, the competent authority is typically the data protection authority of the federal state in which you reside. A list of all German supervisory authorities is available at: +<https://www.bfdi.bund.de/DE/Service/Anschriften/Laender/Laender-node.html> + +--- + +## 10. Data Security + +This website uses SSL/TLS encryption for security reasons and to protect the transmission of confidential content. You can recognise an encrypted connection by the padlock icon in your browser address bar and by the `https://` prefix. + +--- + +## 11. Changes to This Privacy Policy + +We reserve the right to update this privacy policy to reflect changes in legal requirements or changes to our services. The current version of the privacy policy is always available on this page. Please check this page regularly. diff --git a/src/pages/projects/[slug].astro b/src/pages/projects/[slug].astro new file mode 100644 index 0000000..91b514e --- /dev/null +++ b/src/pages/projects/[slug].astro @@ -0,0 +1,236 @@ +--- +import { Image } from "astro:assets"; +import { getCollection, render } from "astro:content"; +import BaseLayout from "@/layouts/BaseLayout.astro"; + +export async function getStaticPaths() { + const projects = await getCollection("projects", ({ data }) => !data.draft); + return projects.map((project) => ({ + params: { slug: project.id }, + props: { project }, + })); +} + +const { project } = Astro.props; +const { Content } = await render(project); + +function capitalize(s: string) { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +function formatDate(date: Date) { + return date.toLocaleDateString("de-DE", { + year: "numeric", + month: "long", + day: "numeric", + }); +} +--- + +<BaseLayout title={project.data.title} description={project.data.description}> + <div class="page"> + <main class="main"> + <a href="/projects" class="back">← Projects</a> + <article> + <header class="project-header"> + <h1 class="project-title">{project.data.title}</h1> + <time class="project-date" datetime={project.data.publishDate.toISOString()}> + {formatDate(project.data.publishDate)} + </time> + <div class="project-links"> + {project.data.url && ( + <a href={project.data.url} class="project-link" target="_blank" rel="noopener noreferrer"> + Live ↗ + </a> + )} + {project.data.github && ( + <a href={project.data.github} class="project-link" target="_blank" rel="noopener noreferrer"> + GitHub ↗ + </a> + )} + </div> + {project.data.tags.length > 0 && ( + <div class="project-tags"> + {project.data.tags.map((tag) => ( + <span class="tag">{capitalize(tag)}</span> + ))} + </div> + )} + </header> + {project.data.cover && ( + <Image class="project-cover" src={project.data.cover} alt={project.data.coverAlt ?? ""} /> + )} + <div class="prose"> + <Content /> + </div> + </article> + <a href="/projects" class="back">← Projects</a> + </main> + </div> +</BaseLayout> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #f5f5f3; + color: #111; + } + + .main { + max-width: var(--width-prose); + margin: 0 auto; + padding: 2rem 2rem 4rem; + width: 100%; + } + + .back { + display: inline-block; + font-size: 0.9rem; + color: #555; + text-decoration: none; + border-bottom: 1px solid #ccc; + padding-bottom: 1px; + margin-bottom: 1.5rem; + } + + .back:hover { + color: var(--accent); + border-color: var(--accent); + } + + article + .back { + margin-bottom: 0; + margin-top: 1rem; + } + + .project-header { + margin-bottom: 2rem; + padding-bottom: 1.25rem; + border-bottom: 1px solid #e0e0de; + } + + .project-title { + font-size: clamp(1.5rem, 4vw, 2.25rem); + font-weight: 400; + letter-spacing: -0.02em; + line-height: 1.2; + margin-bottom: 0.5rem; + } + + .project-date { + font-size: 0.875rem; + color: #888; + display: block; + } + + .project-links { + display: flex; + gap: 1rem; + margin-top: 0.75rem; + } + + .project-link { + font-size: 0.875rem; + color: var(--accent); + text-decoration: none; + } + + .project-link:hover { + text-decoration: underline; + } + + .project-tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 0.75rem; + } + + .tag { + font-size: 0.75rem; + color: #666; + background: #e8e8e6; + padding: 0.15em 0.55em; + border-radius: 3px; + } + + .project-cover { + width: 100%; + height: auto; + border-radius: 6px; + display: block; + margin-bottom: 2rem; + } + + .prose { + font-size: 1rem; + line-height: 1.7; + color: #333; + } + + .prose :global(h2) { + font-size: 1.4rem; + font-weight: 500; + margin: 2rem 0 0.75rem; + color: #111; + } + + .prose :global(h3) { + font-size: 1.15rem; + font-weight: 500; + margin: 1.5rem 0 0.5rem; + color: #111; + } + + .prose :global(p) { + margin-bottom: 1.25rem; + } + + .prose :global(a) { + color: var(--accent); + text-underline-offset: 3px; + } + + .prose :global(blockquote) { + border-left: 3px solid var(--accent); + margin: 1.5rem 0; + padding: 0.25rem 0 0.25rem 1.25rem; + color: #555; + font-style: italic; + } + + .prose :global(code) { + font-family: ui-monospace, monospace; + font-size: 0.875em; + background: #e8e8e6; + padding: 0.1em 0.35em; + border-radius: 3px; + } + + .prose :global(pre) { + background: #2b2b2b; + color: #e0e0e0; + padding: 1.25rem; + border-radius: 6px; + overflow-x: auto; + margin: 1.5rem 0; + } + + .prose :global(pre code) { + background: none; + padding: 0; + font-size: 0.875rem; + } + + .prose :global(ul), + .prose :global(ol) { + padding-left: 1.5rem; + margin-bottom: 1.25rem; + } + + .prose :global(li) { + margin-bottom: 0.4rem; + } +</style> diff --git a/src/pages/projects/category/[slug].astro b/src/pages/projects/category/[slug].astro new file mode 100644 index 0000000..37b1250 --- /dev/null +++ b/src/pages/projects/category/[slug].astro @@ -0,0 +1,209 @@ +--- +import { Image } from "astro:assets"; +import { getCollection } from "astro:content"; +import BaseLayout from "@/layouts/BaseLayout.astro"; + +export async function getStaticPaths() { + const categories = await getCollection("projects_categories"); + return categories.map((category) => ({ + params: { slug: category.id }, + props: { category }, + })); +} + +const { category } = Astro.props; + +const projects = ( + await getCollection( + "projects", + ({ data }) => !data.draft && data.category?.id === category.id, + ) +).sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()); + +function capitalize(s: string) { + return s.charAt(0).toUpperCase() + s.slice(1); +} +--- + +<BaseLayout + title={`Projects: ${category.data.title}`} + description={category.data.description} +> + <div class="page"> + <main class="main"> + <a href="/projects" class="back">← Projects</a> + <h1 class="heading">{category.data.title}</h1> + {category.data.description && ( + <p class="description">{category.data.description}</p> + )} + {category.data.cover && ( + <Image class="category-cover" src={category.data.cover} alt={category.data.coverAlt ?? ""} /> + )} + { + projects.length === 0 ? ( + <p class="empty">Keine Projekte in dieser Kategorie.</p> + ) : ( + <ul class="list"> + {projects.map((project) => ( + <li class="item"> + <div class="item__meta"> + <a href={`/projects/${project.id}`} class="item__title"> + {project.data.title} + </a> + <div class="item__links"> + {project.data.url && ( + <a href={project.data.url} class="item__link" target="_blank" rel="noopener noreferrer"> + Live ↗ + </a> + )} + {project.data.github && ( + <a href={project.data.github} class="item__link" target="_blank" rel="noopener noreferrer"> + GitHub ↗ + </a> + )} + </div> + </div> + {project.data.description && ( + <p class="item__description">{project.data.description}</p> + )} + {project.data.tags.length > 0 && ( + <div class="item__tags"> + {project.data.tags.map((tag) => ( + <span class="tag">{capitalize(tag)}</span> + ))} + </div> + )} + </li> + ))} + </ul> + ) + } + </main> + </div> +</BaseLayout> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #f5f5f3; + color: #111; + } + + .main { + max-width: var(--width-prose); + margin: 0 auto; + padding: 2rem 2rem 4rem; + width: 100%; + } + + .heading { + font-size: 1.75rem; + font-weight: 400; + letter-spacing: -0.02em; + margin-bottom: 0.75rem; + } + + .description { + color: #555; + font-size: 0.95rem; + margin-bottom: 2rem; + } + + .category-cover { + width: 100%; + height: auto; + border-radius: 6px; + display: block; + margin-bottom: 2rem; + } + + .empty { + color: #888; + font-size: 0.95rem; + } + + .back { + display: inline-block; + margin-bottom: 1.5rem; + font-size: 0.9rem; + color: #555; + text-decoration: none; + } + + .back:hover { + color: var(--accent); + } + + .list { + list-style: none; + padding: 0; + display: flex; + flex-direction: column; + } + + .item { + padding: 1.25rem 0; + border-bottom: 1px solid #e0e0de; + } + + .item:first-child { + border-top: 1px solid #e0e0de; + } + + .item__meta { + display: flex; + align-items: baseline; + gap: 1rem; + flex-wrap: wrap; + } + + .item__title { + font-size: 1rem; + font-weight: 500; + color: #111; + text-decoration: none; + } + + .item__title:hover { + color: var(--accent); + } + + .item__links { + display: flex; + gap: 0.75rem; + } + + .item__link { + font-size: 0.8rem; + color: #888; + text-decoration: none; + } + + .item__link:hover { + color: var(--accent); + } + + .item__description { + font-size: 0.9rem; + color: #555; + margin-top: 0.4rem; + line-height: 1.5; + } + + .item__tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 0.75rem; + } + + .tag { + font-size: 0.75rem; + color: #666; + background: #e8e8e6; + padding: 0.15em 0.55em; + border-radius: 3px; + } +</style> diff --git a/src/pages/projects/index.astro b/src/pages/projects/index.astro new file mode 100644 index 0000000..0f51dc2 --- /dev/null +++ b/src/pages/projects/index.astro @@ -0,0 +1,172 @@ +--- +import { getCollection } from "astro:content"; +import BaseLayout from "@/layouts/BaseLayout.astro"; + +const projects = ( + await getCollection("projects", ({ data }) => !data.draft) +).sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()); + +function capitalize(s: string) { + return s.charAt(0).toUpperCase() + s.slice(1); +} +--- + +<BaseLayout title="Projects" description="Projekte von Adrian Altner."> + <div class="page"> + <main class="main"> + <a href="/" class="back">← Home</a> + <h1 class="heading">Projects</h1> + { + projects.length === 0 ? ( + <p class="empty">Noch keine Projekte vorhanden.</p> + ) : ( + <ul class="list"> + {projects.map((project) => ( + <li class="item"> + <div class="item__meta"> + <a href={`/projects/${project.id}`} class="item__title"> + {project.data.title} + </a> + <div class="item__links"> + {project.data.url && ( + <a href={project.data.url} class="item__link" target="_blank" rel="noopener noreferrer"> + Live ↗ + </a> + )} + {project.data.github && ( + <a href={project.data.github} class="item__link" target="_blank" rel="noopener noreferrer"> + GitHub ↗ + </a> + )} + </div> + </div> + {project.data.description && ( + <p class="item__description">{project.data.description}</p> + )} + {project.data.tags.length > 0 && ( + <div class="item__tags"> + {project.data.tags.map((tag) => ( + <span class="tag">{capitalize(tag)}</span> + ))} + </div> + )} + </li> + ))} + </ul> + ) + } + </main> + </div> +</BaseLayout> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #f5f5f3; + color: #111; + } + + .main { + max-width: var(--width-prose); + margin: 0 auto; + padding: 2rem 2rem 4rem; + width: 100%; + } + + .heading { + font-size: 1.75rem; + font-weight: 400; + letter-spacing: -0.02em; + margin-bottom: 2.5rem; + } + + .empty { + color: #888; + font-size: 0.95rem; + } + + .back { + display: inline-block; + margin-bottom: 1.5rem; + font-size: 0.9rem; + color: #555; + text-decoration: none; + } + + .back:hover { + color: var(--accent); + } + + .list { + list-style: none; + padding: 0; + display: flex; + flex-direction: column; + } + + .item { + padding: 1.25rem 0; + border-bottom: 1px solid #e0e0de; + } + + .item:first-child { + border-top: 1px solid #e0e0de; + } + + .item__meta { + display: flex; + align-items: baseline; + gap: 1rem; + flex-wrap: wrap; + } + + .item__title { + font-size: 1rem; + font-weight: 500; + color: #111; + text-decoration: none; + } + + .item__title:hover { + color: var(--accent); + } + + .item__links { + display: flex; + gap: 0.75rem; + } + + .item__link { + font-size: 0.8rem; + color: #888; + text-decoration: none; + } + + .item__link:hover { + color: var(--accent); + } + + .item__description { + font-size: 0.9rem; + color: #555; + margin-top: 0.4rem; + line-height: 1.5; + } + + .item__tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 0.75rem; + } + + .tag { + font-size: 0.75rem; + color: #666; + background: #e8e8e6; + padding: 0.15em 0.55em; + border-radius: 3px; + } +</style> diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts new file mode 100644 index 0000000..a602452 --- /dev/null +++ b/src/pages/rss.xml.ts @@ -0,0 +1,71 @@ +import { getCollection } from "astro:content"; +import rss from "@astrojs/rss"; +import type { APIContext, ImageMetadata } from "astro"; +import type { PhotoSidecar } from "@/lib/collections"; +import { buildCollectionPhotos, collectionSlug } from "@/lib/collections"; + +export async function GET(context: APIContext) { + const articles = await getCollection("blog", ({ data }) => !data.draft); + const notes = await getCollection("notes", ({ data }) => !data.draft); + const links = await getCollection("links_json"); + const allCollections = await getCollection( + "collections_photos", + ({ data }) => !data.draft, + ); + + const sidecars = import.meta.glob<PhotoSidecar>( + "/src/content/photos/collections/**/*.json", + { eager: true }, + ); + const imageModules = import.meta.glob<{ default: ImageMetadata }>( + "/src/content/photos/collections/**/*.jpg", + { eager: true }, + ); + + const articleItems = articles.map((post) => ({ + title: post.data.title, + description: post.data.description, + pubDate: post.data.publishDate, + link: `/blog/${post.id}/`, + })); + + const noteItems = notes.map((note) => ({ + title: note.data.title, + description: note.data.description ?? note.data.title, + pubDate: note.data.publishDate, + link: `/notes/${note.id}/`, + })); + + const linkItems = links.map((link) => ({ + title: link.data.title, + description: link.data.description ?? link.data.title, + pubDate: link.data.publishDate, + link: `/links/${link.id}/`, + })); + + const photoItems = allCollections.flatMap((col) => { + const slug = collectionSlug(col); + return buildCollectionPhotos(sidecars, imageModules, slug).map((p) => ({ + title: p.sidecar.title[0] ?? p.sidecar.id, + description: p.sidecar.alt, + pubDate: new Date(p.sidecar.date), + link: `/photos/collections/${slug}/${p.sidecar.id}/`, + })); + }); + + const items = [ + ...articleItems, + ...noteItems, + ...linkItems, + ...photoItems, + ].sort((a, b) => b.pubDate.valueOf() - a.pubDate.valueOf()); + + return rss({ + title: "Adrian Altner", + description: + "Passionate photographer and avid traveler, bridging a deep technological curiosity with a keen eye for the latest digital innovations.", + site: context.site ?? new URL("https://adrian-altner.com"), + items, + customData: "<language>en-us</language>", + }); +} diff --git a/src/pages/rss/blog.xml.ts b/src/pages/rss/blog.xml.ts new file mode 100644 index 0000000..0d820d9 --- /dev/null +++ b/src/pages/rss/blog.xml.ts @@ -0,0 +1,24 @@ +import { getCollection } from "astro:content"; +import rss from "@astrojs/rss"; +import type { APIContext } from "astro"; + +export async function GET(context: APIContext) { + const articles = await getCollection("blog", ({ data }) => !data.draft); + + const items = articles + .sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()) + .map((post) => ({ + title: post.data.title, + description: post.data.description, + pubDate: post.data.publishDate, + link: `/blog/${post.id}/`, + })); + + return rss({ + title: "Adrian Altner — Blog", + description: "Articles on technology, web development, and photography.", + site: context.site ?? new URL("https://adrian-altner.com"), + items, + customData: "<language>en-us</language>", + }); +} diff --git a/src/pages/rss/links.xml.ts b/src/pages/rss/links.xml.ts new file mode 100644 index 0000000..9568733 --- /dev/null +++ b/src/pages/rss/links.xml.ts @@ -0,0 +1,22 @@ +import rss from "@astrojs/rss"; +import type { APIContext } from "astro"; +import { getLinks } from "@/lib/collections"; + +export async function GET(context: APIContext) { + const links = await getLinks(); + + const items = links.map((link) => ({ + title: link.data.title, + description: link.data.description ?? link.data.title, + pubDate: link.data.publishDate, + link: `/links/${link.id}/`, + })); + + return rss({ + title: "Adrian Altner — Links", + description: "Curated links worth sharing.", + site: context.site ?? new URL("https://adrian-altner.com"), + items, + customData: "<language>en-us</language>", + }); +} diff --git a/src/pages/rss/notes.xml.ts b/src/pages/rss/notes.xml.ts new file mode 100644 index 0000000..f77a017 --- /dev/null +++ b/src/pages/rss/notes.xml.ts @@ -0,0 +1,24 @@ +import { getCollection } from "astro:content"; +import rss from "@astrojs/rss"; +import type { APIContext } from "astro"; + +export async function GET(context: APIContext) { + const notes = await getCollection("notes", ({ data }) => !data.draft); + + const items = notes + .sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()) + .map((note) => ({ + title: note.data.title, + description: note.data.description ?? note.data.title, + pubDate: note.data.publishDate, + link: `/notes/${note.id}/`, + })); + + return rss({ + title: "Adrian Altner — Notes", + description: "Short-form notes and observations.", + site: context.site ?? new URL("https://adrian-altner.com"), + items, + customData: "<language>en-us</language>", + }); +} diff --git a/src/pages/rss/photos.xml.ts b/src/pages/rss/photos.xml.ts new file mode 100644 index 0000000..9ddfffc --- /dev/null +++ b/src/pages/rss/photos.xml.ts @@ -0,0 +1,41 @@ +import { getCollection } from "astro:content"; +import rss from "@astrojs/rss"; +import type { APIContext, ImageMetadata } from "astro"; +import type { PhotoSidecar } from "@/lib/collections"; +import { buildCollectionPhotos, collectionSlug } from "@/lib/collections"; + +export async function GET(context: APIContext) { + const allCollections = await getCollection( + "collections_photos", + ({ data }) => !data.draft, + ); + + const sidecars = import.meta.glob<PhotoSidecar>( + "/src/content/photos/collections/**/*.json", + { eager: true }, + ); + const imageModules = import.meta.glob<{ default: ImageMetadata }>( + "/src/content/photos/collections/**/*.jpg", + { eager: true }, + ); + + const items = allCollections + .flatMap((col) => { + const slug = collectionSlug(col); + return buildCollectionPhotos(sidecars, imageModules, slug).map((p) => ({ + title: p.sidecar.title[0] ?? p.sidecar.id, + description: p.sidecar.alt, + pubDate: new Date(p.sidecar.date), + link: `/photos/collections/${slug}/${p.sidecar.id}/`, + })); + }) + .sort((a, b) => b.pubDate.valueOf() - a.pubDate.valueOf()); + + return rss({ + title: "Adrian Altner — Photos", + description: "Photography from travels and photowalks.", + site: context.site ?? new URL("https://adrian-altner.com"), + items, + customData: "<language>en-us</language>", + }); +} diff --git a/src/pages/tags/[slug].astro b/src/pages/tags/[slug].astro new file mode 100644 index 0000000..7f5dfea --- /dev/null +++ b/src/pages/tags/[slug].astro @@ -0,0 +1,372 @@ +--- +import { getCollection } from "astro:content"; +import BlogPostItem from "@/components/BlogPostItem.astro"; +import BaseLayout from "@/layouts/BaseLayout.astro"; +import { getLinks } from "@/lib/collections"; + +export async function getStaticPaths() { + const [notes, articles, links, allCategories] = await Promise.all([ + getCollection("notes", ({ data }) => !data.draft), + getCollection("blog", ({ data }) => !data.draft), + getLinks(), + getCollection("categories"), + ]); + + const categoryTitleMap = new Map( + allCategories.map((c) => [c.id, c.data.title]), + ); + + const allTags = new Set([ + ...notes.flatMap((n) => n.data.tags), + ...articles.flatMap((p) => p.data.tags), + ...links.flatMap((l) => l.data.tags), + ]); + + return [...allTags].map((tag) => ({ + params: { slug: tag }, + props: { + tag, + notes: notes + .filter((n) => n.data.tags.includes(tag)) + .sort( + (a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf(), + ), + articles: articles + .filter((p) => p.data.tags.includes(tag)) + .sort( + (a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf(), + ), + links: links + .filter((l) => l.data.tags.includes(tag)) + .sort( + (a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf(), + ), + categoryTitleMap, + }, + })); +} + +const { tag, notes, articles, links, categoryTitleMap } = Astro.props; + +function capitalize(s: string) { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +function formatDate(date: Date) { + return date.toLocaleDateString("de-DE", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); +} + +function getDomain(url: string) { + try { + return new URL(url).hostname.replace(/^www\./, ""); + } catch { + return url; + } +} + +const total = notes.length + articles.length + links.length; +--- + +<BaseLayout + title={capitalize(tag)} + description={`All content tagged with "${capitalize(tag)}".`} +> + <div class="page"> + <main class="main"> + <a href="/tags" class="back">← Tags</a> + <h1 class="heading"><span class="tag-name">{capitalize(tag)}</span></h1> + <p class="count">{total} item{total !== 1 ? "s" : ""}</p> + + {notes.length > 0 && ( + <section class="section"> + <h2 class="section__heading">Notes</h2> + <ul class="list"> + {notes.map((note) => ( + <li class="item"> + <time class="item__date" datetime={note.data.publishDate.toISOString()}> + {formatDate(note.data.publishDate)} + </time> + <a href={`/notes/${note.id}`} class="item__title"> + {note.data.title} + </a> + {note.data.tags.length > 0 && ( + <div class="item__tags"> + {note.data.tags.map((t) => ( + <a + href={`/tags/${t}`} + class={`tag${t === tag ? " tag--active" : ""}`} + > + {capitalize(t)} + </a> + ))} + </div> + )} + </li> + ))} + </ul> + </section> + )} + + {articles.length > 0 && ( + <section class="section"> + <h2 class="section__heading">Blog</h2> + <ul class="list list--articles"> + {articles.map((post) => ( + <BlogPostItem + post={post} + categoryTitleMap={categoryTitleMap} + activeTagId={tag} + /> + ))} + </ul> + </section> + )} + + {links.length > 0 && ( + <section class="section"> + <h2 class="section__heading">Links</h2> + <ul class="list"> + {links.map((link) => ( + <li class="item item--link"> + <div class="item__top"> + <a href={`/links/${link.id}`} class="item__title"> + {link.data.title} + </a> + <time class="item__date" datetime={link.data.publishDate.toISOString()}> + {formatDate(link.data.publishDate)} + </time> + </div> + <a + href={link.data.url} + class="item__url" + target="_blank" + rel="noopener noreferrer" + > + <svg + width="11" + height="11" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + stroke-width="2.5" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + > + <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /> + <polyline points="15 3 21 3 21 9" /> + <line x1="10" y1="14" x2="21" y2="3" /> + </svg> + {getDomain(link.data.url)} + </a> + {link.data.description && ( + <p class="item__desc">{link.data.description}</p> + )} + {link.data.via && ( + <p class="item__via">via {link.data.via}</p> + )} + {link.data.tags.length > 0 && ( + <div class="item__tags"> + {link.data.tags.map((t) => ( + <a + href={`/tags/${t}`} + class={`tag${t === tag ? " tag--active" : ""}`} + > + {capitalize(t)} + </a> + ))} + </div> + )} + </li> + ))} + </ul> + </section> + )} + </main> + </div> +</BaseLayout> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #f5f5f3; + color: #111; + } + + .main { + max-width: var(--width-prose); + margin: 0 auto; + padding: 2rem 2rem 4rem; + width: 100%; + } + + .back { + display: inline-block; + margin-bottom: 1.5rem; + font-size: 0.9rem; + color: #555; + text-decoration: none; + } + + .back:hover { + color: var(--accent); + } + + .heading { + font-size: 1.75rem; + font-weight: 400; + letter-spacing: -0.02em; + margin-bottom: 0.25rem; + } + + .tag-name { + color: var(--accent); + } + + .count { + font-size: 0.875rem; + color: #888; + margin-bottom: 2.5rem; + } + + .section { + margin-bottom: 3rem; + } + + .section__heading { + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #888; + border-top: 2px solid #111; + padding-top: 0.6rem; + margin-bottom: 0.5rem; + } + + .list { + list-style: none; + padding: 0; + display: flex; + flex-direction: column; + } + + .list--articles { + gap: 1.5rem; + } + + /* Notes + Links items */ + .item { + display: grid; + grid-template-columns: 7rem 1fr; + gap: 0.25rem 1.25rem; + padding: 1rem 0; + border-bottom: 1px solid #e0e0de; + align-items: baseline; + } + + .item:first-child { + border-top: 1px solid #e0e0de; + } + + /* Links items override to block layout */ + .item--link { + display: block; + padding: 1rem 0; + } + + .item__date { + font-size: 0.8rem; + color: #888; + white-space: nowrap; + padding-top: 0.15rem; + } + + .item__title { + font-size: 1rem; + font-weight: 500; + color: #111; + text-decoration: none; + } + + .item__title:hover { + color: var(--accent); + } + + /* Link-specific layout */ + .item__top { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; + } + + .item__url { + display: inline-flex; + align-items: center; + gap: 0.25rem; + font-size: 0.8rem; + color: #888; + text-decoration: none; + margin-top: 0.2rem; + } + + .item__url:hover { + color: var(--accent); + } + + .item__desc { + font-size: 0.875rem; + color: #555; + margin: 0.4rem 0 0; + line-height: 1.5; + } + + .item__via { + font-size: 0.8rem; + color: #888; + margin: 0.25rem 0 0; + } + + .item__tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 0.6rem; + grid-column: 1 / -1; + } + + .tag { + font-size: 0.75rem; + color: #666; + background: #e8e8e6; + padding: 0.15em 0.55em; + border-radius: 3px; + text-decoration: none; + } + + .tag:hover { + background: var(--accent); + color: #fff; + } + + .tag--active { + background: var(--accent); + color: #fff; + } + + + @media (max-width: 480px) { + .item { + grid-template-columns: 1fr; + gap: 0.2rem; + } + } +</style> diff --git a/src/pages/tags/index.astro b/src/pages/tags/index.astro new file mode 100644 index 0000000..2261fe4 --- /dev/null +++ b/src/pages/tags/index.astro @@ -0,0 +1,128 @@ +--- +import { getCollection } from "astro:content"; +import BaseLayout from "@/layouts/BaseLayout.astro"; + +const [notes, articles, links] = await Promise.all([ + getCollection("notes", ({ data }) => !data.draft), + getCollection("blog", ({ data }) => !data.draft), + getCollection("links_json"), +]); + +function capitalize(s: string) { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +const tagCounts = new Map<string, number>(); +for (const item of [...notes, ...articles, ...links]) { + for (const tag of item.data.tags) { + tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1); + } +} + +const tags = [...tagCounts.entries()] + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([slug, count]) => ({ slug, title: capitalize(slug), count })); +--- + +<BaseLayout title="Tags" description="All tags on adrian-altner.com."> + <div class="page"> + <main class="main"> + <a href="/" class="back">← Home</a> + <h1 class="heading">Tags</h1> + {tags.length === 0 ? ( + <p class="empty">No tags yet.</p> + ) : ( + <ul class="list"> + {tags.map(({ slug, title, count }) => ( + <li class="item"> + <a href={`/tags/${slug}`} class="item__link"> + <span class="item__title">{title}</span> + <span class="item__count">{count}</span> + </a> + </li> + ))} + </ul> + )} + </main> + </div> +</BaseLayout> + +<style> + .page { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100dvh; + background: #f5f5f3; + color: #111; + } + + .main { + max-width: var(--width-prose); + margin: 0 auto; + padding: 2rem 2rem 4rem; + width: 100%; + } + + .back { + display: inline-block; + margin-bottom: 1.5rem; + font-size: 0.9rem; + color: #555; + text-decoration: none; + } + + .back:hover { + color: var(--accent); + } + + .heading { + font-size: 1.75rem; + font-weight: 400; + letter-spacing: -0.02em; + margin-bottom: 2.5rem; + } + + .empty { + color: #888; + font-size: 0.95rem; + } + + .list { + list-style: none; + padding: 0; + display: flex; + flex-direction: column; + } + + .item { + border-bottom: 1px solid #e0e0de; + } + + .item:first-child { + border-top: 1px solid #e0e0de; + } + + .item__link { + display: flex; + justify-content: space-between; + align-items: baseline; + padding: 0.75rem 0; + text-decoration: none; + color: inherit; + } + + .item__link:hover .item__title { + color: var(--accent); + } + + .item__title { + font-size: 1rem; + font-weight: 500; + transition: color 0.15s; + } + + .item__count { + font-size: 0.8rem; + color: #888; + } +</style> diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..abfade7 --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,108 @@ +@font-face { + font-family: "Exo 2"; + src: url("/fonts/Exo2-VariableFont_wght.ttf") format("truetype"); + font-weight: 100 900; + font-style: normal; + font-display: optional; +} + +@font-face { + font-family: "Exo 2"; + src: url("/fonts/Exo2-Italic-VariableFont_wght.ttf") format("truetype"); + font-weight: 100 900; + font-style: italic; + font-display: optional; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-VariableFont_wght.ttf") format("truetype"); + font-weight: 100 800; + font-style: normal; + font-display: optional; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/JetBrainsMono-Italic-VariableFont_wght.ttf") + format("truetype"); + font-weight: 100 800; + font-style: italic; + font-display: optional; +} + +:root { + --accent: #d41c47; + font-size: 16.8px; + + /* ── Layout widths ── */ + --width-wide: 1600px; /* photos / gallery */ + --width-shell: 1020px; /* nav, footer */ + --width-content: 1020px; /* page containers */ + --width-hero: 1020px; /* homepage hero */ + --width-prose: 680px; /* reading width */ + --width-sidebar: 250px; /* articles index sidebar */ + --width-sidebar-toc: 250px; /* article detail toc */ + + /* ── Type scale ── */ + --text-xl: clamp(1.75rem, 4.5vw, 2.5rem); + --text-lg: clamp(1.4rem, 3.5vw, 1.9rem); + --text-md: clamp(1.2rem, 2.5vw, 1.5rem); + --text-sm: clamp(1.05rem, 2vw, 1.2rem); + --text-xs: clamp(1rem, 1.5vw, 1.1rem); +} + +body { + font-family: "Exo 2", system-ui, sans-serif; + background: #f5f5f3; +} + +h1, +h2, +h3, +h4, +h5 { + line-height: 1.2; + text-wrap: balance; +} + +p, +li, +blockquote, +figcaption { + text-wrap: pretty; +} + +h1 { + font-size: var(--text-xl); +} +h2 { + font-size: var(--text-lg); +} +h3 { + font-size: var(--text-md); +} +h4 { + font-size: var(--text-sm); +} +h5 { + font-size: var(--text-xs); +} + +a[target="_blank"]:not(:has(svg))::after { + content: "↗"; + font-size: 0.7em; + vertical-align: super; + margin-left: 0.15em; + opacity: 0.6; + text-decoration: none; + display: inline-block; +} + +code, +pre, +kbd, +samp { + font-family: "JetBrains Mono", ui-monospace, monospace; + max-height: 40rem; +} diff --git a/src/styles/reset.css b/src/styles/reset.css new file mode 100644 index 0000000..628262d --- /dev/null +++ b/src/styles/reset.css @@ -0,0 +1,48 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +* { + margin: 0; +} + +html { + scrollbar-gutter: stable; +} + +body { + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +img, +picture, +video, +canvas, +svg { + display: block; + max-width: 100%; +} + +input, +button, +textarea, +select { + font: inherit; +} + +p, +h1, +h2, +h3, +h4, +h5, +h6 { + overflow-wrap: break-word; +} + +#root { + isolation: isolate; +} diff --git a/src/types/smartypants.d.ts b/src/types/smartypants.d.ts new file mode 100644 index 0000000..3078bd4 --- /dev/null +++ b/src/types/smartypants.d.ts @@ -0,0 +1,6 @@ +declare module "smartypants" { + export default function smartypants( + text: string, + behavior?: string | number, + ): string; +} diff --git a/stylelint.config.js b/stylelint.config.js new file mode 100644 index 0000000..8b2c200 --- /dev/null +++ b/stylelint.config.js @@ -0,0 +1,8 @@ +/** @type {import('stylelint').Config} */ +export default { + extends: ["stylelint-config-standard", "stylelint-config-astro"], + ignoreFiles: ["dist/**/*"], + rules: { + // Add your own rules here + }, +}; diff --git a/tasks/lessons.md b/tasks/lessons.md new file mode 100644 index 0000000..157adf2 --- /dev/null +++ b/tasks/lessons.md @@ -0,0 +1,14 @@ +# Lessons + +- Vor dem Abschluss immer den aktuellen Workspace-Stand erneut verifizieren, wenn der Nutzer parallel selbst Änderungen gemacht haben könnte; Zwischenstände nicht als finalen Restzustand formulieren. +- Absichtliche Schutzmechanismen wie Kontakt-Obfuskation nicht entfernen, nur weil eine formell sauberere Lösung möglich erscheint; erst die Nutzerintention prüfen und den Tradeoff zwischen Rechtssicherheit und Spam-Schutz explizit machen. +- Bei englischer Website-Copy keine abstrakten Übergangsformulierungen wie "bringing together" oder zu konzeptionelle Bildsprache verwenden, wenn der Nutzer natürliche, muttersprachliche Formulierungen will; lieber direkte, idiomatische Sätze mit klaren Substantiven und Verben schreiben. +- Wenn der Nutzer für Website-Copy einen konservativen Ton verlangt, Superlative, Selbstinszenierung und stilisierte Attribute weiter reduzieren; kurze, sachliche Selbstbeschreibung bevorzugen. +- Bei englischer Selbstbeschreibung im konservativen Ton `self-taught` gegenüber formelleren Wörtern wie `autodidact` bevorzugen, wenn Natürlichkeit wichtiger ist als begriffliche Präzision. +- Bei Selbstbeschreibungen keine Hobbys als primäre Rollen labeln, wenn der Nutzer sie nur freizeitlich betreibt; Hobbys explizit als interests, hobbies oder spare-time activities formulieren. +- Bei kurzer Hero-Copy immer auch den visuellen Umbruch prüfen: zu viele hervorgehobene Wörter, zu enge `max-width` oder unruhige Parallelkonstruktionen verschlechtern den Lesefluss selbst dann, wenn die Grammatik korrekt ist. +- Bei schlecht formatierten Markdown-Tabellen zuerst die fehlenden `prose`-Styles im Renderer prüfen und dort beheben; Content nicht mit `<br>`-Workarounds verbiegen, wenn das eigentliche Problem fehlendes Tabellen-CSS ist. +- Bei Mastodon-Automation keine unnötigen API-Calls mit zusätzlichen Scopes (z. B. `verify_credentials`) erzwingen; den minimalen Scope-Pfad für den eigentlichen Task bevorzugen, um Token-Fehler zu vermeiden. +- Bei Social-Webmentions nicht nur Quell-URL-Erreichbarkeit prüfen: Interaktions-URLs mit Hash-Signaturen (`favorited-by`, `liked_by`) brauchen eine API-basierte Validierung der konkreten Like/Repost-Relation, sonst bleiben gelöschte Reaktionen sichtbar. +- Bei Umbenennung-Tasks (z. B. articles→blog) immer ALLE Vorkommen prüfen: Code, Pfade, Kommentare und Label-Strings in Scripts — nicht nur die offensichtlichen getCollection/URL-Stellen. Danach grep-Verifikation ausführen. +- CLAUDE.md-Konventionen einhalten: Plan in `tasks/todo.md` schreiben (nicht nur TodoWrite-Tool), nach Nutzerkorrekturen `tasks/lessons.md` aktualisieren, TodoWrite-Tool nur ergänzend nutzen. diff --git a/tasks/todo.md b/tasks/todo.md new file mode 100644 index 0000000..c87e3c2 --- /dev/null +++ b/tasks/todo.md @@ -0,0 +1,140 @@ +# Todo + +- [x] Prüfen, wo `links` noch `data.date` statt `data.publishDate` verwendet +- [x] Link-Seiten, RSS und Archiv auf `publishDate` umstellen +- [x] Verifikation laufen lassen und Ergebnis dokumentieren + +## Current Task + +- [x] `src/pages/imprint.md` auf aktuelle Rechtsbezüge und schlanke englische Fassung umstellen +- [x] redundante Datenschutzpassage aus dem Impressum entfernen und auf die separate Datenschutzseite verweisen +- [x] Ergebnis per Diff prüfen und in diesem Dokument kurz reviewen +- [ ] Obfuskierte Kontaktdaten in `src/pages/privacy-policy.md` konsistent halten +- [ ] Privacy-Link im Impressum auf den neuen Slug `/privacy-policy` korrigieren +- [ ] Änderung per Diff und Astro-Check verifizieren +- [x] Obfuskierte Kontaktdaten in `src/pages/privacy-policy.md` konsistent halten +- [x] Privacy-Link im Impressum auf den neuen Slug `/privacy-policy` korrigieren +- [x] Änderung per Diff und Astro-Check verifizieren + +## Review + +- `src/pages/links/index.astro`, `src/pages/links/[...slug].astro`, `src/pages/rss.xml.ts`, `src/pages/rss/links.xml.ts`, `src/pages/tags/[slug].astro` und `src/pages/archives/index.astro` lesen `links` jetzt konsistent über `data.publishDate`. +- `rg` findet keine verbleibenden `links`-Zugriffe mehr über `data.date`. +- `pnpm astro check` läuft auf dem aktuellen Stand komplett sauber durch: 0 Errors, 0 Warnings, 0 Hints. +- `src/pages/imprint.md` verweist jetzt korrekt auf `Section 5 DDG` statt auf das alte `TMG`, enthält obfuskierte direkte Kontaktangaben und hält den `MStV`-Block separat. +- Die redundante Datenschutzpassage wurde aus dem Impressum entfernt; stattdessen verweist die Seite knapp auf `src/pages/privacy-policy.md`. +- Der Diff für `src/pages/imprint.md` ist inhaltlich sauber und `pnpm astro check` läuft nach der Änderung weiter ohne Fehler. +- `src/pages/privacy-policy.md` nutzt für die E-Mail jetzt dieselbe Obfuskationslogik mit `data-obf` und `data-obf-href` wie die übrigen Kontaktstellen. +- `src/pages/imprint.md` verweist auf den tatsächlichen Privacy-Slug `/privacy-policy`, sodass der Link zur umbenannten Datei wieder stimmt. + +## Current Task (2026-03-24) + +- [x] Intro-Text in `src/pages/index.astro` und Copy in `src/pages/about.astro` auf Konsistenz prüfen +- [x] Grammatik, Rechtschreibung und Formulierungen in idiomatischem Englisch glätten +- [x] Änderungen per Diff prüfen und Review hier dokumentieren + +## Review (2026-03-24) + +- `src/pages/index.astro` klingt jetzt weniger generisch und passt sprachlich besser zur persönlicheren Stimme der About-Seite. +- `src/pages/about.astro` nutzt jetzt idiomatischeres Englisch, glattere Satzrhythmen und konsistente Wortwahl rund um web, photography und curiosity. +- Der Diff für beide Dateien ist inhaltlich eng begrenzt auf Copy-Änderungen. +- `pnpm astro check` läuft nach den Anpassungen sauber durch: 0 Errors, 0 Warnings, 0 Hints. + +## Current Task (2026-03-24, Article Tables) + +- [x] Tabellenlayout in `src/pages/articles/[...slug].astro` für Markdown-Content ergänzen +- [x] Tabellen-Workaround im Artikelcontent auf semantisches Markdown zurückführen +- [x] Diff und `pnpm astro check` prüfen und Review hier dokumentieren + +## Review (2026-03-24, Article Tables) + +- `src/pages/articles/[...slug].astro` deklariert jetzt ein eigenes Tabellenlayout für Markdown in `.prose`, inklusive Abständen, Linien, Zell-Padding und kontrolliertem Umbruch. +- Der vorherige `<br>`-Workaround in `src/content/articles/2026/03/23/obfuscating-contact-data-on-a-static-site.md` wurde wieder entfernt; der Artikel bleibt semantisch sauberes Markdown. +- Der Diff ist eng begrenzt auf die fehlenden Tabellen-Styles und die Projektdokumentation. +- `pnpm astro check` läuft nach der Änderung sauber durch: 0 Errors, 0 Warnings, 0 Hints. + +## Current Task (2026-03-24, Syndication Links) + +- [x] Inkonsistenz der `syndication`-Links in `src/pages/articles/[...slug].astro` und `src/pages/notes/[...slug].astro` eingrenzen +- [x] Notes-Seite auf konsistente `syndication`-Darstellung und korrekten Datentyp umstellen +- [x] Diff und `pnpm astro check` prüfen und Review hier dokumentieren + +## Review (2026-03-24, Syndication Links) + +- `src/components/SyndicationLinks.astro` bündelt Rendering, Plattformnamen und Chip-Styling der `syndication`-Links jetzt an einer Stelle, sodass Artikel und Notizen dieselbe Darstellung verwenden. +- `src/pages/notes/[...slug].astro` normalisiert den optionalen einzelnen `syndication`-URL-Wert korrekt zu einem Array, statt einen String zu spreaden. +- `src/pages/articles/[...slug].astro` verwendet dieselbe Komponente wie die Notes-Seite; der Diff bleibt dadurch eng begrenzt und reduziert künftige Drift zwischen beiden Templates. +- `pnpm astro check` läuft nach der Änderung sauber durch: 0 Errors, 0 Warnings, 0 Hints. + +## Current Task (2026-03-24, Mastodon Syndication) + +- [x] `scripts/mastodon-syndicate.mjs` für RSS-basiertes Autoposting von Artikeln und Notes mit State-Datei implementieren +- [x] `scripts/publish-posts.sh` um Mastodon-Env, Script-Aufruf, State-Commit und Remote-Clean-Exclude erweitern +- [x] Artikel- und Notes-Seiten um `.mastodon-posted.json` für automatische Syndication-Links ergänzen +- [x] Änderungen per Diff, `node --check` und `pnpm astro check` verifizieren + +## Review (2026-03-24, Mastodon Syndication) + +- `scripts/mastodon-syndicate.mjs` postet neue RSS-Einträge aus Articles und Notes nach Mastodon, baut den Status als Titel + Teaser + Link, lädt OG-Bilder als Media hoch und persistiert erfolgreiche Posts in `.mastodon-posted.json`. +- `scripts/publish-posts.sh` behandelt `.mastodon-posted.json` jetzt wie `.bluesky-posted.json` (Clean-Exclude + Commit/Push bei Änderungen) und führt Mastodon-Syndication nur mit gesetzten `MASTODON_BASE_URL` und `MASTODON_ACCESS_TOKEN` aus. +- `src/pages/articles/[...slug].astro` und `src/pages/notes/[...slug].astro` lesen zusätzlich `.mastodon-posted.json`, sodass Mastodon-Status-URLs in `SyndicationLinks` erscheinen. +- `node --check scripts/mastodon-syndicate.mjs` und `pnpm astro check` laufen nach den Änderungen sauber durch. + +## Current Task (2026-03-24, Mastodon Hardening) + +- [x] Media-Upload in `scripts/mastodon-syndicate.mjs` um Retry/Backoff bei HTTP 429 ergänzen +- [x] Optionale `MASTODON_DRY_RUN`- und `MASTODON_LIMIT`-Steuerung im Script ergänzen +- [x] Optionale Mastodon-Flags in `scripts/publish-posts.sh` durchreichen +- [x] Änderungen mit `node --check`, `pnpm biome check` und `pnpm astro check` verifizieren + +## Review (2026-03-24, Mastodon Hardening) + +- `scripts/mastodon-syndicate.mjs` retried Media-Uploads jetzt bis zu 5-mal bei 429 mit `Retry-After`-Support und exponentiellem Backoff als Fallback. +- Das Script unterstützt jetzt `MASTODON_DRY_RUN` (simuliert Posts ohne Veröffentlichung) und `MASTODON_LIMIT` (begrenzt die Anzahl verarbeiteter Items). +- `scripts/publish-posts.sh` liest und übergibt die optionalen Variablen `MASTODON_DRY_RUN` und `MASTODON_LIMIT` an das Mastodon-Script. +- `node --check scripts/mastodon-syndicate.mjs`, `pnpm biome check scripts/mastodon-syndicate.mjs scripts/publish-posts.sh` und `pnpm astro check` laufen nach den Änderungen erfolgreich. + +## Current Task (2026-03-24, Webmention Re-Validation) + +- [x] Serverseitige Webmention-Fetch/Filter-Lib für Bluesky-/Mastodon-Quellen ergänzen +- [x] API-Endpoint `api/webmentions.json` für gefilterte Mentions bereitstellen +- [x] `WebMentions.astro` auf gefilterte Datenquelle für SSR und Client-Refresh umstellen +- [x] Änderungen mit `pnpm biome check` und `pnpm astro check` verifizieren + +## Review (2026-03-24, Webmention Re-Validation) + +- `src/lib/webmentions.ts` filtert eingehende Mentions jetzt serverseitig und prüft Social-Reaktionen auf Interaktionsebene: + - Mastodon `#favorited-by-...`/`#reblogged-by-...` wird über die jeweiligen Status-Listen (`favourited_by`/`reblogged_by`) validiert. + - Bluesky `#liked_by_did:...`/`#reposted_by_did:...` wird über die Public-API (`getLikes`/`getRepostedBy`) validiert. + - Fallback bleibt eine Existenzprüfung der Quell-URL (404/410/Tombstone), wenn keine Interaktionssignatur erkannt wird. +- `src/pages/api/webmentions.json.ts` liefert gefilterte Mentions als JSON und wird nicht prerendered, damit die Prüfung live auf dem Server stattfindet. +- `src/components/WebMentions.astro` nutzt die Filter-Lib für das initiale Rendering und ruft clientseitig statt `webmention.io` jetzt den lokalen Endpoint auf, damit beide Pfade konsistent sind. +- `pnpm biome check src/lib/webmentions.ts src/pages/api/webmentions.json.ts src/components/WebMentions.astro` und `pnpm astro check` laufen nach der Änderung sauber durch. + +## Current Task (2026-03-24, Mastodon Articles) + +- [x] Artikel zur Mastodon-Implementierung in `src/content/articles` erstellen +- [x] Artikel zum Troubleshooting von Backfeed/Stale Webmentions in `src/content/articles` erstellen +- [x] Frontmatter und Content-Struktur gegen bestehende Artikelkonventionen verifizieren + +## Review (2026-03-24, Mastodon Articles) + +- Neuer Implementierungsartikel: `src/content/articles/2026/03/24/posse-to-mastodon-with-state-and-media.md` +- Neuer Troubleshooting-Artikel: `src/content/articles/2026/03/24/troubleshooting-mastodon-backfeed-and-stale-webmentions.md` +- Beide Artikel verwenden konsistentes Frontmatter (`title`, `description`, `publishDate`, `tags`) und orientieren sich in Struktur, Ton und technischer Tiefe an den bestehenden Engineering-Posts. + +## Current Task (2026-03-24, Remove Bluesky) + +- [x] Bluesky-Syndication in `scripts/publish-posts.sh` und `scripts/bluesky-syndicate.mjs` vollständig entfernen +- [x] Bluesky-State (`.bluesky-posted.json`) und Seiten-Imports in Artikel-/Notes-Templates entfernen +- [x] Bluesky-Links in `src/components/Footer.astro`, `src/layouts/BaseLayout.astro` und Label-Mapping bereinigen +- [x] Webmention-Filter in `src/lib/webmentions.ts` von Bluesky-Pfaden auf Mastodon-only zurückbauen +- [x] Änderungen per Diff, `pnpm biome check` und `pnpm astro check` verifizieren und Review dokumentieren + +## Review (2026-03-24, Remove Bluesky) + +- `scripts/publish-posts.sh` enthält keinen Bluesky-Block mehr und nutzt im Remote-Clean nur noch die Mastodon-State-Ausnahme; `scripts/bluesky-syndicate.mjs` wurde entfernt. +- `.bluesky-posted.json` wurde entfernt und die Artikel-/Notes-Templates lesen nur noch `.mastodon-posted.json` für automatische Syndication-Links. +- Bluesky-Profile/Verweise wurden aus `src/components/Footer.astro`, `src/layouts/BaseLayout.astro` und dem Host-Label-Mapping in `src/components/SyndicationLinks.astro` entfernt. +- `src/lib/webmentions.ts` prüft Social-Interaktionen jetzt Mastodon-only; alle Bluesky-spezifischen Resolver/Checks sind entfernt. +- Verifikation: `pnpm biome check scripts/publish-posts.sh src/components/Footer.astro src/components/SyndicationLinks.astro src/layouts/BaseLayout.astro src/lib/webmentions.ts src/pages/articles/[...slug].astro src/pages/notes/[...slug].astro` und `pnpm astro check` laufen erfolgreich (0 Errors, 0 Warnings, 0 Hints). diff --git a/templates/obsidian/new-note-cover.md b/templates/obsidian/new-note-cover.md new file mode 100644 index 0000000..9e14387 --- /dev/null +++ b/templates/obsidian/new-note-cover.md @@ -0,0 +1,18 @@ +<%* +const title = await tp.system.prompt("Note title"); +if (!title) { return; } +const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, '').trim().replace(/\s+/g, '-'); +const publishDate = tp.date.now("YYYY-MM-DD"); +await tp.file.move(`content/notes/${tp.date.now("YYYY/MM/DD")}/${slug}`); +tR += `--- +title: "${title}" +publishDate: ${publishDate} +description: "" +cover: "./${slug}.jpg" +coverAlt: "" +tags: [] +draft: false +syndication: +--- +`; +%> diff --git a/templates/obsidian/new-note.md b/templates/obsidian/new-note.md new file mode 100644 index 0000000..52f4596 --- /dev/null +++ b/templates/obsidian/new-note.md @@ -0,0 +1,16 @@ +<%* +const title = await tp.system.prompt("Note title"); +if (!title) { return; } +const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, '').trim().replace(/\s+/g, '-'); +const publishDate = tp.date.now("YYYY-MM-DD"); +await tp.file.move(`content/notes/${tp.date.now("YYYY/MM/DD")}/${slug}`); +tR += `--- +title: "${title}" +publishDate: ${publishDate} +description: "" +tags: [] +draft: false +syndication: +--- +`; +%> diff --git a/templates/obsidian/new-post.md b/templates/obsidian/new-post.md new file mode 100644 index 0000000..80aeb2e --- /dev/null +++ b/templates/obsidian/new-post.md @@ -0,0 +1,17 @@ +<%* +const title = await tp.system.prompt("Post title"); +if (!title) { return; } +const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, '').trim().replace(/\s+/g, '-'); +const publishDate = tp.date.now("YYYY-MM-DDTHH:mm:ss+01:00"); +await tp.file.move(`content/blog/posts/${tp.date.now("YYYY/MM/DD")}/${slug}`); +tR += `--- +title: "${title}" +description: "" +publishDate: ${publishDate} +tags: [] +category: general +draft: true +syndication: +--- +`; +%> diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b4b0e69 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "astro/tsconfigs/strictest", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +}