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:
@@ -16,6 +16,26 @@ export default defineConfig({
|
|||||||
|
|
||||||
integrations: [sitemap()],
|
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: {
|
image: {
|
||||||
// Bilder werden beim Build zu AVIF/WebP konvertiert.
|
// Bilder werden beim Build zu AVIF/WebP konvertiert.
|
||||||
responsiveStyles: true,
|
responsiveStyles: true,
|
||||||
|
|||||||
+18
-12
@@ -3,13 +3,9 @@ import { Image } from 'astro:assets';
|
|||||||
import banner from '../assets/hero-banner.jpg';
|
import banner from '../assets/hero-banner.jpg';
|
||||||
import { ABOUT, GENRES, CONTACT } from '../data/site';
|
import { ABOUT, GENRES, CONTACT } from '../data/site';
|
||||||
|
|
||||||
const accentVar: Record<(typeof GENRES)[number]['accent'], string> = {
|
// Die Akzentfarbe kommt ueber die Klasse accent-<name> aus dem <style>-Block
|
||||||
magenta: 'var(--color-laser-magenta)',
|
// unten. Bewusst kein style-Attribut: sobald die CSP Hashes fuer Styles setzt,
|
||||||
cyan: 'var(--color-laser-cyan)',
|
// ignorieren Browser 'unsafe-inline', und inline gesetzte Farben waeren weg.
|
||||||
gold: 'var(--color-gold-400)',
|
|
||||||
violet: 'var(--color-laser-violet)',
|
|
||||||
lime: 'var(--color-laser-lime)',
|
|
||||||
};
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<section id="about" class="relative overflow-hidden py-24 sm:py-32">
|
<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) => (
|
GENRES.map((genre, i) => (
|
||||||
<article
|
<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
|
||||||
data-reveal-delay={i * 90}
|
data-reveal-delay={i * 90}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="absolute inset-x-0 top-0 h-px opacity-60 transition-opacity duration-300 group-hover:opacity-100"
|
class="accent-line 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);`}
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<span
|
<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"
|
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"
|
||||||
style={`background:${accentVar[genre.accent]};`}
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<h3 class="font-display text-xl text-white">{genre.name}</h3>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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
@@ -4,14 +4,11 @@ import club from '../assets/hero-club.jpg';
|
|||||||
import nizz from '../assets/nizz-cutout.png';
|
import nizz from '../assets/nizz-cutout.png';
|
||||||
import { CONTACT, GENRES } from '../data/site';
|
import { CONTACT, GENRES } from '../data/site';
|
||||||
|
|
||||||
// Laserstrahlen: Position in %, Start-/Endwinkel, Farbe, Startverzoegerung
|
// Fuenf Laserstrahlen. Position, Winkel, Farbe und Verzoegerung stehen im
|
||||||
const beams = [
|
// <style>-Block unten als .beam-1 bis .beam-5 - bewusst nicht als
|
||||||
{ x: 18, from: '-26deg', to: '-13deg', color: 'var(--color-laser-cyan)', delay: '0s' },
|
// style-Attribut, sonst braeuchte die Content-Security-Policy
|
||||||
{ x: 32, from: '-12deg', to: '-3deg', color: 'var(--color-laser-magenta)', delay: '-2.6s' },
|
// 'unsafe-inline' fuer Styles.
|
||||||
{ x: 47, from: '9deg', to: '19deg', color: 'var(--color-gold-400)', delay: '-5.1s' },
|
const beams = [1, 2, 3, 4, 5];
|
||||||
{ 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' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const genreLine = GENRES.map((g) => g.name).join(' • ');
|
const genreLine = GENRES.map((g) => g.name).join(' • ');
|
||||||
---
|
---
|
||||||
@@ -33,11 +30,8 @@ const genreLine = GENRES.map((g) => g.name).join(' • ');
|
|||||||
{/* Ebene 2: animierte Laser */}
|
{/* Ebene 2: animierte Laser */}
|
||||||
<div class="absolute inset-x-0 top-0 -z-20 h-[125%] overflow-hidden" aria-hidden="true">
|
<div class="absolute inset-x-0 top-0 -z-20 h-[125%] overflow-hidden" aria-hidden="true">
|
||||||
{
|
{
|
||||||
beams.map((b) => (
|
beams.map((n) => (
|
||||||
<span
|
<span class={`beam-${n} absolute top-[-12%] h-full w-px origin-top animate-sweep blur-[1.5px] mix-blend-screen`} />
|
||||||
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%);`}
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -156,3 +150,18 @@ const genreLine = GENRES.map((g) => g.name).join(' • ');
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user