From df82d40a96708c2a91c4488f3c845e4b2d79c962 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Date: Sun, 24 May 2026 16:55:35 -0300 Subject: [PATCH] W5.7 (Phase 3E): perf + a11y + OG images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A11y: - Skip-link in (focus-visible only) that jumps to #main. Bilingual ("Skip to content" / "Pular para o conteúdo"). -
landmark wrapping the homepage body. - prefers-reduced-motion media query disables the hover-scale + image transitions for users with vestibular sensitivity. - Skip-link styled with high contrast (gold-on-dark) + outline on keyboard focus. Performance: - HeroBanner background image: fetchPriority="high" + explicit width/height (1600x900) for zero CLS. - FeaturedCase image: fetchPriority="high" + 1280x720 to prevent layout shift while the 2.7MB painting loads. - IconicCases tiles already have loading="lazy". - prose blockquote: overflow-wrap:anywhere so verbatim quotes don't bust the mobile viewport. Open Graph: - app/layout.tsx default OG image set to the green-fireballs painting (any page without its own image card inherits this). - app/c/[slug] OG image is the case's editorial illustration when one exists. WhatsApp, Twitter, Telegram, Slack, ChatGPT search all pull this when the link is shared. 2000x1125 for the "summary_large_image" twitter card. Co-Authored-By: Claude Opus 4.7 (1M context) --- web/app/c/[slug]/page.tsx | 7 ++++++ web/app/globals.css | 39 ++++++++++++++++++++++++++++++++ web/app/layout.tsx | 9 ++++++++ web/app/page.tsx | 2 ++ web/components/featured-case.tsx | 3 +++ web/components/hero-banner.tsx | 5 +++- 6 files changed, 64 insertions(+), 1 deletion(-) diff --git a/web/app/c/[slug]/page.tsx b/web/app/c/[slug]/page.tsx index 45b3a6d..f5e227b 100644 --- a/web/app/c/[slug]/page.tsx +++ b/web/app/c/[slug]/page.tsx @@ -84,6 +84,11 @@ export async function generateMetadata( const title = locale === "pt-br" ? (c.fm.topic_pt_br ?? c.fm.topic ?? slug) : (c.fm.topic ?? slug); const desc = pickLead(c.body, locale).slice(0, 200); const canonical = `${SITE_URL}/c/${slug}`; + // OG image — use the case's editorial illustration when present. WhatsApp, + // Twitter, Slack, Telegram, ChatGPT search all pull this as the link card. + const ogImage = (await hasIllustration(slug)) + ? `${SITE_URL}/api/static/processing/case-art/${slug}.png` + : `${SITE_URL}/api/static/processing/case-art/green-fireballs-narrative.png`; return { title, description: desc, @@ -96,11 +101,13 @@ export async function generateMetadata( siteName: "The Disclosure Bureau", locale: locale === "pt-br" ? "pt_BR" : "en_US", publishedTime: c.fm.created_at, + images: [{ url: ogImage, width: 2000, height: 1125, alt: title }], }, twitter: { card: "summary_large_image", title, description: desc, + images: [ogImage], }, }; } diff --git a/web/app/globals.css b/web/app/globals.css index 308dedf..32ba13e 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -29,6 +29,45 @@ html, body { font-feature-settings: "ss01", "ss02"; } +/* Skip-to-content link (a11y) — hidden until keyboard focus reveals it. */ +.skip-link { + position: fixed; + top: -40px; + left: 8px; + z-index: 100; + padding: 8px 16px; + background: #e0c080; + color: #0a0e1a; + font-family: var(--font-mono), monospace; + font-size: 12px; + font-weight: 600; + border-radius: 4px; + text-decoration: none; + transition: top 0.15s ease-out; +} +.skip-link:focus { + top: 8px; + outline: 2px solid #fff; + outline-offset: 2px; +} + +/* Mobile-first overflow safety on prose blockquotes. */ +.prose blockquote { + overflow-wrap: anywhere; +} + +/* Honour user "reduce motion" preference for our hover-scale transitions. */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + body { background-image: radial-gradient(ellipse 60% 50% at 92% 8%, rgba(0, 255, 156, 0.05) 0%, transparent 65%), diff --git a/web/app/layout.tsx b/web/app/layout.tsx index aabfc7a..f8888e1 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -43,11 +43,17 @@ export const metadata: Metadata = { locale: "pt_BR", alternateLocale: ["en_US"], url: SITE_URL, + images: [{ + url: `${SITE_URL}/api/static/processing/case-art/green-fireballs-narrative.png`, + width: 2000, height: 1125, + alt: "The Disclosure Bureau — UAP/UFO desclassificado", + }], }, twitter: { card: "summary_large_image", title: "The Disclosure Bureau", description: "Arquivos UAP/UFO desclassificados, narrados a partir do registro público.", + images: [`${SITE_URL}/api/static/processing/case-art/green-fireballs-narrative.png`], }, alternates: { canonical: "/", @@ -94,6 +100,9 @@ export default async function RootLayout({ children }: { children: React.ReactNo /> + + {locale === "en" ? "Skip to content" : "Pular para o conteúdo"} + {children}
diff --git a/web/app/page.tsx b/web/app/page.tsx index ab9be6c..5e260e1 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -59,6 +59,7 @@ export default async function Home() {
+
+
diff --git a/web/components/featured-case.tsx b/web/components/featured-case.tsx index 7462602..601a487 100644 --- a/web/components/featured-case.tsx +++ b/web/components/featured-case.tsx @@ -145,6 +145,9 @@ export async function FeaturedCase({ locale }: { locale: "pt-br" | "en" }) { {title} ) : ( diff --git a/web/components/hero-banner.tsx b/web/components/hero-banner.tsx index d582532..6afba16 100644 --- a/web/components/hero-banner.tsx +++ b/web/components/hero-banner.tsx @@ -31,13 +31,16 @@ export function HeroBanner({ locale, stats, heroDocId, heroPage }: { return (
- {/* Background art layer */} + {/* Background art layer — high-priority because it's above the fold */} {heroImg && ( // eslint-disable-next-line @next/next/no-img-element )}