Compare commits

..

No commits in common. "ef9ed650b335d5268ad07083500cf2e84c23cc05" and "017bf6e0673b0648e714439c919b163d6214fd7e" have entirely different histories.

12 changed files with 439 additions and 382 deletions

View file

@ -10,7 +10,7 @@ jobs:
codeberg: codeberg:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@ -31,11 +31,11 @@ jobs:
- name: Verify SSH access - name: Verify SSH access
run: | run: |
ssh -T git@codeberg.org || true ssh -T git@codeberg.org || true
git ls-remote git@codeberg.org:adrian-altner/adrian-altner.com.git > /dev/null git ls-remote git@codeberg.org:adrian-altner/www.adrian-altner.com.git > /dev/null
- name: Mirror to Codeberg - name: Mirror to Codeberg
run: | run: |
git remote add mirror git@codeberg.org:adrian-altner/adrian-altner.com.git 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/*). # Remove previously mirrored remote-tracking refs (e.g. refs/remotes/origin/*).
while IFS= read -r ref; do while IFS= read -r ref; do
git push mirror ":${ref}" git push mirror ":${ref}"

2
.gitignore vendored
View file

@ -14,8 +14,6 @@ pnpm-debug.log*
# local content hotfolder # local content hotfolder
src/content/* src/content/*
!src/content/links/
!src/content/links/links.json
# environment variables # environment variables
.env .env

View file

@ -5,4 +5,5 @@
"source.fixAll": "explicit" "source.fixAll": "explicit"
}, },
"editor.defaultFormatter": "biomejs.biome", "editor.defaultFormatter": "biomejs.biome",
"chat.disableAIFeatures": true
} }

View file

@ -2,46 +2,36 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Prerequisites
Node.js >= 22. Package manager is pnpm (enforced via `preinstall` script). Husky pre-commit hook runs `pnpm check`.
## Commands ## Commands
```bash ```bash
pnpm dev # Start dev server (localhost:4321) npm run dev # Start dev server (localhost:4321)
pnpm build # astro check + build + copy-sw (use this to verify changes) npm run build # astro check + build + copy-sw (use this to verify changes)
pnpm build:production # Production build (skips astro check, uses --mode production) npm run check # Type check + Biome lint
pnpm check # Type check + Biome lint npm run check:fix # Type check + Biome lint with auto-fix
pnpm check:fix # Type check + Biome lint with auto-fix npm run stylelint # Lint CSS/Astro styles
pnpm stylelint # Lint CSS/Astro styles npm run stylelint:fix # Fix style issues
pnpm stylelint:fix # Fix style issues
pnpm generate:icons # Regenerate PWA icon assets
``` ```
There are no automated tests. Verification is done via `pnpm build` (0 errors required) and the preview MCP tools. There are no automated tests. Verification is done via `npm run build` (0 errors required) and the preview MCP tools.
## Architecture ## Architecture
Astro 6 site running in SSR mode (Node.js standalone adapter) with static output for most routes. Pure Astro components — no React/Vue/Svelte. TypeScript strict mode (`astro/tsconfigs/strictest`). Path alias `@/*``src/*`. 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. Biome config disables some rules for `.astro` files (`useConst`, `useImportType`, `noUnusedVariables`, `noUnusedImports`). **Formatter/linter:** Biome (not ESLint/Prettier). Run `check:fix` after larger edits.
**Markdown plugins:** `remarkObsidianLinks` (custom, `src/lib/remark-obsidian-links.mjs`) for `[[wiki-link]]` syntax, `rehypeExternalLinks` for `target="_blank"` on external links.
### Content Collections (`src/content.config.ts`) ### Content Collections (`src/content.config.ts`)
Seven collections defined with Zod schemas: Five collections defined with Zod schemas:
| Collection | Loader | Path | Notes | | Collection | Path | Notes |
|---|---|---|---| |---|---|---|
| `blog` | glob | `src/content/blog/posts/` | Series support (`seriesParent`, `seriesOrder`), tags, category ref, syndication URLs | | `blog` | `src/content/blog/` | Posts with series support (`seriesParent`, `seriesOrder`), tags, category ref, syndication URLs |
| `categories` | glob | `src/content/blog/categories/` | Referenced by blog posts | | `categories` | `src/content/categories/` | Referenced by blog posts |
| `notes` | glob | `src/content/notes/` | Short-form with optional cover image | | `notes` | `src/content/notes/` | Short-form with optional cover image |
| `links_json` | file | `src/content/links/links.json` | JSON file with auto-generated IDs (`json/date/slug`) | | `links` | `src/content/links/` | Curated external links |
| `projects` | glob | `src/content/projects/project/` | Portfolio items with optional URL/GitHub links | | `collections_photos` | `src/content/photos/collections/` | Photo collections; photos stored as JPG + JSON sidecar files in `img/` subdirs |
| `projects_categories` | glob | `src/content/projects/categories/` | Referenced by projects |
| `collections_photos` | glob | `src/content/photos/collections/` | Matches `**/index.{md,mdx}`; photos as JPG + JSON sidecar files in `img/` subdirs |
### Key Routing Patterns ### Key Routing Patterns
@ -49,35 +39,22 @@ Seven collections defined with Zod schemas:
- `/og/blog/[...slug].png` — OG images generated server-side via Satori (`src/lib/og.ts`) - `/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 - `/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 - `/photos/collections/[...slug]` — Nested photo collections with breadcrumb navigation
- `/projects/[...slug]`, `/projects/category/[...slug]` — Project portfolio
- `/tags/[slug]` — Cross-content-type tag pages
- `/archives/` — Timeline of all content
- `/api/webmentions.json` — Webmention data endpoint
### Lib Utilities (`src/lib/`) ### Lib Utilities (`src/lib/`)
- `collections.ts` — Photo collection helpers: `collectionSlug()`, `buildCollectionPhotos()`, `buildBreadcrumbs()`, `getChildCollections()` - `collections.ts` — Photo collection helpers: `collectionSlug()`, `buildCollectionPhotos()`, `buildBreadcrumbs()`, `getChildCollections()`
- `og.ts` — OG image generation using Satori (`buildArticleVNode`, `renderOgImage`) - `og.ts` — OG image generation using Satori (`buildArticleVNode`, `renderOgImage`)
- `webmentions.ts` — Fetch and filter webmentions from webmention.io (includes Mastodon/Bluesky validation) - `webmentions.ts` — Fetch and filter webmentions from webmention.io
- `photo-albums.ts` — Photo album organisation utilities - `photo-albums.ts` — Photo album organisation utilities
- `links.ts` — Link collection helpers
- `remark-obsidian-links.mjs` — Custom remark plugin for Obsidian-style `[[links]]`
### Scripts (`scripts/`) ### Scripts (`scripts/`)
- `mastodon-syndicate.mjs` — POSSE: scans blog/notes for posts without `syndication` field, posts to Mastodon, writes status URL back to frontmatter. Env vars: `MASTODON_BASE_URL`, `MASTODON_ACCESS_TOKEN`, `MASTODON_VISIBILITY`, `MASTODON_DRY_RUN`, `MASTODON_LIMIT` - `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`
- `vision.ts` — Uses Claude Vision API to auto-generate EXIF sidecars + metadata for photo collections. Requires `ANTHROPIC_API_KEY` - `publish-posts.sh` — Full deploy orchestration: rsync content to VPS → rebuild container → send webmentions → run mastodon-syndicate
- `publish-all.sh` — Full deploy orchestration: rsync content to VPS → rebuild container → send webmentions → run mastodon-syndicate. Per-collection variants: `publish-blog.sh`, `publish-notes.sh`, `publish-links.sh`, `publish-photos.sh`, `publish-projects.sh`
- `new-post.sh`, `new-note.sh` — Content scaffolding templates
- `copy-sw.js` — Post-build service worker copy
### IndieWeb / Syndication ### IndieWeb / Syndication
Blog posts support POSSE via `mastodon-syndicate.mjs`. After posting, the Mastodon status URL is written to frontmatter as `syndication: ["https://..."]`. The `SyndicationLinks` component renders `u-syndication` microformat links. Webmentions are fetched at build time from webmention.io and displayed via `WebMentions.astro`. 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`.
### Deployment
Multi-stage container build (`Containerfile`): Node 22 with pnpm for build, Node 22 slim for runtime. Runs `node dist/server/entry.mjs` on port 4321. Orchestrated via `compose.yml`. Deploy scripts rsync content to VPS and rebuild the container.
--- ---
@ -101,7 +78,7 @@ Multi-stage container build (`Containerfile`): Node 22 with pnpm for build, Node
### 4. Verification Before Done ### 4. Verification Before Done
- Never mark a task complete without proving it works - Never mark a task complete without proving it works
- Run `pnpm build` — must complete with 0 errors - Run `npm run build` — must complete with 0 errors
- Use preview MCP tools to visually verify UI changes - Use preview MCP tools to visually verify UI changes
### 5. Demand Elegance (Balanced) ### 5. Demand Elegance (Balanced)

