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:
60
app/page.tsx
60
app/page.tsx
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user