Sezione Campus: layout dedicato, route protette e documentazione

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 15:40:07 +02:00
parent 7baebebc09
commit c4107b84a5
15 changed files with 565 additions and 52 deletions
+94
View File
@@ -0,0 +1,94 @@
// Accesso ai contenuti della sezione Campus, generati da `npm run sync-campus`
// in campus-content/. I file sono letti a runtime (non a build time) e tenuti
// in cache in memoria: cambiano solo con un nuovo deploy.
import { readFileSync, existsSync } from 'node:fs';
import { join, normalize, resolve } from 'node:path';
export interface CampusLink { slug: string; area: string; title: string }
export interface CampusLesson {
slug: string;
kind: string;
label: string;
titleFull: string;
titleShort: string;
relatore: string;
concetti: string[];
chapter: number;
area: string;
prev: CampusLink | null;
next: CampusLink | null;
}
export interface CampusChapter {
num: number;
title: string;
sub: string;
color: string;
lessons: CampusLesson[];
}
export interface CampusArea {
slug: string;
title: string;
sub: string;
color: string;
chapters: CampusChapter[];
}
export interface CampusNav { areas: CampusArea[]; count: number }
const CONTENT_DIR = resolve(process.env.CAMPUS_DIR ?? './campus-content');
let navCache: CampusNav | null = null;
/** Struttura della sezione, o null se i contenuti non sono stati generati. */
export function getNav(): CampusNav | null {
if (navCache) return navCache;
const file = join(CONTENT_DIR, 'nav.json');
if (!existsSync(file)) {
console.warn(`Campus: contenuti assenti in ${CONTENT_DIR}. Eseguire "npm run sync-campus".`);
return null;
}
navCache = JSON.parse(readFileSync(file, 'utf8')) as CampusNav;
return navCache;
}
export function findArea(nav: CampusNav, slug: string): CampusArea | null {
return nav.areas.find((a) => a.slug === slug) ?? null;
}
export function findLesson(nav: CampusNav, slug: string): CampusLesson | null {
for (const area of nav.areas)
for (const ch of area.chapters) {
const l = ch.lessons.find((x) => x.slug === slug);
if (l) return l;
}
return null;
}
export function chapterOf(nav: CampusNav, lesson: CampusLesson): CampusChapter | null {
for (const area of nav.areas) {
const ch = area.chapters.find((c) => c.num === lesson.chapter);
if (ch) return ch;
}
return null;
}
/** Corpo HTML di una guida; null se il frammento non esiste. */
export function readLessonHtml(slug: string): string | null {
const file = join(CONTENT_DIR, 'pages', `${slug}.html`);
if (!existsSync(file)) return null;
return readFileSync(file, 'utf8');
}
/**
* Percorso assoluto di un asset dentro campus-content/assets, oppure null se
* il path esce dalla cartella (traversal) o il file non esiste.
*/
export function resolveAsset(relPath: string): string | null {
const base = join(CONTENT_DIR, 'assets');
const full = resolve(base, normalize(relPath));
if (full !== base && !full.startsWith(base + '/')) return null;
return existsSync(full) ? full : null;
}