Compare commits
No commits in common. "ef9ed650b335d5268ad07083500cf2e84c23cc05" and "017bf6e0673b0648e714439c919b163d6214fd7e" have entirely different histories.
ef9ed650b3
...
017bf6e067
12 changed files with 439 additions and 382 deletions
6
.github/workflows/codeberg_mirror.yml
vendored
6
.github/workflows/codeberg_mirror.yml
vendored
|
|
@ -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
2
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
|
@ -5,4 +5,5 @@
|
||||||
"source.fixAll": "explicit"
|
"source.fixAll": "explicit"
|
||||||
},
|
},
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
|
"chat.disableAIFeatures": true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
67
CLAUDE.md
67
CLAUDE.md
|
|
@ -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
11
LICENSE
|
|
@ -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.
|
||||||
|
|
|
||||||
14
package.json
14
package.json
|
|
@ -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
673
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -13,9 +13,6 @@ 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>
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue