87 lines
2.8 KiB
TypeScript
87 lines
2.8 KiB
TypeScript
// Chargement des actualités depuis content/actualites/*.md (éditées via Sveltia CMS).
|
|||
|
|
// $lib/server → garanti côté serveur : `marked` ne part jamais dans le bundle client.
|
||
|
|
// Le site étant prérendu, ce code s'exécute au build ; les pages sont ensuite 100 % statiques.
|
||
|
|
import { marked } from 'marked';
|
||
|
|
|
||
|
|
const files = import.meta.glob('/content/actualites/*.md', {
|
||
|
|
query: '?raw',
|
||
|
|
import: 'default',
|
||
|
|
eager: true
|
||
|
|
}) as Record<string, string>;
|
||
|
|
|
||
|
|
export interface Actualite {
|
||
|
|
slug: string;
|
||
|
|
titre: string;
|
||
|
|
date: string; // affichage libre (ex. « 26 mai 2026 »)
|
||
|
|
iso: string; // ISO pour <time datetime> / schema
|
||
|
|
chapeau: string;
|
||
|
|
couverture?: string;
|
||
|
|
couvertureAlt?: string;
|
||
|
|
top: boolean;
|
||
|
|
html: string; // corps rendu (first-party, Sveltia)
|
||
|
|
texte: string; // corps brut (pour description/estimation de lecture)
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Parse un front-matter YAML simple (clés scalaires, valeurs entre guillemets optionnelles). */
|
||
|
|
function parseFrontmatter(raw: string): { data: Record<string, string>; body: string } {
|
||
|
|
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
||
|
|
if (!m) return { data: {}, body: raw };
|
||
|
|
const data: Record<string, string> = {};
|
||
|
|
for (const line of m[1].split(/\r?\n/)) {
|
||
|
|
const kv = line.match(/^([A-Za-z0-9_]+)\s*:\s*(.*)$/);
|
||
|
|
if (!kv) continue;
|
||
|
|
let val = kv[2].trim();
|
||
|
|
if (
|
||
|
|
(val.startsWith('"') && val.endsWith('"')) ||
|
||
|
|
(val.startsWith("'") && val.endsWith("'"))
|
||
|
|
) {
|
||
|
|
val = val.slice(1, -1);
|
||
|
|
}
|
||
|
|
data[kv[1]] = val;
|
||
|
|
}
|
||
|
|
return { data, body: m[2] };
|
||
|
|
}
|
||
|
|
|
||
|
|
function frToIso(date: string): string {
|
||
|
|
// Accepte déjà l'ISO ; sinon renvoie tel quel (Sveltia écrit du YYYY-MM-DD).
|
||
|
|
return /^\d{4}-\d{2}-\d{2}/.test(date) ? date : date;
|
||
|
|
}
|
||
|
|
|
||
|
|
function frDisplay(iso: string): string {
|
||
|
|
const d = new Date(iso);
|
||
|
|
if (Number.isNaN(d.getTime())) return iso;
|
||
|
|
return d.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' });
|
||
|
|
}
|
||
|
|
|
||
|
|
let cache: Actualite[] | null = null;
|
||
|
|
|
||
|
|
export function getActualites(): Actualite[] {
|
||
|
|
if (cache) return cache;
|
||
|
|
const out: Actualite[] = [];
|
||
|
|
for (const [path, raw] of Object.entries(files)) {
|
||
|
|
const slug = path.split('/').pop()!.replace(/\.md$/, '');
|
||
|
|
const { data, body } = parseFrontmatter(raw);
|
||
|
|
if (!data.titre) continue;
|
||
|
|
const iso = frToIso(data.date ?? '');
|
||
|
|
out.push({
|
||
|
|
slug,
|
||
|
|
titre: data.titre,
|
||
|
|
iso,
|
||
|
|
date: data.date ? frDisplay(iso) : '',
|
||
|
|
chapeau: data.chapeau ?? '',
|
||
|
|
couverture: data.couverture || undefined,
|
||
|
|
couvertureAlt: data.couvertureAlt || data.titre,
|
||
|
|
top: data.top === 'true',
|
||
|
|
html: marked.parse(body.trim(), { async: false }) as string,
|
||
|
|
texte: body.trim()
|
||
|
|
});
|
||
|
|
}
|
||
|
|
out.sort((a, b) => (a.iso < b.iso ? 1 : a.iso > b.iso ? -1 : 0));
|
||
|
|
cache = out;
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getActualite(slug: string): Actualite | undefined {
|
||
|
|
return getActualites().find((a) => a.slug === slug);
|
||
|
|
}
|