diff --git a/web/app/bureau/page.tsx b/web/app/bureau/page.tsx index 21aae11..071807f 100644 --- a/web/app/bureau/page.tsx +++ b/web/app/bureau/page.tsx @@ -4,7 +4,6 @@ * Reader-facing list of every assembled narrative. No detective surfacing, * no artefact dumps. Just stories with hooks. */ -import { AuthBar } from "@/components/auth-bar"; import { BureauNav } from "@/components/bureau-nav"; import { CaseLibrary } from "@/components/case-library"; import { getLocale } from "@/components/locale-toggle"; @@ -18,7 +17,6 @@ export default async function BureauPage() { return (
-

diff --git a/web/app/c/[slug]/page.tsx b/web/app/c/[slug]/page.tsx index 45e2ecb..35d9143 100644 --- a/web/app/c/[slug]/page.tsx +++ b/web/app/c/[slug]/page.tsx @@ -10,7 +10,6 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; import type { Metadata } from "next"; import { MarkdownBody } from "@/components/markdown-body"; -import { AuthBar } from "@/components/auth-bar"; import { BureauNav } from "@/components/bureau-nav"; import { getLocale } from "@/components/locale-toggle"; import { pickLocaleBody } from "@/lib/bilingual"; @@ -156,7 +155,6 @@ export default async function CaseReportPage({ { label: locale === "en" ? "case files" : "casos", href: "/bureau" }, { label: title.length > 32 ? title.slice(0, 32) + "…" : title }, ]} /> - {/* Full-bleed editorial hero — only when a painterly illustration exists for this case. Other cases get the plain title header. */} diff --git a/web/app/h/[hypothesisId]/page.tsx b/web/app/h/[hypothesisId]/page.tsx index 8bb1187..d5d4dc6 100644 --- a/web/app/h/[hypothesisId]/page.tsx +++ b/web/app/h/[hypothesisId]/page.tsx @@ -16,7 +16,6 @@ import Link from "next/link"; import { readFile } from "node:fs/promises"; import path from "node:path"; import { pgQuery } from "@/lib/retrieval/db"; -import { AuthBar } from "@/components/auth-bar"; import { BureauNav } from "@/components/bureau-nav"; import { getLocale } from "@/components/locale-toggle"; import { RedTeamRequestButton } from "@/components/red-team-request-button"; @@ -204,7 +203,6 @@ export default async function HypothesisPage({ { label: "hypotheses", href: "/bureau#hypotheses" }, { label: hypothesisId }, ]} /> -
{/* Header */} diff --git a/web/app/jobs/[id]/page.tsx b/web/app/jobs/[id]/page.tsx index d7d0b15..29d07e3 100644 --- a/web/app/jobs/[id]/page.tsx +++ b/web/app/jobs/[id]/page.tsx @@ -17,7 +17,6 @@ import { notFound } from "next/navigation"; import Link from "next/link"; import { pgQuery } from "@/lib/retrieval/db"; -import { AuthBar } from "@/components/auth-bar"; import { BureauNav } from "@/components/bureau-nav"; import { getLocale } from "@/components/locale-toggle"; import { JobStatusPoller } from "@/components/job-status-poller"; @@ -83,7 +82,6 @@ export default async function JobPage({ { label: "jobs", href: "/bureau#jobs" }, { label: job.job_id.slice(0, 8) }, ]} /> -
diff --git a/web/app/layout.tsx b/web/app/layout.tsx index f8888e1..80086ae 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -2,7 +2,7 @@ import type { Metadata } from "next"; import { JetBrains_Mono, Inter, Fraunces } from "next/font/google"; import "./globals.css"; import { CommandPalette } from "@/components/command-palette"; -import { LocaleToggle, getLocale } from "@/components/locale-toggle"; +import { getLocale } from "@/components/locale-toggle"; const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); const mono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono" }); @@ -16,67 +16,90 @@ const fraunces = Fraunces({ const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://disclosure.top"; -export const metadata: Metadata = { - metadataBase: new URL(SITE_URL), - title: { - default: "The Disclosure Bureau — UAP/UFO desclassificado", - template: "%s · The Disclosure Bureau", - }, - description: - "122 documentos desclassificados do Departamento de Guerra dos EUA sobre UAP/UFO. " + - "Pilotos, oficiais e físicos relatam o que viram. Avistamentos, testemunhas, " + - "objetos catalogados — arquivos abertos da divulgação.", - keywords: [ - "UAP", "UFO", "ovni", "desclassificado", "war.gov", "Pentagon", - "Kenneth Arnold", "Mantell", "green fireballs", "Sandia", - "Project Blue Book", "Robertson Panel", "AATIP", - "documentos desclassificados", "avistamento", "testemunha", - "disclosure", "divulgação UFO", - ], - authors: [{ name: "The Disclosure Bureau" }], - openGraph: { - type: "website", - siteName: "The Disclosure Bureau", - title: "The Disclosure Bureau — UAP/UFO desclassificado", - description: - "122 documentos desclassificados. Pilotos, oficiais, físicos relatam o que viram.", - 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: "/", - languages: { "pt-BR": "/", "en-US": "/" }, - }, - robots: { - index: true, - follow: true, - googleBot: { +// Site-wide metadata is locale-aware: title, description, OG, twitter, html +// lang and openGraph.locale must all switch when the cookie toggles. Static +// `export const metadata` is resolved without cookies, so the title would +// stay pinned to whichever language built the page. generateMetadata() runs +// per request and can read the cookie via getLocale(). +export async function generateMetadata(): Promise { + const locale = await getLocale(); + const t = locale === "en" + ? { + title: "The Disclosure Bureau — declassified UAP/UFO", + template: "%s · The Disclosure Bureau", + desc: "122 declassified US Department of War documents on UAP/UFO. " + + "Pilots, officers and physicists describe what they saw. Sightings, " + + "witnesses, catalogued craft — the open files of disclosure.", + og_desc: "122 declassified documents. Pilots, officers, physicists describe what they saw.", + tw_desc: "Declassified UAP/UFO files, narrated from the public record.", + ogLocale: "en_US", + altLocale: "pt_BR", + } + : { + title: "The Disclosure Bureau — UAP/UFO desclassificado", + template: "%s · The Disclosure Bureau", + desc: "122 documentos desclassificados do Departamento de Guerra dos EUA sobre UAP/UFO. " + + "Pilotos, oficiais e físicos relatam o que viram. Avistamentos, testemunhas, " + + "objetos catalogados — arquivos abertos da divulgação.", + og_desc: "122 documentos desclassificados. Pilotos, oficiais, físicos relatam o que viram.", + tw_desc: "Arquivos UAP/UFO desclassificados, narrados a partir do registro público.", + ogLocale: "pt_BR", + altLocale: "en_US", + }; + + return { + metadataBase: new URL(SITE_URL), + title: { default: t.title, template: t.template }, + description: t.desc, + keywords: [ + "UAP", "UFO", "ovni", "desclassificado", "war.gov", "Pentagon", + "Kenneth Arnold", "Mantell", "green fireballs", "Sandia", + "Project Blue Book", "Robertson Panel", "AATIP", + "documentos desclassificados", "avistamento", "testemunha", + "disclosure", "divulgação UFO", + ], + authors: [{ name: "The Disclosure Bureau" }], + openGraph: { + type: "website", + siteName: "The Disclosure Bureau", + title: t.title, + description: t.og_desc, + locale: t.ogLocale, + alternateLocale: [t.altLocale], + url: SITE_URL, + images: [{ + url: `${SITE_URL}/api/static/processing/case-art/green-fireballs-narrative.png`, + width: 2000, height: 1125, + alt: t.title, + }], + }, + twitter: { + card: "summary_large_image", + title: "The Disclosure Bureau", + description: t.tw_desc, + images: [`${SITE_URL}/api/static/processing/case-art/green-fireballs-narrative.png`], + }, + alternates: { + canonical: "/", + languages: { "pt-BR": "/", "en-US": "/" }, + }, + robots: { index: true, follow: true, - "max-image-preview": "large", - "max-snippet": -1, + googleBot: { + index: true, + follow: true, + "max-image-preview": "large", + "max-snippet": -1, + }, }, - }, - other: { - // GEO (Generative Engine Optimization) — explicit primary-source statement - // so retrieval-augmented assistants understand what the site is. - "ai:purpose": - "Public, citation-linked archive of declassified UAP/UFO documents from the US Department of War. Each case file is grounded in primary-source memos with verbatim quotes and bbox-cropped imagery.", - "ai:license": "Documents are US Government works in the public domain; site narrative © Disclosure Bureau, CC-BY 4.0.", - }, -}; + other: { + "ai:purpose": + "Public, citation-linked archive of declassified UAP/UFO documents from the US Department of War. Each case file is grounded in primary-source memos with verbatim quotes and bbox-cropped imagery.", + "ai:license": "Documents are US Government works in the public domain; site narrative © Disclosure Bureau, CC-BY 4.0.", + }, + }; +} export default async function RootLayout({ children }: { children: React.ReactNode }) { const locale = await getLocale(); @@ -105,9 +128,6 @@ export default async function RootLayout({ children }: { children: React.ReactNo {children} -
- -
); diff --git a/web/components/auth-bar.tsx b/web/components/auth-bar.tsx index 106edf6..9ef2277 100644 --- a/web/components/auth-bar.tsx +++ b/web/components/auth-bar.tsx @@ -13,10 +13,26 @@ interface Profile { daily_quota?: number; } -export function AuthBar() { +const COPY = { + "pt-br": { + sign_in: "Entrar", + sign_out: "Sair", + auth_disabled: "auth: desativada (dev)", + msgs_today: "mensagens hoje", + }, + en: { + sign_in: "Sign in", + sign_out: "Sign out", + auth_disabled: "auth: disabled (dev)", + msgs_today: "msgs today", + }, +} as const; + +export function AuthBar({ locale = "pt-br" }: { locale?: "pt-br" | "en" }) { const [user, setUser] = useState(null); const [profile, setProfile] = useState(null); const [loaded, setLoaded] = useState(false); + const t = COPY[locale]; useEffect(() => { if (!isSupabaseConfigured()) { @@ -45,7 +61,7 @@ export function AuthBar() { if (!isSupabaseConfigured()) { return (
- auth: disabled (dev) + {t.auth_disabled}
); } @@ -54,10 +70,10 @@ export function AuthBar() { return ( - sign in + {t.sign_in} ); } @@ -69,17 +85,17 @@ export function AuthBar() {
{profile.display_name ?? user.email}
${(profile.total_cost_usd ?? 0).toFixed(2)} / ${(profile.budget_cap_usd ?? 0).toFixed(2)} ·{" "} - {profile.daily_used ?? 0}/{profile.daily_quota ?? 0} msgs today + {profile.daily_used ?? 0}/{profile.daily_quota ?? 0} {t.msgs_today}
)}
diff --git a/web/components/bureau-nav.tsx b/web/components/bureau-nav.tsx index 03e548d..dc09733 100644 --- a/web/components/bureau-nav.tsx +++ b/web/components/bureau-nav.tsx @@ -1,16 +1,18 @@ /** - * BureauNav — top navigation bar present on every bureau page. + * BureauNav — top navigation bar present on every bureau sub-page. * * Solves "I'm stuck on this page, there's no way back" UX. Shows: * - "← Home" (always → /) - * - "🔎 Bureau" (always → /bureau) + * - "Bureau" (always → /bureau) * - Breadcrumb trail of current page (passed via props) + * - Locale toggle + auth pill on the right * - * Server component. No client-side history needed because the parent links - * are deterministic (every bureau page has a known parent in the hierarchy). + * Visually harmonised with SiteHeader (single gold palette, no cyan); labels + * localised. */ import Link from "next/link"; -import { ArrowLeft, Home } from "lucide-react"; +import { ArrowLeft } from "lucide-react"; +import { AuthBar } from "./auth-bar"; import { LocaleToggle, type Locale } from "./locale-toggle"; export interface Crumb { @@ -20,49 +22,56 @@ export interface Crumb { href?: string; } -export function BureauNav({ crumbs, locale }: { crumbs: Crumb[]; locale?: Locale }) { +const COPY = { + "pt-br": { home: "home", bureau: "casos", aria_home: "Voltar para a home", aria_bureau: "Listagem de casos" }, + en: { home: "home", bureau: "case files", aria_home: "Back to home", aria_bureau: "Case file library" }, +} as const; + +export function BureauNav({ crumbs, locale = "pt-br" }: { crumbs: Crumb[]; locale?: Locale }) { + const t = COPY[locale]; return ( -