CSP: Inline-Styles entfernt, Policy vom Build erzeugen lassen

Ein CSP-Header im Reverse-Proxy hat die Seite still zerlegt: Astro liefert
seine Skripte als inline type=module aus, "script-src 'self'" blockiert die.
Im Browser blieb data-reveal leer und das Mobilmenue reagierte nicht, ohne
dass eine Fehlermeldung sichtbar war.

Astro kennt die Hashes seiner eigenen Bundles, deshalb setzt jetzt der Build
die Policy per <meta> (security.csp in astro.config.mjs).

Damit die Policy auch fuer Styles streng bleiben kann, sind die letzten drei
style-Attribute raus: sobald eine CSP Hashes fuer Styles enthaelt, ignorieren
Browser 'unsafe-inline', inline gesetzte Farben waeren also ohnehin verloren.

- Laserstrahlen im Hero: .beam-1 bis .beam-5 im <style>-Block
- Genre-Akzentfarben: .accent-<name> statt berechnetem style-Attribut

Geprueft im Browser hinter Caddy: Skripte laufen, Laser-Winkel und
Akzentfarben stimmen, keine Verstoesse in der Konsole.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 23:03:57 +02:00
co-authored by Claude Opus 5
parent 50150639ee
commit de7e8e3c37
3 changed files with 60 additions and 25 deletions
+20
View File
@@ -16,6 +16,26 @@ export default defineConfig({
integrations: [sitemap()],
// Content-Security-Policy. Astro kennt seine eigenen gebuendelten Skripte und
// Styles und traegt deren SHA-256-Hashes automatisch nach - deshalb wird die
// Policy hier gesetzt und nicht im Reverse-Proxy: der weiss von den Hashes
// nichts und wuerde die Inline-Module blockieren.
security: {
csp: {
directives: [
"default-src 'self'",
"img-src 'self' data:",
"font-src 'self'",
"connect-src 'self'",
"form-action 'self'",
"base-uri 'self'",
"object-src 'none'",
],
scriptDirective: { resources: ["'self'"] },
styleDirective: { resources: ["'self'"] },
},
},
image: {
// Bilder werden beim Build zu AVIF/WebP konvertiert.
responsiveStyles: true,
+18 -12
View File
@@ -3,13 +3,9 @@ import { Image } from 'astro:assets';
import banner from '../assets/hero-banner.jpg';
import { ABOUT, GENRES, CONTACT } from '../data/site';
const accentVar: Record<(typeof GENRES)[number]['accent'], string> = {
magenta: 'var(--color-laser-magenta)',
cyan: 'var(--color-laser-cyan)',
gold: 'var(--color-gold-400)',
violet: 'var(--color-laser-violet)',
lime: 'var(--color-laser-lime)',
};
// Die Akzentfarbe kommt ueber die Klasse accent-<name> aus dem <style>-Block
// unten. Bewusst kein style-Attribut: sobald die CSP Hashes fuer Styles setzt,
// ignorieren Browser 'unsafe-inline', und inline gesetzte Farben waeren weg.
---
<section id="about" class="relative overflow-hidden py-24 sm:py-32">
@@ -84,18 +80,16 @@ const accentVar: Record<(typeof GENRES)[number]['accent'], string> = {
{
GENRES.map((genre, i) => (
<article
class="group relative overflow-hidden rounded-2xl border border-white/10 bg-ink-900/50 p-6 transition duration-300 hover:-translate-y-1 hover:border-white/20 hover:bg-ink-850/70"
class={`accent-${genre.accent} group relative overflow-hidden rounded-2xl border border-white/10 bg-ink-900/50 p-6 transition duration-300 hover:-translate-y-1 hover:border-white/20 hover:bg-ink-850/70`}
data-reveal
data-reveal-delay={i * 90}
>
<span
class="absolute inset-x-0 top-0 h-px opacity-60 transition-opacity duration-300 group-hover:opacity-100"
style={`background:linear-gradient(to right, transparent, ${accentVar[genre.accent]}, transparent);`}
class="accent-line absolute inset-x-0 top-0 h-px opacity-60 transition-opacity duration-300 group-hover:opacity-100"
aria-hidden="true"
/>
<span
class="absolute -top-16 -right-16 h-40 w-40 rounded-full opacity-0 blur-3xl transition-opacity duration-500 group-hover:opacity-30"
style={`background:${accentVar[genre.accent]};`}
class="accent-glow absolute -top-16 -right-16 h-40 w-40 rounded-full opacity-0 blur-3xl transition-opacity duration-500 group-hover:opacity-30"
aria-hidden="true"
/>
<h3 class="font-display text-xl text-white">{genre.name}</h3>
@@ -106,3 +100,15 @@ const accentVar: Record<(typeof GENRES)[number]['accent'], string> = {
</div>
</div>
</section>
<style>
/* Akzentfarbe pro Genre. Der Name muss zu `accent` in src/data/site.ts passen. */
.accent-magenta { --accent: var(--color-laser-magenta); }
.accent-cyan { --accent: var(--color-laser-cyan); }
.accent-gold { --accent: var(--color-gold-400); }
.accent-violet { --accent: var(--color-laser-violet); }
.accent-lime { --accent: var(--color-laser-lime); }
.accent-line { background: linear-gradient(to right, transparent, var(--accent), transparent); }
.accent-glow { background: var(--accent); }
</style>
+22 -13
View File
@@ -4,14 +4,11 @@ import club from '../assets/hero-club.jpg';
import nizz from '../assets/nizz-cutout.png';
import { CONTACT, GENRES } from '../data/site';
// Laserstrahlen: Position in %, Start-/Endwinkel, Farbe, Startverzoegerung
const beams = [
{ x: 18, from: '-26deg', to: '-13deg', color: 'var(--color-laser-cyan)', delay: '0s' },
{ x: 32, from: '-12deg', to: '-3deg', color: 'var(--color-laser-magenta)', delay: '-2.6s' },
{ x: 47, from: '9deg', to: '19deg', color: 'var(--color-gold-400)', delay: '-5.1s' },
{ x: 61, from: '22deg', to: '11deg', color: 'var(--color-laser-violet)', delay: '-1.3s' },
{ x: 74, from: '31deg', to: '20deg', color: 'var(--color-laser-cyan)', delay: '-6.7s' },
];
// Fuenf Laserstrahlen. Position, Winkel, Farbe und Verzoegerung stehen im
// <style>-Block unten als .beam-1 bis .beam-5 - bewusst nicht als
// style-Attribut, sonst braeuchte die Content-Security-Policy
// 'unsafe-inline' fuer Styles.
const beams = [1, 2, 3, 4, 5];
const genreLine = GENRES.map((g) => g.name).join(' • ');
---
@@ -33,11 +30,8 @@ const genreLine = GENRES.map((g) => g.name).join(' • ');
{/* Ebene 2: animierte Laser */}
<div class="absolute inset-x-0 top-0 -z-20 h-[125%] overflow-hidden" aria-hidden="true">
{
beams.map((b) => (
<span
class="absolute top-[-12%] h-full w-px origin-top animate-sweep blur-[1.5px] mix-blend-screen"
style={`left:${b.x}%;--beam-from:${b.from};--beam-to:${b.to};animation-delay:${b.delay};background:linear-gradient(to bottom, ${b.color} 0%, ${b.color} 35%, transparent 100%);`}
/>
beams.map((n) => (
<span class={`beam-${n} absolute top-[-12%] h-full w-px origin-top animate-sweep blur-[1.5px] mix-blend-screen`} />
))
}
</div>
@@ -156,3 +150,18 @@ const genreLine = GENRES.map((g) => g.name).join(' • ');
</div>
</div>
</section>
<style>
/* Je Strahl: Position, Winkelbereich der Animation, Farbe, Versatz.
Die Keyframes "sweep" stehen in global.css. */
.beam-1 { left: 18%; --beam-from: -26deg; --beam-to: -13deg; animation-delay: 0s;
background: linear-gradient(to bottom, var(--color-laser-cyan) 0%, var(--color-laser-cyan) 35%, transparent 100%); }
.beam-2 { left: 32%; --beam-from: -12deg; --beam-to: -3deg; animation-delay: -2.6s;
background: linear-gradient(to bottom, var(--color-laser-magenta) 0%, var(--color-laser-magenta) 35%, transparent 100%); }
.beam-3 { left: 47%; --beam-from: 9deg; --beam-to: 19deg; animation-delay: -5.1s;
background: linear-gradient(to bottom, var(--color-gold-400) 0%, var(--color-gold-400) 35%, transparent 100%); }
.beam-4 { left: 61%; --beam-from: 22deg; --beam-to: 11deg; animation-delay: -1.3s;
background: linear-gradient(to bottom, var(--color-laser-violet) 0%, var(--color-laser-violet) 35%, transparent 100%); }
.beam-5 { left: 74%; --beam-from: 31deg; --beam-to: 20deg; animation-delay: -6.7s;
background: linear-gradient(to bottom, var(--color-laser-cyan) 0%, var(--color-laser-cyan) 35%, transparent 100%); }
</style>