Skip to content

Commit 46ce3ff

Browse files
LucaDeLeoclaude
andcommitted
fix: site audit improvements — a11y, perf, SEO, DX
Accessibility: - Mobile menu: add focus trap, aria-modal, aria-labelledby, focus restore to trigger button on close, visibility hidden when closed - Backdrop gets aria-hidden="true" Performance: - Add LazyYouTubeEmbed facade (thumbnail + play button, loads iframe on click via youtube-nocookie.com) - InteractiveCourseCard: replace useState with refs + rAF throttling for mouse glow effect, eliminating re-renders on mousemove - FadeInSection: fix broken dynamic Tailwind classes (duration-${n}) by using inline style properties with bounds clamping - Consolidate RUMMonitor into PerformanceMonitor using web-vitals library with deduplication and Vercel Analytics retry - Remove dead API fallback from PerformanceMonitor - Remove redundant useMemo in LazyYouTubeEmbed (React Compiler) SEO: - Sitemap: replace lastModified: new Date() with static per-route dates for accurate lastmod signals - Fix LinkedIn URL inconsistency in json-ld.tsx structured data - Fix logo path in OrganizationJsonLd (baish-logo-192 -> logo-192) - Generate OG default image and remove TODO comment DX: - Extract LinkedIn URL to constants/social-links.ts - Add build wrapper script to suppress baseline-browser-mapping noise - Pin baseline-browser-mapping@2.9.19 via overrides - Tighten Lighthouse CI thresholds (perf: error@0.7, LCP: error@4s) - Add outputFileTracingRoot and turbopack.root to next.config.ts - Move scroll-to-button constants to module level - Fix glow CSS custom property unit mismatch in InteractiveCourseCard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 86ae8e4 commit 46ce3ff

24 files changed

Lines changed: 619 additions & 358 deletions

app/[locale]/contact/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { BreadcrumbJsonLd, FAQJsonLd } from "@/app/components/json-ld";
88
import { withLocale } from "@/app/utils/locale";
99
import { getDictionary } from "../dictionaries";
1010
import { generatePageMetadata, SEO_CONTENT } from "@/app/utils/seo";
11+
import { ORGANIZATION_LINKEDIN_URL } from "@/app/constants/social-links";
1112
import type { AppLocale } from "@/i18n.config";
1213
import { isAppLocale } from "@/i18n.config";
1314

