W5.7 (Phase 3E): perf + a11y + OG images
A11y:
- Skip-link in <body> (focus-visible only) that jumps to #main.
Bilingual ("Skip to content" / "Pular para o conteúdo").
- <main id="main"> 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) <noreply@anthropic.com>
This commit is contained in:
parent
1687120b7f
commit
df82d40a96
6 changed files with 64 additions and 1 deletions
|
|
@ -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 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 desc = pickLead(c.body, locale).slice(0, 200);
|
||||||
const canonical = `${SITE_URL}/c/${slug}`;
|
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 {
|
return {
|
||||||
title,
|
title,
|
||||||
description: desc,
|
description: desc,
|
||||||
|
|
@ -96,11 +101,13 @@ export async function generateMetadata(
|
||||||
siteName: "The Disclosure Bureau",
|
siteName: "The Disclosure Bureau",
|
||||||
locale: locale === "pt-br" ? "pt_BR" : "en_US",
|
locale: locale === "pt-br" ? "pt_BR" : "en_US",
|
||||||
publishedTime: c.fm.created_at,
|
publishedTime: c.fm.created_at,
|
||||||
|
images: [{ url: ogImage, width: 2000, height: 1125, alt: title }],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title,
|
title,
|
||||||
description: desc,
|
description: desc,
|
||||||
|
images: [ogImage],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,45 @@ html, body {
|
||||||
font-feature-settings: "ss01", "ss02";
|
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 {
|
body {
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(ellipse 60% 50% at 92% 8%, rgba(0, 255, 156, 0.05) 0%, transparent 65%),
|
radial-gradient(ellipse 60% 50% at 92% 8%, rgba(0, 255, 156, 0.05) 0%, transparent 65%),
|
||||||
|
|
|
||||||
|
|
@ -43,11 +43,17 @@ export const metadata: Metadata = {
|
||||||
locale: "pt_BR",
|
locale: "pt_BR",
|
||||||
alternateLocale: ["en_US"],
|
alternateLocale: ["en_US"],
|
||||||
url: SITE_URL,
|
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: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title: "The Disclosure Bureau",
|
title: "The Disclosure Bureau",
|
||||||
description: "Arquivos UAP/UFO desclassificados, narrados a partir do registro público.",
|
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: {
|
alternates: {
|
||||||
canonical: "/",
|
canonical: "/",
|
||||||
|
|
@ -94,6 +100,9 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body className={`${inter.variable} ${mono.variable} ${fraunces.variable}`}>
|
<body className={`${inter.variable} ${mono.variable} ${fraunces.variable}`}>
|
||||||
|
<a href="#main" className="skip-link">
|
||||||
|
{locale === "en" ? "Skip to content" : "Pular para o conteúdo"}
|
||||||
|
</a>
|
||||||
{children}
|
{children}
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
<div className="fixed bottom-3 left-3 z-40 opacity-70 hover:opacity-100 transition">
|
<div className="fixed bottom-3 left-3 z-40 opacity-70 hover:opacity-100 transition">
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ export default async function Home() {
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<SiteHeader locale={locale} />
|
<SiteHeader locale={locale} />
|
||||||
|
|
||||||
|
<main id="main">
|
||||||
<HeroBanner
|
<HeroBanner
|
||||||
locale={locale}
|
locale={locale}
|
||||||
stats={{
|
stats={{
|
||||||
|
|
@ -96,6 +97,7 @@ export default async function Home() {
|
||||||
</p>
|
</p>
|
||||||
<DocListFilters docs={docs} />
|
<DocListFilters docs={docs} />
|
||||||
</section>
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
<ChatBubble context={{}} />
|
<ChatBubble context={{}} />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,9 @@ export async function FeaturedCase({ locale }: { locale: "pt-br" | "en" }) {
|
||||||
<img
|
<img
|
||||||
src={heroImg}
|
src={heroImg}
|
||||||
alt={title}
|
alt={title}
|
||||||
|
fetchPriority="high"
|
||||||
|
width={1280}
|
||||||
|
height={720}
|
||||||
className={`absolute inset-0 w-full h-full object-cover ${heroIsArt ? "object-center" : "object-top"} opacity-95 group-hover:opacity-100 group-hover:scale-[1.02] transition-all duration-700`}
|
className={`absolute inset-0 w-full h-full object-cover ${heroIsArt ? "object-center" : "object-top"} opacity-95 group-hover:opacity-100 group-hover:scale-[1.02] transition-all duration-700`}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -31,13 +31,16 @@ export function HeroBanner({ locale, stats, heroDocId, heroPage }: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative overflow-hidden border-b border-[rgba(224,192,128,0.15)]">
|
<section className="relative overflow-hidden border-b border-[rgba(224,192,128,0.15)]">
|
||||||
{/* Background art layer */}
|
{/* Background art layer — high-priority because it's above the fold */}
|
||||||
{heroImg && (
|
{heroImg && (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img
|
<img
|
||||||
src={heroImg}
|
src={heroImg}
|
||||||
alt=""
|
alt=""
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
fetchPriority="high"
|
||||||
|
width={1600}
|
||||||
|
height={900}
|
||||||
className="absolute inset-0 w-full h-full object-cover opacity-[0.18] mix-blend-luminosity"
|
className="absolute inset-0 w-full h-full object-cover opacity-[0.18] mix-blend-luminosity"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue