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.
No files matched your search
@@ -46,6 +46,7 @@ Dockerfile text eol=lf
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.webp binary
|
||||
*.pdf binary
|
||||
*.zip binary
|
||||
*.tar binary
|
||||
|
||||
@@ -90,6 +90,7 @@ Then open http://localhost:8080
|
||||
| `npm test` | Unit tests (Vitest + jsdom, no browser needed) |
|
||||
| `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 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):
|
||||
|
||||
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`
|
||||
3. Unit tests — `npm test`
|
||||
4. Production build — `npx ng build --configuration production`
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test --watch=false",
|
||||
"lint": "ng lint",
|
||||
"i18n:check": "node scripts/check-i18n.mjs"
|
||||
"lint": "ng lint && npm run assets:check",
|
||||
"i18n:check": "node scripts/check-i18n.mjs",
|
||||
"assets:check": "node scripts/check-assets.mjs"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -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.');
|
||||
@@ -1,44 +1,44 @@
|
||||
export class AssetsConstants {
|
||||
|
||||
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_EN = '/assets/flags/gb.svg';
|
||||
|
||||
//logos
|
||||
static readonly CHAMAELEON_LOGO = '/assets/logos/logo_chamaeleon.svg';
|
||||
static readonly TH_BINGEN_LOGO = '/assets/logos/bingen-logo-white.svg';
|
||||
static readonly ASSYST_LOGO = '/assets/logos/assyst_gmbh_logo.jpg';
|
||||
static readonly COLORDIGITAL_LOGO = '/assets/logos/dmixcloud_logo.jpg';
|
||||
static readonly TERAPORT_LOGO = '/assets/logos/teraport_gmbh_logo.jpg';
|
||||
static readonly ASSYST_LOGO = '/assets/logos/assyst_gmbh_logo.webp';
|
||||
static readonly COLORDIGITAL_LOGO = '/assets/logos/dmixcloud_logo.webp';
|
||||
static readonly TERAPORT_LOGO = '/assets/logos/teraport_gmbh_logo.webp';
|
||||
|
||||
static readonly DIPLOMA = '/assets/projects/diploma/Dahm2010-Diplomarbeit.pdf';
|
||||
|
||||
//project images
|
||||
static readonly PLAYGROUND_IMAGES = [
|
||||
'/assets/projects/playground/1.png'
|
||||
'/assets/projects/playground/1.webp'
|
||||
];
|
||||
|
||||
static readonly EL_MUCHO_IMAGES = [
|
||||
'/assets/projects/el-mucho/1.jpg',
|
||||
'/assets/projects/el-mucho/2.jpg',
|
||||
'/assets/projects/el-mucho/3.jpg',
|
||||
'/assets/projects/el-mucho/4.jpg'
|
||||
'/assets/projects/el-mucho/1.webp',
|
||||
'/assets/projects/el-mucho/2.webp',
|
||||
'/assets/projects/el-mucho/3.webp',
|
||||
'/assets/projects/el-mucho/4.webp'
|
||||
];
|
||||
|
||||
static readonly GAME_JAMS_IMAGES = [
|
||||
'/assets/projects/game-jams/1.png',
|
||||
'/assets/projects/game-jams/2.png',
|
||||
'/assets/projects/game-jams/3.png'
|
||||
'/assets/projects/game-jams/1.webp',
|
||||
'/assets/projects/game-jams/2.webp',
|
||||
'/assets/projects/game-jams/3.webp'
|
||||
];
|
||||
|
||||
static readonly DIPLOMA_IMAGES = [
|
||||
'/assets/projects/diploma/1.jpg',
|
||||
'/assets/projects/diploma/2.jpg',
|
||||
'/assets/projects/diploma/3.jpg',
|
||||
'/assets/projects/diploma/4.jpg',
|
||||
'/assets/projects/diploma/5.jpg',
|
||||
'/assets/projects/diploma/6.jpg'
|
||||
'/assets/projects/diploma/1.webp',
|
||||
'/assets/projects/diploma/2.webp',
|
||||
'/assets/projects/diploma/3.webp',
|
||||
'/assets/projects/diploma/4.webp',
|
||||
'/assets/projects/diploma/5.webp',
|
||||
'/assets/projects/diploma/6.webp'
|
||||
];
|
||||
|
||||
static readonly CLAUDE_BOX_IMAGES = [
|
||||
@@ -46,16 +46,16 @@ export class AssetsConstants {
|
||||
];
|
||||
|
||||
static readonly TRIBBLE_IMAGES = [
|
||||
'/assets/projects/tribble-the-homeserver/1.png',
|
||||
'/assets/projects/tribble-the-homeserver/2.png',
|
||||
'/assets/projects/tribble-the-homeserver/3.png',
|
||||
'/assets/projects/tribble-the-homeserver/4.png'
|
||||
'/assets/projects/tribble-the-homeserver/1.webp',
|
||||
'/assets/projects/tribble-the-homeserver/2.webp',
|
||||
'/assets/projects/tribble-the-homeserver/3.webp',
|
||||
'/assets/projects/tribble-the-homeserver/4.webp'
|
||||
];
|
||||
|
||||
static readonly TBP_IMAGES = [
|
||||
'/assets/projects/to-be-played/1.png',
|
||||
'/assets/projects/to-be-played/2.png',
|
||||
'/assets/projects/to-be-played/3.png',
|
||||
'/assets/projects/to-be-played/4.png'
|
||||
'/assets/projects/to-be-played/1.webp',
|
||||
'/assets/projects/to-be-played/2.webp',
|
||||
'/assets/projects/to-be-played/3.webp',
|
||||
'/assets/projects/to-be-played/4.webp'
|
||||
];
|
||||
}
|
||||
@@ -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)">
|
||||
<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;">
|
||||
<span class="font-semibold tracking-[0.2px] text-[clamp(1rem,3vw,1.2rem)]">{{ 'APP.TITLE' | translate }}</span>
|
||||
</a>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { NgOptimizedImage } from '@angular/common';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { ThemeService } from '../../service/theme.service';
|
||||
import { LanguageService } from '../../service/language.service';
|
||||
@@ -17,7 +18,7 @@ import {RouterConstants} from '../../constants/RouterConstants';
|
||||
imports: [
|
||||
RouterLink, RouterLinkActive,
|
||||
MatToolbarModule, MatIconModule, MatButtonModule, MatMenuModule, MatTooltipModule,
|
||||
TranslatePipe, MatDivider
|
||||
TranslatePipe, MatDivider, NgOptimizedImage
|
||||
],
|
||||
templateUrl: './topbar.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 932 B |
|
After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 426 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 739 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 742 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 97 KiB |
@@ -19,12 +19,12 @@
|
||||
<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: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: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: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">
|
||||
|
||||
|
||||