diff --git a/web/app/c/[slug]/page.tsx b/web/app/c/[slug]/page.tsx
index a8d2c85..c39ba57 100644
--- a/web/app/c/[slug]/page.tsx
+++ b/web/app/c/[slug]/page.tsx
@@ -121,9 +121,18 @@ export default async function CaseReportPage({
const locale = (await getLocale()) === "en" ? "en" : "pt-br";
const c = await loadCase(slug);
if (!c) notFound();
- const { fm, body } = c;
+ const { fm } = c;
- const title = locale === "pt-br" ? (fm.topic_pt_br ?? fm.topic ?? slug) : (fm.topic ?? slug);
+ // The narrator writes two H1s in the body: "#
" then
+ // "# ". Prefer those over the generic frontmatter topic
+ // (which is often an auto-derived "Doc 18 100754 …"). Pick by locale and
+ // strip BOTH leading H1s from the body so the headline isn't duplicated.
+ const h1s = [...c.body.matchAll(/^#\s+(.+)$/gm)].map((m) => m[1].trim());
+ const bodyTitle = locale === "pt-br" ? (h1s[1] ?? h1s[0]) : h1s[0];
+ // Strip H1 lines ("# Title") — the "## section" headers survive since the
+ // regex requires whitespace after a single #. Avoids duplicating the title.
+ const body = c.body.replace(/^#\s+.+$\n?/gm, "").replace(/^\s+/, "");
+ const title = bodyTitle ?? (locale === "pt-br" ? (fm.topic_pt_br ?? fm.topic ?? slug) : (fm.topic ?? slug));
const dateLabel = fm.created_at ? new Date(fm.created_at).toLocaleDateString(locale === "pt-br" ? "pt-BR" : "en-US", { day: "numeric", month: "long", year: "numeric" }) : null;
const canonical = `${SITE_URL}/c/${slug}`;
const lead = pickLead(body, locale).slice(0, 280);
diff --git a/web/components/case-library.tsx b/web/components/case-library.tsx
index edcd76a..3f8b8d2 100644
--- a/web/components/case-library.tsx
+++ b/web/components/case-library.tsx
@@ -93,10 +93,15 @@ async function loadCases(locale: "pt-br" | "en"): Promise {
const md = await readFile(full, "utf-8");
const st = await stat(full);
const { fm, body } = parseFrontmatter(md);
+ // Prefer the narrator's body H1 (magazine title) over the generic
+ // auto-derived frontmatter topic. Two H1s: EN then PT-BR.
+ const h1s = [...body.matchAll(/^#\s+(.+)$/gm)].map((m) => m[1].trim());
+ const enTitle = h1s[0] ?? fm.topic ?? f;
+ const ptTitle = h1s[1] ?? h1s[0] ?? fm.topic_pt_br ?? fm.topic ?? f;
items.push({
slug: f.replace(/\.md$/, ""),
- topic: fm.topic ?? f,
- topic_pt_br: fm.topic_pt_br ?? null,
+ topic: enTitle,
+ topic_pt_br: ptTitle,
opening: pickOpening(body, locale),
mtimeMs: st.mtimeMs,
});
diff --git a/web/components/featured-case.tsx b/web/components/featured-case.tsx
index 601a487..d8fd5d9 100644
--- a/web/components/featured-case.tsx
+++ b/web/components/featured-case.tsx
@@ -81,10 +81,11 @@ async function loadFeatured(locale: "pt-br" | "en"): Promise m[1].trim());
items.push({
slug,
- topic: fm.topic ?? f,
- topic_pt_br: fm.topic_pt_br ?? null,
+ topic: h1s[0] ?? fm.topic ?? f,
+ topic_pt_br: h1s[1] ?? h1s[0] ?? fm.topic_pt_br ?? null,
opening: pickOpening(body, locale),
hero_doc_id: docRef?.doc_id ?? null,
hero_page: docRef?.page ?? null,
@@ -92,6 +93,11 @@ async function loadFeatured(locale: "pt-br" | "en"): Promise i.slug === FLAGSHIP);
+ if (flagship) return flagship;
items.sort((a, b) => b.mtimeMs - a.mtimeMs);
return items[0] ?? null;
} catch {