@@ -198,7 +199,7 @@ export default async function ContactPage({
198199
Instagram
199200
</a>
200201
<a
201-
href="https://www.linkedin.com/company/baish-arg"
202+
href={ORGANIZATION_LINKEDIN_URL}
202203
className="inline-flex items-center gap-2 text-sm font-semibold text-[var(--color-accent-primary)] hover:text-[var(--color-accent-tertiary)] transition"
203204
target="_blank"
204205
rel="noopener noreferrer"

app/[locale]/layout.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { i18n, isAppLocale, type AppLocale } from "../../i18n.config";
1515
import { getDictionary } from "./dictionaries";
1616
import Head from "../head";
1717
import Header from "../components/header";
18-
import RUMMonitor from "../components/rum-monitor";
1918
import "../globals.css";
2019

2120
const sourceSerif = Source_Serif_4({
@@ -136,7 +135,6 @@ export default async function LocaleLayout({
136135
{children}
137136
<DeferredAnalytics />
138137
<PerformanceMonitor />
139-
<RUMMonitor />
140138
{/* {process.env.NODE_ENV === "development" && <LCPDebugger />} */}
141139
</LanguageProvider>
142140
</Suspense>

app/[locale]/resources/page.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { AnimatedTitle } from "@/app/components/animated-title";
88
import { BreadcrumbJsonLd } from "@/app/components/json-ld";
99
import { withLocale } from "@/app/utils/locale";
1010
import { InteractiveCourseCard } from "@/app/components/interactive-course-card";
11+
import { LazyYouTubeEmbed } from "@/app/components/lazy-youtube-embed";
1112
import { HugeiconsIcon } from "@hugeicons/react";
1213
import {
1314
Video01Icon,
@@ -123,13 +124,9 @@ export default async function Resources({
123124
className="relative w-full overflow-hidden rounded-xl"
124125
style={{ paddingBottom: "56.25%" }}
125126
>
126-
<iframe
127-
className="absolute inset-0 h-full w-full"
128-
src="https://www.youtube.com/embed/oAJUuY6gAnY"
127+
<LazyYouTubeEmbed
128+
videoId="oAJUuY6gAnY"
129129
title="Why experts fear superintelligent AI – and what we can do about it"
130-
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
131-
allowFullScreen
132-
style={{ border: 0 }}
133130
/>
134131
</div>
135132
</div>

app/components/fade-in-section.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,23 @@ export function FadeInSection({
7676
});
7777

7878
const variantClass = variantClasses[variant];
79-
const durationClass = `duration-${duration}`;
80-
const delayClass = delay > 0 ? `delay-${delay}` : "";
79+
const safeDuration = Number.isFinite(duration)
80+
? Math.min(Math.max(duration, 0), 2000)
81+
: 400;
82+
const safeDelay =
83+
delay > 0 && Number.isFinite(delay) ? Math.min(delay, 2000) : 0;
8184

8285
return (
8386
<Component
8487
ref={ref}
8588
id={id}
86-
className={`transition-all ease-out ${durationClass} ${delayClass} ${
89+
className={`transition-all ease-out ${
8790
isVisible ? variantClass.visible : variantClass.hidden
8891
} ${className}`}
92+
style={{
93+
transitionDuration: `${safeDuration}ms`,
94+
transitionDelay: safeDelay > 0 ? `${safeDelay}ms` : "0ms",
95+
}}
8996
>
9097
{children}
9198
</Component>

app/components/footer.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ScrollToButton } from "./scroll-to-button";
44
import type { AppLocale } from "@/i18n.config";
55
import type { Dictionary } from "@/app/[locale]/dictionaries";
66
import { withLocale } from "@/app/utils/locale";
7+
import { ORGANIZATION_LINKEDIN_URL } from "@/app/constants/social-links";
78
import { HugeiconsIcon } from "@hugeicons/react";
89
import {
910
InstagramIcon,
@@ -94,7 +95,7 @@ export default function Footer({ locale, t }: FooterProps) {
9495
<HugeiconsIcon icon={InstagramIcon} size={20} />
9596
</a>
9697
<a
97-
href="https://www.linkedin.com/company/baish-arg"
98+
href={ORGANIZATION_LINKEDIN_URL}
9899
aria-label="LinkedIn"
99100
className="text-slate-600 hover:text-[var(--color-accent-primary)] transition"
100101
target="_blank"

app/components/header.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const HeaderComponent = ({ locale, t }: HeaderProps) => {
6969
const [scrolled, setScrolled] = useState(false);
7070
const [isNarrow, setIsNarrow] = useState(false);
7171
const [isCramped, setIsCramped] = useState(false);
72+
const mobileMenuButtonRef = useRef<HTMLButtonElement>(null);
7273
const titleContainerRef = useRef<HTMLDivElement>(null);
7374
const lastScrollY = useRef(0);
7475
const prefersReducedMotion = usePrefersReducedMotion();
@@ -368,6 +369,7 @@ const HeaderComponent = ({ locale, t }: HeaderProps) => {
368369
</ScrollToButton>
369370

370371
<button
372+
ref={mobileMenuButtonRef}
371373
className="md:hidden flex flex-col justify-center items-center w-10 h-10 rounded-lg hover:bg-slate-100 transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--color-accent-primary)] focus:ring-offset-2 header-menu-btn"
372374
onClick={toggleMobileMenu}
373375
aria-label={mobileMenuOpen ? t.closeMenu : t.openMenu}
@@ -411,6 +413,7 @@ const HeaderComponent = ({ locale, t }: HeaderProps) => {
411413
pathname={pathname}
412414
isOpen={mobileMenuOpen}
413415
onClose={closeMobileMenu}
416+
triggerRef={mobileMenuButtonRef}
414417
/>
415418
)}
416419
</header>

app/components/interactive-course-card.tsx

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useRef, useState, MouseEvent } from "react";
3+
import { useEffect, useRef, MouseEvent } from "react";
44
import { HugeiconsIcon } from "@hugeicons/react";
55
import { BookOpen01Icon, GraduationScrollIcon, ArrowUpRight01Icon } from "@hugeicons/core-free-icons";
66

@@ -24,21 +24,43 @@ export function InteractiveCourseCard({
2424
accentColor,
2525
}: InteractiveCourseCardProps) {
2626
const cardRef = useRef<HTMLAnchorElement>(null);
27-
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
28-
const [isHovered, setIsHovered] = useState(false);
27+
const pointerRef = useRef({ x: 0, y: 0 });
28+
const frameRef = useRef<number | null>(null);
29+
30+
useEffect(() => {
31+
return () => {
32+
if (frameRef.current !== null) {
33+
cancelAnimationFrame(frameRef.current);
34+
}
35+
};
36+
}, []);
37+
38+
const commitPointerPosition = () => {
39+
frameRef.current = null;
40+
const card = cardRef.current;
41+
if (!card) return;
42+
card.style.setProperty("--glow-x", `${pointerRef.current.x}px`);
43+
card.style.setProperty("--glow-y", `${pointerRef.current.y}px`);
44+
};
2945

3046
const handleMouseMove = (e: MouseEvent<HTMLAnchorElement>) => {
3147
if (!cardRef.current) return;
3248

3349
const rect = cardRef.current.getBoundingClientRect();
34-
const x = e.clientX - rect.left;
35-
const y = e.clientY - rect.top;
50+
pointerRef.current.x = e.clientX - rect.left;
51+
pointerRef.current.y = e.clientY - rect.top;
3652

37-
setMousePosition({ x, y });
53+
if (frameRef.current === null) {
54+
frameRef.current = requestAnimationFrame(commitPointerPosition);
55+
}
3856
};
3957

40-
const handleMouseEnter = () => setIsHovered(true);
41-
const handleMouseLeave = () => setIsHovered(false);
58+
const handleMouseLeave = () => {
59+
if (frameRef.current !== null) {
60+
cancelAnimationFrame(frameRef.current);
61+
frameRef.current = null;
62+
}
63+
};
4264

4365
const IconSVG = icon === "book" ? (
4466
<HugeiconsIcon icon={BookOpen01Icon} size={24} className="text-[#9275E5]" />
@@ -52,38 +74,31 @@ export function InteractiveCourseCard({
5274
href={url}
5375
target="_blank"
5476
rel="noopener noreferrer"
55-
className="group relative block overflow-hidden rounded-2xl border border-slate-200 bg-white/80 backdrop-blur-sm p-6 transition-all duration-300 hover:border-transparent hover:shadow-2xl"
77+
className="group relative block overflow-hidden rounded-2xl border border-slate-200 bg-white/80 p-6 backdrop-blur-sm transition-all duration-300 hover:-translate-y-1 hover:border-transparent hover:shadow-2xl [--glow-x:150px] [--glow-y:150px]"
5678
onMouseMove={handleMouseMove}
57-
onMouseEnter={handleMouseEnter}
5879
onMouseLeave={handleMouseLeave}
59-
style={{
60-
transform: isHovered ? 'translateY(-4px)' : 'translateY(0)',
61-
}}
6280
>
6381
{/* Cursor glow effect */}
64-
{isHovered && (
65-
<div
66-
className="pointer-events-none absolute z-0 rounded-full opacity-0 transition-opacity duration-300 group-hover:opacity-100"
67-
style={{
68-
width: '300px',
69-
height: '300px',
70-
left: mousePosition.x - 150,
71-
top: mousePosition.y - 150,
72-
background: `radial-gradient(circle, ${accentColor}40 0%, ${accentColor}20 40%, transparent 70%)`,
73-
filter: 'blur(20px)',
74-
}}
75-
/>
76-
)}
82+
<div
83+
className="pointer-events-none absolute z-0 h-[300px] w-[300px] rounded-full opacity-0 transition-opacity duration-300 group-hover:opacity-100"
84+
style={{
85+
left: "calc(var(--glow-x) - 150px)",
86+
top: "calc(var(--glow-y) - 150px)",
87+
background: `radial-gradient(circle, ${accentColor}40 0%, ${accentColor}20 40%, transparent 70%)`,
88+
filter: "blur(20px)",
89+
}}
90+
/>
7791

7892
{/* Border glow */}
7993
<div
8094
className="absolute inset-0 rounded-2xl opacity-0 transition-opacity duration-300 group-hover:opacity-100"
8195
style={{
8296
background: `linear-gradient(135deg, ${accentColor}30, ${accentColor}10, transparent)`,
83-
padding: '1px',
84-
WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
85-
WebkitMaskComposite: 'xor',
86-
maskComposite: 'exclude',
97+
padding: "1px",
98+
WebkitMask:
99+
"linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)",
100+
WebkitMaskComposite: "xor",
101+
maskComposite: "exclude",
87102
}}
88103
/>
89104

app/components/json-ld.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AppLocale } from "@/i18n.config";
2+
import { ORGANIZATION_LINKEDIN_URL } from "@/app/constants/social-links";
23

34
const BASE_URL = "https://baish.com.ar";
45

@@ -140,12 +141,12 @@ export function OrganizationJsonLd() {
140141
name: "BAISH - Buenos Aires AI Safety Hub",
141142
alternateName: "BAISH",
142143
url: BASE_URL,
143-
logo: `${BASE_URL}/images/logos/baish-logo-192.png`,
144+
logo: `${BASE_URL}/images/logos/logo-192.png`,
144145
description:
145146
"Supporting students in Buenos Aires to enter AI safety research through courses, workshops, and community.",
146147
sameAs: [
147148
"https://t.me/+zhSGhXrn56g1YjVh",
148-
"https://www.linkedin.com/company/baish-ai-safety",
149+
ORGANIZATION_LINKEDIN_URL,
149150
"https://www.youtube.com/@BAISHaiSafety",
150151
],
151152
contactPoint: {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"use client";
2+
3+
import Image from "next/image";
4+
import { useState } from "react";
5+
6+
interface LazyYouTubeEmbedProps {
7+
videoId: string;
8+
title: string;
9+
className?: string;
10+
}
11+
12+
export function LazyYouTubeEmbed({
13+
videoId,
14+
title,
15+
className = "",
16+
}: LazyYouTubeEmbedProps) {
17+
const [isActivated, setIsActivated] = useState(false);
18+
19+
const thumbnailSrc = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
20+
const embedSrc = `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&modestbranding=1&rel=0`;
21+
22+
if (isActivated) {
23+
return (
24+
<iframe
25+
className={`absolute inset-0 h-full w-full ${className}`}
26+
src={embedSrc}
27+
title={title}
28+
loading="lazy"
29+
referrerPolicy="strict-origin-when-cross-origin"
30+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
31+
allowFullScreen
32+
/>
33+
);
34+
}
35+
36+
return (
37+
<button
38+
type="button"
39+
onClick={() => setIsActivated(true)}
40+
className={`absolute inset-0 block h-full w-full overflow-hidden bg-slate-900 ${className}`}
41+
aria-label={`Play video: ${title}`}
42+
>
43+
<Image
44+
src={thumbnailSrc}
45+
alt=""
46+
fill
47+
sizes="(max-width: 768px) 100vw, 896px"
48+
className="object-cover"
49+
/>
50+
<span className="absolute inset-0 bg-slate-900/25 transition-colors duration-200 hover:bg-slate-900/10" />
51+
<span className="absolute left-1/2 top-1/2 flex h-16 w-16 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-white/95 shadow-xl">
52+
<span
53+
className="ml-1 h-0 w-0 border-y-[10px] border-y-transparent border-l-[16px] border-l-[var(--color-accent-primary)]"
54+
aria-hidden="true"
55+
/>
56+
</span>
57+
</button>
58+
);
59+
}

0 commit comments

Comments
 (0)