11
LICENSE
View file

@ -19,3 +19,14 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. 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.

View file

@ -44,13 +44,13 @@
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.80.0", "@anthropic-ai/sdk": "^0.80.0",
"@astrojs/check": "^0.9.8", "@astrojs/check": "^0.9.7",
"@astrojs/mdx": "^5.0.3", "@astrojs/mdx": "^5.0.2",
"@astrojs/node": "^10.0.4", "@astrojs/node": "^10.0.2",
"@astrojs/rss": "^4.0.18", "@astrojs/rss": "^4.0.17",
"@astrojs/sitemap": "^3.7.2", "@astrojs/sitemap": "^3.7.1",
"@fontsource/exo-2": "^5.2.8", "@fontsource/exo-2": "^5.2.8",
"astro": "^6.1.1", "astro": "^6.0.4",
"astro-embed": "^0.12.0", "astro-embed": "^0.12.0",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"consola": "^3.4.2", "consola": "^3.4.2",
@ -63,7 +63,7 @@
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.4.9", "@biomejs/biome": "^2.4.7",
"@types/justified-layout": "^4.1.4", "@types/justified-layout": "^4.1.4",
"@types/leaflet": "^1.9.21", "@types/leaflet": "^1.9.21",
"@types/leaflet.markercluster": "^1.5.6", "@types/leaflet.markercluster": "^1.5.6",

