Skip to content

Commit c9c2511

Browse files
feat: cross-chapter navigation at section boundaries (#130) (#133)
When reaching the last section of a chapter, "Next →" now links to the first section of the next chapter (with "(Ch. N)" indicator). Similarly, the first section links backward to the previous chapter's last section. Implementation: - Pre-compute sorted chapter list per title in getStaticPaths - Pass prevChapterLast and nextChapterFirst as props - Fall back to cross-chapter link when at chapter boundary - Show "(Ch. N)" in nav label to signal chapter crossing Closes #130 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7e2a834 commit c9c2511

1 file changed

Lines changed: 42 additions & 7 deletions

File tree

apps/web/src/pages/statute/[...slug].astro

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,58 @@ export async function getStaticPaths() {
2828
});
2929
}
3030
31+
// Pre-compute sorted chapter list per title for cross-chapter nav
32+
const titleChapters = new Map<number, number[]>();
33+
for (const [key] of byTitleChapter) {
34+
const [titleStr, chapterStr] = key.split('-');
35+
const titleNum = parseInt(titleStr ?? '0', 10);
36+
const chapterNum = parseInt(chapterStr ?? '0', 10);
37+
if (!titleChapters.has(titleNum)) titleChapters.set(titleNum, []);
38+
titleChapters.get(titleNum)!.push(chapterNum);
39+
}
40+
for (const [, chapters] of titleChapters) {
41+
chapters.sort((a, b) => a - b);
42+
}
43+
3144
return entries.map((entry) => {
3245
const key = `${entry.data.usc_title}-${entry.data.chapter}`;
3346
const chapterSiblings = byTitleChapter.get(key) ?? [];
47+
const titleNum = entry.data.usc_title;
48+
const chapterNum = entry.data.chapter;
49+
const chapters = titleChapters.get(titleNum) ?? [];
50+
const chapterIdx = chapters.indexOf(chapterNum);
51+
52+
// Cross-chapter boundary links
53+
let prevChapterLast: typeof entry | null = null;
54+
let nextChapterFirst: typeof entry | null = null;
55+
56+
if (chapterIdx > 0) {
57+
const prevCh = chapters[chapterIdx - 1];
58+
const prevEntries = byTitleChapter.get(`${titleNum}-${prevCh}`) ?? [];
59+
prevChapterLast = prevEntries.length > 0 ? prevEntries[prevEntries.length - 1] ?? null : null;
60+
}
61+
if (chapterIdx < chapters.length - 1) {
62+
const nextCh = chapters[chapterIdx + 1];
63+
const nextEntries = byTitleChapter.get(`${titleNum}-${nextCh}`) ?? [];
64+
nextChapterFirst = nextEntries.length > 0 ? nextEntries[0] ?? null : null;
65+
}
66+
3467
return {
3568
params: { slug: entry.id },
36-
props: { entry, chapterSiblings },
69+
props: { entry, chapterSiblings, prevChapterLast, nextChapterFirst },
3770
};
3871
});
3972
}
4073
41-
const { entry, chapterSiblings } = Astro.props;
74+
const { entry, chapterSiblings, prevChapterLast, nextChapterFirst } = Astro.props;
4275
const { Content } = await render(entry);
4376
44-
// Compute prev/next section links
77+
// Compute prev/next section links (with cross-chapter fallback)
4578
const currentIdx = chapterSiblings.findIndex((s: typeof entry) => s.id === entry.id);
46-
const prevSection = currentIdx > 0 ? chapterSiblings[currentIdx - 1] : null;
47-
const nextSection = currentIdx < chapterSiblings.length - 1 ? chapterSiblings[currentIdx + 1] : null;
79+
const prevSection = currentIdx > 0 ? chapterSiblings[currentIdx - 1] : prevChapterLast;
80+
const nextSection = currentIdx < chapterSiblings.length - 1 ? chapterSiblings[currentIdx + 1] : nextChapterFirst;
81+
const prevIsCrossChapter = currentIdx === 0 && !!prevChapterLast;
82+
const nextIsCrossChapter = currentIdx === chapterSiblings.length - 1 && !!nextChapterFirst;
4883
4984
const { usc_title, usc_section, chapter, classification, current_through, generated_at } = entry.data;
5085
@@ -314,14 +349,14 @@ const readingTimeMin = Math.max(1, Math.round(wordCount / 200));
314349
<nav class="not-prose mt-8 flex items-center justify-between border-t border-gray-200 pt-4 font-sans dark:border-gray-800" aria-label="Section navigation">
315350
{prevSection ? (
316351
<a href={`${base}statute/${prevSection.id}/`} class="flex flex-col gap-0.5 rounded-lg px-3 py-2 transition-colors hover:bg-warm-gray dark:hover:bg-gray-800 max-w-[45%]">
317-
<span class="text-[11px] text-gray-400">&larr; Previous</span>
352+
<span class="text-[11px] text-gray-400">&larr; Previous{prevIsCrossChapter ? ` (Ch. ${prevSection.data.chapter})` : ''}</span>
318353
<span class="text-sm font-medium text-teal">&sect; {prevSection.data.usc_section}</span>
319354
<span class="truncate text-xs text-gray-500">{prevSection.data.title.replace(/^Section \S+ - /, '')}</span>
320355
</a>
321356
) : <span></span>}
322357
{nextSection ? (
323358
<a href={`${base}statute/${nextSection.id}/`} class="flex flex-col items-end gap-0.5 rounded-lg px-3 py-2 transition-colors hover:bg-warm-gray dark:hover:bg-gray-800 max-w-[45%] ml-auto text-right">
324-
<span class="text-[11px] text-gray-400">Next &rarr;</span>
359+
<span class="text-[11px] text-gray-400">Next{nextIsCrossChapter ? ` (Ch. ${nextSection.data.chapter})` : ''} &rarr;</span>
325360
<span class="text-sm font-medium text-teal">&sect; {nextSection.data.usc_section}</span>
326361
<span class="truncate text-xs text-gray-500">{nextSection.data.title.replace(/^Section \S+ - /, '')}</span>
327362
</a>

0 commit comments

Comments
 (0)