feat(glossary): add step 21 smart navigation
All checks were successful
SMOKE / smoke (push) Successful in 13s
CI / build-and-anchors (push) Successful in 1m4s
CI / build-and-anchors (pull_request) Successful in 35s

This commit is contained in:
2026-04-26 13:03:45 +02:00
parent 689619d14d
commit 9f88112aca
45 changed files with 1020 additions and 112 deletions

View File

@@ -1,4 +1,5 @@
import type { CollectionEntry } from "astro:content";
import { GLOSSARY_NAV_DEFAULTS } from "./glossary-navigation-defaults";
export type GlossaryEntry = CollectionEntry<"glossaire">;
@@ -16,6 +17,32 @@ export type GlossaryRelationBlock = GlossaryRelationSection & {
className: string;
};
export type GlossarySmartNavigationPathKey =
| "understand"
| "deepen"
| "compare"
| "apply";
export type GlossarySmartNavigationPath = {
key: GlossarySmartNavigationPathKey;
label: string;
entries: GlossaryEntry[];
};
export type GlossarySmartNavigationFlow = {
key: string;
label: string;
primaryNext?: GlossaryEntry;
primaryReason?: string;
};
export type GlossarySmartNavigation = {
primaryNext?: GlossaryEntry;
primaryReason?: string;
paths: GlossarySmartNavigationPath[];
flows: GlossarySmartNavigationFlow[];
};
export type GlossaryHomeStats = {
totalEntries: number;
paradigmesCount: number;
@@ -122,6 +149,16 @@ export const FAMILY_SECTION_TITLES: Record<string, string> = {
epistemologie: "Outillage épistémologique",
};
export const SMART_NAV_PATH_LABELS: Record<
GlossarySmartNavigationPathKey,
string
> = {
understand: "Comprendre",
deepen: "Approfondir",
compare: "Comparer",
apply: "Appliquer",
};
const PREFERRED_PARADIGME_SLUGS = [
"gouvernementalite",
"gouvernementalite-algorithmique",
@@ -202,35 +239,35 @@ export function uniqueGlossaryEntries(
}
export function slugsOfGlossaryEntries(
entries: GlossaryEntry[] = [],
): Set<string> {
const slugs = new Set<string>();
for (const entry of entries) {
const slug = slugOfGlossaryEntry(entry);
if (!slug) continue;
slugs.add(slug);
}
return slugs;
}
export function excludeGlossaryEntries(
entries: GlossaryEntry[] = [],
excluded: Iterable<string> = [],
): GlossaryEntry[] {
const excludedSlugs = new Set(
Array.from(excluded)
.map((value) => normalizeGlossarySlug(value))
.filter(Boolean),
);
return entries.filter((entry) => {
const slug = slugOfGlossaryEntry(entry);
return Boolean(slug) && !excludedSlugs.has(slug);
});
entries: GlossaryEntry[] = [],
): Set<string> {
const slugs = new Set<string>();
for (const entry of entries) {
const slug = slugOfGlossaryEntry(entry);
if (!slug) continue;
slugs.add(slug);
}
return slugs;
}
export function excludeGlossaryEntries(
entries: GlossaryEntry[] = [],
excluded: Iterable<string> = [],
): GlossaryEntry[] {
const excludedSlugs = new Set(
Array.from(excluded)
.map((value) => normalizeGlossarySlug(value))
.filter(Boolean),
);
return entries.filter((entry) => {
const slug = slugOfGlossaryEntry(entry);
return Boolean(slug) && !excludedSlugs.has(slug);
});
}
export function resolveGlossaryEntriesInSourceOrder(
slugs: string[] = [],
allEntries: GlossaryEntry[] = [],
@@ -467,6 +504,96 @@ export function getRelationBlocks(
].filter((block) => block.items.length > 0);
}
export function getGlossarySmartNavigation(
currentEntry: GlossaryEntry,
allEntries: GlossaryEntry[] = [],
): GlossarySmartNavigation {
const currentSlug = slugOfGlossaryEntry(currentEntry);
const rawNavigation = currentEntry.data.navigation;
const defaultNavigation = GLOSSARY_NAV_DEFAULTS[familyOf(currentEntry)];
const navigationSource = rawNavigation ?? {
primaryNext: undefined,
primaryReason: undefined,
paths: defaultNavigation ?? {},
flows: {},
};
const primaryNext = resolveGlossaryEntriesInSourceOrder(
navigationSource.primaryNext ? [navigationSource.primaryNext] : [],
allEntries,
).find((entry) => slugOfGlossaryEntry(entry) !== currentSlug);
const pathKeys: GlossarySmartNavigationPathKey[] = [
"understand",
"deepen",
"compare",
"apply",
];
const paths = pathKeys
.map((key) => {
const entries = resolveGlossaryEntriesInSourceOrder(
navigationSource.paths?.[key] ?? [],
allEntries,
).filter((entry) => slugOfGlossaryEntry(entry) !== currentSlug);
return {
key,
label: SMART_NAV_PATH_LABELS[key],
entries,
};
})
.filter((path) => path.entries.length > 0);
const flows = Object.entries(navigationSource.flows ?? {})
.map(([key, flow]) => {
const primaryNext = resolveGlossaryEntriesInSourceOrder(
flow.primaryNext ? [flow.primaryNext] : [],
allEntries,
).find((entry) => slugOfGlossaryEntry(entry) !== currentSlug);
return {
key,
label: flow.label,
primaryNext,
primaryReason: flow.primaryReason,
};
})
.filter((flow) => flow.primaryNext);
if (primaryNext || paths.length > 0 || flows.length > 0) {
return {
primaryNext,
primaryReason: navigationSource.primaryReason,
paths,
flows,
};
}
const fallbackEntries = uniqueGlossaryEntries([
...resolveGlossaryEntriesInSourceOrder(
currentEntry.data.related ?? [],
allEntries,
),
...resolveGlossaryEntriesInSourceOrder(
currentEntry.data.seeAlso ?? [],
allEntries,
),
]).filter((entry) => slugOfGlossaryEntry(entry) !== currentSlug);
const fallbackPrimary = fallbackEntries[0];
return {
primaryNext: fallbackPrimary,
primaryReason: fallbackPrimary
? "Ce lien prolonge directement les relations conceptuelles de cette fiche."
: undefined,
paths: [],
flows: [],
};
}
export function getRelationSections(
entry: GlossaryEntry,
allEntries: GlossaryEntry[] = [],