673
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@ const { dark } = Astro.props;
<footer class={`footer${dark ? " footer--dark" : ""}`}> <footer class={`footer${dark ? " footer--dark" : ""}`}>
<div class="footer__inner"> <div class="footer__inner">
<p class="footer__license"> <p class="footer__license">
© 2026 Adrian Altner © 2026 Adrian Altner · <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener noreferrer">CC BY-SA 4.0</a> · <a href="/imprint">Imprint</a> · <a href="/privacy-policy">Privacy Policy</a> · <a href="/contact">Contact</a>
</p> </p>
<div class="footer__icons"> <div class="footer__icons">
<a href="https://mastodon.social/@altner" aria-label="Mastodon" target="_blank" rel="me noopener noreferrer"> <a href="https://mastodon.social/@altner" aria-label="Mastodon" target="_blank" rel="me noopener noreferrer">

View file

@ -1,19 +0,0 @@
[
{
"title": "icomoon.io",
"url": "https://icomoon.io/",
"publishDate": "2026-03-27",
"description": "Many free icons",
"tags": ["ICONS", "SVG"],
"via": "https://dev.to/",
"collection": "Design"
},
{
"title": "13 CSS Blog Cards",
"url": "https://dev.to/frontendsolutions/13-css-blog-cards-54d7",
"publishDate": "2026-03-27",
"description": "not bad as inspiration",
"tags": ["css", "html", "frontend"],
"collection": "Development"
}
]

