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:
@@ -71,7 +71,7 @@ const packageFiles: PackageFile[] = [
|
||||
class:list={[{ active: index === 0 }]}
|
||||
data-skill-file={file.id}
|
||||
role="tab"
|
||||
aria-selected={String(index === 0)}
|
||||
aria-selected={index === 0 ? 'true' : 'false'}
|
||||
>
|
||||
<code>{`${file.prefix}${file.label}`}</code>
|
||||
<small>{file.caption}</small>
|
||||
|
||||
+32
-5
@@ -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>
|
||||
|
||||
+30
-5
@@ -8,8 +8,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(`models chapter: missing required field "${name}"`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const base = import.meta.env.BASE_URL;
|
||||
const chapter = await getEntry('chapters', 'models');
|
||||
if (!chapter) {
|
||||
throw new Error('models chapter: missing collection entry');
|
||||
}
|
||||
const lede = chapter.data.lede.en;
|
||||
const title = chapter.data.title.en;
|
||||
const eyebrow = chapter.data.eyebrow.en;
|
||||
@@ -18,6 +32,17 @@ const sections = chapter.data.sections ?? [];
|
||||
// First section carries the routing-rule panel, second carries the steps.
|
||||
const ruleSection = sections[0];
|
||||
const sequenceSection = sections[1];
|
||||
if (!ruleSection) {
|
||||
throw new Error('models chapter: missing sections[0]');
|
||||
}
|
||||
if (!sequenceSection) {
|
||||
throw new Error('models chapter: missing sections[1]');
|
||||
}
|
||||
const ruleEyebrow = requireField(ruleSection.eyebrow, 'sections[0].eyebrow');
|
||||
const rulePanelLabel = requireField(ruleSection.panelLabel, 'sections[0].panelLabel');
|
||||
const rulePanelCode = requireField(ruleSection.panelCode, 'sections[0].panelCode');
|
||||
const sequenceEyebrow = requireField(sequenceSection.eyebrow, 'sections[1].eyebrow');
|
||||
const sequenceSteps = requireField(sequenceSection.steps, 'sections[1].steps');
|
||||
---
|
||||
|
||||
<ChapterLayout title="AI For Dummies — Models" description={lede.replace(/<[^>]+>/g, '')}>
|
||||
@@ -44,23 +69,23 @@ const sequenceSection = sections[1];
|
||||
|
||||
<section class="model">
|
||||
<div>
|
||||
<p class="eyebrow">{ruleSection.eyebrow.en}</p>
|
||||
<p class="eyebrow">{ruleEyebrow.en}</p>
|
||||
<h2 set:html={ruleSection.title.en} />
|
||||
</div>
|
||||
<div class="panel">
|
||||
<strong>{ruleSection.panelLabel.en}</strong>
|
||||
<code>{ruleSection.panelCode.en}</code>
|
||||
<strong>{rulePanelLabel.en}</strong>
|
||||
<code>{rulePanelCode.en}</code>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="practice">
|
||||
<div>
|
||||
<p class="eyebrow">{sequenceSection.eyebrow.en}</p>
|
||||
<p class="eyebrow">{sequenceEyebrow.en}</p>
|
||||
<h2 set:html={sequenceSection.title.en} />
|
||||
</div>
|
||||
<div class="steps">
|
||||
{
|
||||
sequenceSection.steps.map((step, index) => (
|
||||
sequenceSteps.map((step, index) => (
|
||||
<article>
|
||||
<b>{String(index + 1).padStart(2, '0')}</b>
|
||||
<div>
|
||||
|
||||
+28
-4
@@ -10,14 +10,38 @@ import ChapterHero from '../components/blocks/ChapterHero.astro';
|
||||
import SkillPackageExplorer from '../components/islands/SkillPackageExplorer.astro';
|
||||
import skillsStylesheet from '../../skills/styles.css?url';
|
||||
|
||||
// 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(`skills chapter: missing required field "${name}"`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const base = import.meta.env.BASE_URL;
|
||||
const chapter = await getEntry('chapters', 'skills');
|
||||
if (!chapter) {
|
||||
throw new Error('skills chapter: missing collection entry');
|
||||
}
|
||||
const lede = chapter.data.lede.en;
|
||||
const title = chapter.data.title.en;
|
||||
const eyebrow = chapter.data.eyebrow.en;
|
||||
const sections = chapter.data.sections ?? [];
|
||||
const anatomySection = sections[0];
|
||||
const createSection = sections[1];
|
||||
if (!anatomySection) {
|
||||
throw new Error('skills chapter: missing sections[0]');
|
||||
}
|
||||
if (!createSection) {
|
||||
throw new Error('skills chapter: missing sections[1]');
|
||||
}
|
||||
const anatomyEyebrow = requireField(anatomySection.eyebrow, 'sections[0].eyebrow');
|
||||
const anatomyCopy = requireField(anatomySection.copy, 'sections[0].copy');
|
||||
const createEyebrow = requireField(createSection.eyebrow, 'sections[1].eyebrow');
|
||||
const createSteps = requireField(createSection.steps, 'sections[1].steps');
|
||||
---
|
||||
|
||||
<ChapterLayout title="AI For Dummies — Skills" description={lede.replace(/<[^>]+>/g, '')}>
|
||||
@@ -33,21 +57,21 @@ const createSection = sections[1];
|
||||
|
||||
<section class="pipeline package-anatomy">
|
||||
<div>
|
||||
<p class="eyebrow">{anatomySection.eyebrow.en}</p>
|
||||
<p class="eyebrow">{anatomyEyebrow.en}</p>
|
||||
<h2 set:html={anatomySection.title.en} />
|
||||
<p class="package-hint">{anatomySection.copy.en}</p>
|
||||
<p class="package-hint">{anatomyCopy.en}</p>
|
||||
</div>
|
||||
<SkillPackageExplorer />
|
||||
</section>
|
||||
|
||||
<section class="practice">
|
||||
<div>
|
||||
<p class="eyebrow">{createSection.eyebrow.en}</p>
|
||||
<p class="eyebrow">{createEyebrow.en}</p>
|
||||
<h2 set:html={createSection.title.en} />
|
||||
</div>
|
||||
<div class="steps">
|
||||
{
|
||||
createSection.steps.map((step, index) => (
|
||||
createSteps.map((step, index) => (
|
||||
<article>
|
||||
<b>{String(index + 1).padStart(2, '0')}</b>
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user