fix(chapters): narrow optional section fields in page frontmatter

The chapters schema marks section.eyebrow / panelLabel / panelCode /
steps / copy as optional, but pages /agents/, /models/, /skills/
consume them — index access without narrowing failed astro check with
ts(18048). Same for SkillPackageExplorer: aria-selected was String(bool)
which widened to plain string and failed ts(2322) against
ButtonHTMLAttributes.

Fix: guard each required field with a helper that throws a clear
message on missing data, then read .en / .map from the narrowed value.
Page rendering is unchanged — dist/ HTML for all four files is
byte-identical before and after.

No any / as any / ! / @ts-ignore. Schema and tsconfig untouched.
This commit is contained in:
Marcos Paulo
2026-09-05 17:41:38 +00:00
parent 73d1b62989
commit c19e77bcac
4 changed files with 91 additions and 15 deletions
+32 -5
View File
@@ -7,8 +7,22 @@ import { getEntry } from 'astro:content';
import ChapterLayout from '../layouts/ChapterLayout.astro';
import ChapterHero from '../components/blocks/ChapterHero.astro';
// Required-field guard. The chapters schema marks section eyebrow /
// panelLabel / panelCode / steps / copy as optional because the schema
// does not know which page consumes which shape. These four pages do
// consume them — fail loudly here rather than rendering a blank section.
function requireField<T>(value: T | undefined, name: string): T {
if (value === undefined) {
throw new Error(`agents chapter: missing required field "${name}"`);
}
return value;
}
const base = import.meta.env.BASE_URL;
const chapter = await getEntry('chapters', 'agents');
if (!chapter) {
throw new Error('agents chapter: missing collection entry');
}
const lede = chapter.data.lede.en;
const title = chapter.data.title.en;
const eyebrow = chapter.data.eyebrow.en;
@@ -16,6 +30,19 @@ const cards = chapter.data.cards ?? [];
const sections = chapter.data.sections ?? [];
const treeSection = sections[0];
const handoffSection = sections[1];
if (!treeSection) {
throw new Error('agents chapter: missing sections[0]');
}
if (!handoffSection) {
throw new Error('agents chapter: missing sections[1]');
}
// Narrow the section fields this page renders. Each `requireField` either
// returns a non-null value or throws — TS narrows from `T | undefined` to `T`.
const treeEyebrow = requireField(treeSection.eyebrow, 'sections[0].eyebrow');
const treePanelLabel = requireField(treeSection.panelLabel, 'sections[0].panelLabel');
const treePanelCode = requireField(treeSection.panelCode, 'sections[0].panelCode');
const handoffEyebrow = requireField(handoffSection.eyebrow, 'sections[1].eyebrow');
const handoffSteps = requireField(handoffSection.steps, 'sections[1].steps');
---
<ChapterLayout title="AI For Dummies — Agents and trees" description={lede.replace(/<[^>]+>/g, '')}>
@@ -30,12 +57,12 @@ const handoffSection = sections[1];
<section class="pipeline">
<div>
<p class="eyebrow">{treeSection.eyebrow.en}</p>
<p class="eyebrow">{treeEyebrow.en}</p>
<h2 set:html={treeSection.title.en} />
</div>
<div class="panel">
<strong>{treeSection.panelLabel.en}</strong>
<code>{treeSection.panelCode.en}</code>
<strong>{treePanelLabel.en}</strong>
<code>{treePanelCode.en}</code>
</div>
</section>
@@ -53,12 +80,12 @@ const handoffSection = sections[1];
<section class="practice">
<div>
<p class="eyebrow">{handoffSection.eyebrow.en}</p>
<p class="eyebrow">{handoffEyebrow.en}</p>
<h2 set:html={handoffSection.title.en} />
</div>
<div class="steps">
{
handoffSection.steps.map((step, index) => (
handoffSteps.map((step, index) => (
<article>
<b>{String(index + 1).padStart(2, '0')}</b>
<div>