View file

@ -13,10 +13,7 @@ import BaseLayout from "@/layouts/BaseLayout.astro";
<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> <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 /> <hr />
<div class="note">
<strong>Please note:</strong> Unsolicited press releases, advertising, and SEO pitches will be deleted without a response.
</div>
<h2>How to reach me</h2> <h2>How to reach me</h2>
<div class="cards"> <div class="cards">
@ -29,7 +26,7 @@ import BaseLayout from "@/layouts/BaseLayout.astro";
</div> </div>
<div class="card__body"> <div class="card__body">
<p class="card__label">Email</p> <p class="card__label">Email</p>
<span class="card__value r" data-obf="bW9je3RvZH1yZW50bGEtbmFpcmRhe3RhfXllaA=="></span> <a class="card__value r" data-obf="bW9je3RvZH1yZW50bGEtbmFpcmRhe3RhfXllaA==" data-obf-href="bWFpbHRvOmhleUBhZHJpYW4tYWx0bmVyLmNvbQ=="></a>
</div> </div>
</div> </div>
@ -45,6 +42,12 @@ import BaseLayout from "@/layouts/BaseLayout.astro";
</div> </div>
</div> </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> </div>
</main> </main>
</div> </div>
@ -176,7 +179,8 @@ import BaseLayout from "@/layouts/BaseLayout.astro";
.card__value { .card__value {
font-size: clamp(0.8rem, 3.5vw, 1rem); font-size: clamp(0.8rem, 3.5vw, 1rem);
color: #333; color: var(--accent);
text-underline-offset: 3px;
margin: 0; margin: 0;
white-space: nowrap; white-space: nowrap;
} }

View file

@ -13,8 +13,8 @@ description: Legal notice for adrian-altner.com.
<span class="r" data-obf="bmVkc2VyRCA3OTAxMA=="></span><br> <span class="r" data-obf="bmVkc2VyRCA3OTAxMA=="></span><br>
<span class="r" data-obf="eW5hbXJlRw=="></span> <span class="r" data-obf="eW5hbXJlRw=="></span>
**Phone:** <span class="r" data-obf="MDI0MDM1ODcgNjUxIDk0Kw=="></span><br> **Phone:** <a class="r" data-obf="MDI0MDM1ODcgNjUxIDk0Kw==" data-obf-href="dGVsOis0OTE1Njc4NTMwNDIw"></a>
**Email:** <span class="r" data-obf="bW9je3RvZH1yZW50bGEtbmFpcmRhe3RhfXllaA=="></span> **Email:** <a class="r" data-obf="bW9je3RvZH1yZW50bGEtbmFpcmRhe3RhfXllaA==" data-obf-href="bWFpbHRvOmhleUBhZHJpYW4tYWx0bmVyLmNvbQ=="></a>
--- ---
@ -27,9 +27,9 @@ description: Legal notice for adrian-altner.com.
--- ---
## License ## Content License
Unless otherwise noted, content published on this website is licensed under the [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/) license. The source code is licensed under the [MIT License](https://mit-license.org/). 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.
--- ---

View file

@ -15,7 +15,7 @@ description: Privacy policy and data protection information for adrian-altner.co
The controller responsible for data processing on this website within the meaning of the General Data Protection Regulation (GDPR) is: 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> <strong><span class="r" data-obf="cmVudGxBIG5haXJkQQ=="></span></strong><br>
**Email:** <span class="r" data-obf="bW9je3RvZH1yZW50bGEtbmFpcmRhe3RhfXllaA=="></span> **Email:** <a class="r" data-obf="bW9je3RvZH1yZW50bGEtbmFpcmRhe3RhfXllaA==" data-obf-href="bWFpbHRvOmhleUBhZHJpYW4tYWx0bmVyLmNvbQ=="></a>
--- ---