security: fix XSS in map popups, refactor HeroTextContent, add evict test

- escapeHtml() in MapView.tsx sanitises city/country/url/name before HTML injection
- HeroTextContent extracted to top-level component with explicit props (was nested inside async Server Component)
- rate-limit.test.ts: add evictIfFull stress test (6/6 pass)
This commit is contained in:
ralksta
2026-03-13 12:52:09 +01:00
parent c610ad2a7d
commit 6c20218369
3 changed files with 64 additions and 29 deletions

View File

@@ -39,6 +39,37 @@ function HeroNavLinks({
);
}
/** Title + optional subtitle + nav — shared by mosaic, minimal, fullbleed, split. */
function HeroTextContent({
title,
subtitle,
subpages,
albums,
}: {
title: string;
subtitle?: string;
subpages: { slug: string; name: string }[];
albums: { id: string; slug: string; albumName: string }[];
}) {
return (
<>
<FadeIn delay={0}>
<h1 className="hero__title">{title}</h1>
</FadeIn>
{subtitle && (
<FadeIn delay={100}>
<p className="hero__subtitle">{subtitle}</p>
</FadeIn>
)}
<FadeIn delay={200}>
<nav className="hero__nav">
<HeroNavLinks subpages={subpages} albums={albums} />
</nav>
</FadeIn>
</>
);
}
export default async function HomePage() {
const config = getConfig();
@@ -140,27 +171,6 @@ export default async function HomePage() {
);
}
/** Title + optional subtitle + nav — shared by mosaic, minimal, fullbleed, split. */
function HeroTextContent() {
return (
<>
<FadeIn delay={0}>
<h1 className="hero__title">{config.siteTitle}</h1>
</FadeIn>
{config.siteSubtitle && (
<FadeIn delay={100}>
<p className="hero__subtitle">{config.siteSubtitle}</p>
</FadeIn>
)}
<FadeIn delay={200}>
<nav className="hero__nav">
<HeroNavLinks subpages={subpages} albums={albums} />
</nav>
</FadeIn>
</>
);
}
// ── Mosaic: multi-image grid with frosted title overlay ────────
if (heroStyle === 'mosaic') {
return (
@@ -182,7 +192,7 @@ export default async function HomePage() {
))}
</div>
<div className="hero__mosaic-overlay">
<HeroTextContent />
<HeroTextContent title={config.siteTitle} subtitle={config.siteSubtitle} subpages={subpages} albums={albums} />
</div>
</div>
);
@@ -193,7 +203,7 @@ export default async function HomePage() {
return (
<div className="hero hero--minimal">
<div className="hero__content">
<HeroTextContent />
<HeroTextContent title={config.siteTitle} subtitle={config.siteSubtitle} subpages={subpages} albums={albums} />
</div>
</div>
);
@@ -205,7 +215,7 @@ export default async function HomePage() {
<div className="hero hero--fullbleed">
<HeroCarousel images={heroData} />
<div className="hero__fullbleed-overlay">
<HeroTextContent />
<HeroTextContent title={config.siteTitle} subtitle={config.siteSubtitle} subpages={subpages} albums={albums} />
</div>
</div>
);
@@ -217,7 +227,7 @@ export default async function HomePage() {
{/* ── Left Panel ──────────────────────────────── */}
<div className="hero__left">
<div className="hero__content">
<HeroTextContent />
<HeroTextContent title={config.siteTitle} subtitle={config.siteSubtitle} subpages={subpages} albums={albums} />
</div>
</div>