perf(assets): convert images to webp and check asset paths

Convert 26 PNG/JPG assets to WebP: max 1600 px, q80, diagrams lossless.
src/assets shrinks from 8.3 MB to 4.2 MB.
Use me.jpg for og:image and twitter:image; some crawlers skip WebP.
Add scripts/check-assets.mjs to lint, so a wrong asset path fails lint,
not only at runtime.
This commit is contained in:
lobo committed 2026-10-13 09:13:58 +02:00
1 parent 4e59770e53
commit 2abc7c31e2
62 files changed
+74 -33

No files matched your search

+1
View File
@@ -46,6 +46,7 @@ Dockerfile text eol=lf
*.jpeg binary *.jpeg binary
*.gif binary *.gif binary
*.ico binary *.ico binary
*.webp binary
*.pdf binary *.pdf binary
*.zip binary *.zip binary
*.tar binary *.tar binary
+2 -1
View File
@@ -90,6 +90,7 @@ Then open http://localhost:8080
| `npm test` | Unit tests (Vitest + jsdom, no browser needed) | | `npm test` | Unit tests (Vitest + jsdom, no browser needed) |
| `npm run lint` | ESLint over `src/**/*.ts` and `src/**/*.html` | | `npm run lint` | ESLint over `src/**/*.ts` and `src/**/*.html` |
| `npm run i18n:check` | Verifies `en.json` and `de.json` share the exact same keys | | `npm run i18n:check` | Verifies `en.json` and `de.json` share the exact same keys |
| `npm run assets:check` | Verifies every asset path referenced in `src/` exists (also runs in `npm run lint`) |
--- ---
@@ -102,7 +103,7 @@ Triggered on every push to `main` and on pull requests targeting `main`.
**Job 1 — `quality-check`** (runs on both pushes and PRs): **Job 1 — `quality-check`** (runs on both pushes and PRs):
1. Lint & type check — `npm run lint` 1. Lint & type check — `npm run lint` (includes `npm run assets:check`)
2. i18n key sync — `npm run i18n:check` 2. i18n key sync — `npm run i18n:check`
3. Unit tests — `npm test` 3. Unit tests — `npm test`
4. Production build — `npx ng build --configuration production` 4. Production build — `npx ng build --configuration production`
+3 -2
View File
@@ -7,8 +7,9 @@
"build": "ng build", "build": "ng build",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"test": "ng test --watch=false", "test": "ng test --watch=false",
"lint": "ng lint", "lint": "ng lint && npm run assets:check",
"i18n:check": "node scripts/check-i18n.mjs" "i18n:check": "node scripts/check-i18n.mjs",
"assets:check": "node scripts/check-assets.mjs"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env node
// Verifies that every asset path in AssetsConstants.ts and index.html exists.
// A wrong path builds fine and only 404s at runtime.
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { join, dirname, extname } from 'node:path';
import { fileURLToPath } from 'node:url';
const srcDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'src');
const EXTENSIONS = new Set(['.ts', '.html', '.scss', '.json']);
function* walk(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
yield* walk(path);
} else if (EXTENSIONS.has(extname(path)) && !path.endsWith('.spec.ts')) {
yield path;
}
}
}
const missing = [];
for (const file of walk(srcDir)) {
const content = readFileSync(file, 'utf8');
for (const [, path] of content.matchAll(/\b(assets\/[\w./-]+)/g)) {
if (!existsSync(join(srcDir, path))) {
missing.push(`${file}: ${path}`);
}
}
}
if (missing.length > 0) {
console.error(`Missing assets:\n${missing.join('\n')}`);
process.exit(1);
}
console.log('All referenced assets exist.');
+26 -26
View File
@@ -1,44 +1,44 @@
export class AssetsConstants { export class AssetsConstants {
static readonly ME = '/assets/me.webp'; static readonly ME = '/assets/me.webp';
static readonly LOGO = '/assets/logos/logo-wolf.png'; static readonly LOGO = '/assets/logos/logo-wolf.webp';
static readonly FLAG_DE = '/assets/flags/de.svg'; static readonly FLAG_DE = '/assets/flags/de.svg';
static readonly FLAG_EN = '/assets/flags/gb.svg'; static readonly FLAG_EN = '/assets/flags/gb.svg';
//logos //logos
static readonly CHAMAELEON_LOGO = '/assets/logos/logo_chamaeleon.svg'; static readonly CHAMAELEON_LOGO = '/assets/logos/logo_chamaeleon.svg';
static readonly TH_BINGEN_LOGO = '/assets/logos/bingen-logo-white.svg'; static readonly TH_BINGEN_LOGO = '/assets/logos/bingen-logo-white.svg';
static readonly ASSYST_LOGO = '/assets/logos/assyst_gmbh_logo.jpg'; static readonly ASSYST_LOGO = '/assets/logos/assyst_gmbh_logo.webp';
static readonly COLORDIGITAL_LOGO = '/assets/logos/dmixcloud_logo.jpg'; static readonly COLORDIGITAL_LOGO = '/assets/logos/dmixcloud_logo.webp';
static readonly TERAPORT_LOGO = '/assets/logos/teraport_gmbh_logo.jpg'; static readonly TERAPORT_LOGO = '/assets/logos/teraport_gmbh_logo.webp';
static readonly DIPLOMA = '/assets/projects/diploma/Dahm2010-Diplomarbeit.pdf'; static readonly DIPLOMA = '/assets/projects/diploma/Dahm2010-Diplomarbeit.pdf';
//project images //project images
static readonly PLAYGROUND_IMAGES = [ static readonly PLAYGROUND_IMAGES = [
'/assets/projects/playground/1.png' '/assets/projects/playground/1.webp'
]; ];
static readonly EL_MUCHO_IMAGES = [ static readonly EL_MUCHO_IMAGES = [
'/assets/projects/el-mucho/1.jpg', '/assets/projects/el-mucho/1.webp',
'/assets/projects/el-mucho/2.jpg', '/assets/projects/el-mucho/2.webp',
'/assets/projects/el-mucho/3.jpg', '/assets/projects/el-mucho/3.webp',
'/assets/projects/el-mucho/4.jpg' '/assets/projects/el-mucho/4.webp'
]; ];
static readonly GAME_JAMS_IMAGES = [ static readonly GAME_JAMS_IMAGES = [
'/assets/projects/game-jams/1.png', '/assets/projects/game-jams/1.webp',
'/assets/projects/game-jams/2.png', '/assets/projects/game-jams/2.webp',
'/assets/projects/game-jams/3.png' '/assets/projects/game-jams/3.webp'
]; ];
static readonly DIPLOMA_IMAGES = [ static readonly DIPLOMA_IMAGES = [
'/assets/projects/diploma/1.jpg', '/assets/projects/diploma/1.webp',
'/assets/projects/diploma/2.jpg', '/assets/projects/diploma/2.webp',
'/assets/projects/diploma/3.jpg', '/assets/projects/diploma/3.webp',
'/assets/projects/diploma/4.jpg', '/assets/projects/diploma/4.webp',
'/assets/projects/diploma/5.jpg', '/assets/projects/diploma/5.webp',
'/assets/projects/diploma/6.jpg' '/assets/projects/diploma/6.webp'
]; ];
static readonly CLAUDE_BOX_IMAGES = [ static readonly CLAUDE_BOX_IMAGES = [
@@ -46,16 +46,16 @@ export class AssetsConstants {
]; ];
static readonly TRIBBLE_IMAGES = [ static readonly TRIBBLE_IMAGES = [
'/assets/projects/tribble-the-homeserver/1.png', '/assets/projects/tribble-the-homeserver/1.webp',
'/assets/projects/tribble-the-homeserver/2.png', '/assets/projects/tribble-the-homeserver/2.webp',
'/assets/projects/tribble-the-homeserver/3.png', '/assets/projects/tribble-the-homeserver/3.webp',
'/assets/projects/tribble-the-homeserver/4.png' '/assets/projects/tribble-the-homeserver/4.webp'
]; ];
static readonly TBP_IMAGES = [ static readonly TBP_IMAGES = [
'/assets/projects/to-be-played/1.png', '/assets/projects/to-be-played/1.webp',
'/assets/projects/to-be-played/2.png', '/assets/projects/to-be-played/2.webp',
'/assets/projects/to-be-played/3.png', '/assets/projects/to-be-played/3.webp',
'/assets/projects/to-be-played/4.png' '/assets/projects/to-be-played/4.webp'
]; ];
} }
+1 -1
View File
@@ -1,6 +1,6 @@
<mat-toolbar class="flex! items-center! p-[clamp(0.5rem,1vw,1rem)]! backdrop-blur-sm! backdrop-saturate-[1.1]! bg-white/80! dark:bg-[#313131]/80! border-b! border-black/8!" color="primary" (keydown)="onKeydown($event)"> <mat-toolbar class="flex! items-center! p-[clamp(0.5rem,1vw,1rem)]! backdrop-blur-sm! backdrop-saturate-[1.1]! bg-white/80! dark:bg-[#313131]/80! border-b! border-black/8!" color="primary" (keydown)="onKeydown($event)">
<a class="flex items-center gap-[clamp(0.4rem,1vw,0.6rem)] text-inherit no-underline" routerLink="/"> <a class="flex items-center gap-[clamp(0.4rem,1vw,0.6rem)] text-inherit no-underline" routerLink="/">
<img class="w-[clamp(36px,10vw,48px)] h-auto rounded-full" src="{{AssetsConstants.LOGO}}" width="117" height="128" alt="" aria-hidden="true" draggable="false" <img class="w-[clamp(36px,10vw,48px)] h-auto rounded-full" [ngSrc]="AssetsConstants.LOGO" width="117" height="128" loading="eager" alt="" aria-hidden="true" draggable="false"
oncontextmenu="return false;"> oncontextmenu="return false;">
<span class="font-semibold tracking-[0.2px] text-[clamp(1rem,3vw,1.2rem)]">{{ 'APP.TITLE' | translate }}</span> <span class="font-semibold tracking-[0.2px] text-[clamp(1rem,3vw,1.2rem)]">{{ 'APP.TITLE' | translate }}</span>
</a> </a>
+2 -1
View File
@@ -5,6 +5,7 @@ import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { NgOptimizedImage } from '@angular/common';
import { TranslatePipe } from '@ngx-translate/core'; import { TranslatePipe } from '@ngx-translate/core';
import { ThemeService } from '../../service/theme.service'; import { ThemeService } from '../../service/theme.service';
import { LanguageService } from '../../service/language.service'; import { LanguageService } from '../../service/language.service';
@@ -17,7 +18,7 @@ import {RouterConstants} from '../../constants/RouterConstants';
imports: [ imports: [
RouterLink, RouterLinkActive, RouterLink, RouterLinkActive,
MatToolbarModule, MatIconModule, MatButtonModule, MatMenuModule, MatTooltipModule, MatToolbarModule, MatIconModule, MatButtonModule, MatMenuModule, MatTooltipModule,
TranslatePipe, MatDivider TranslatePipe, MatDivider, NgOptimizedImage
], ],
templateUrl: './topbar.component.html', templateUrl: './topbar.component.html',
changeDetection: ChangeDetectionStrategy.Eager, changeDetection: ChangeDetectionStrategy.Eager,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 739 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 742 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

+2 -2
View File
@@ -19,12 +19,12 @@
<meta property="og:title" content="Andreas Dahm – Playground"> <meta property="og:title" content="Andreas Dahm – Playground">
<meta property="og:description" content="Portfolio and interactive playground of Andreas Dahm, senior software developer: algorithm visualizations, GPU simulations and personal projects."> <meta property="og:description" content="Portfolio and interactive playground of Andreas Dahm, senior software developer: algorithm visualizations, GPU simulations and personal projects.">
<meta property="og:url" content="https://andreas-dahm.eu/"> <meta property="og:url" content="https://andreas-dahm.eu/">
<meta property="og:image" content="https://andreas-dahm.eu/assets/me.webp"> <meta property="og:image" content="https://andreas-dahm.eu/assets/me.jpg">
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Andreas Dahm – Playground"> <meta name="twitter:title" content="Andreas Dahm – Playground">
<meta name="twitter:description" content="Portfolio and interactive playground of Andreas Dahm, senior software developer: algorithm visualizations, GPU simulations and personal projects."> <meta name="twitter:description" content="Portfolio and interactive playground of Andreas Dahm, senior software developer: algorithm visualizations, GPU simulations and personal projects.">
<meta name="twitter:image" content="https://andreas-dahm.eu/assets/me.webp"> <meta name="twitter:image" content="https://andreas-dahm.eu/assets/me.jpg">
<link rel="icon" type="image/x-icon" href="assets/favicon.ico"> <link rel="icon" type="image/x-icon" href="assets/favicon.ico">