9.6 KiB
Front-matter Enrichment & Quick Links
Vue d'ensemble
Cette fonctionnalité ajoute deux améliorations majeures à ObsiViewer :
-
Enrichissement automatique de la front-matter YAML : À chaque ouverture d'un fichier Markdown, le système valide et enrichit automatiquement la front-matter avec des propriétés standardisées.
-
Quick Links avec filtres : Ajout de liens rapides (Favorites, Templates, Tasks) dans le sidebar qui permettent de filtrer les notes selon leurs propriétés.
Architecture
Backend
1. Enrichissement de la front-matter (server/ensureFrontmatter.mjs)
Responsabilités :
- Valider et enrichir la front-matter YAML des fichiers Markdown
- Garantir l'idempotence (pas de modification sur les fichiers déjà conformes)
- Préserver les propriétés existantes
- Maintenir l'ordre strict des clés
Propriétés garanties (ordre exact) :
---
titre: <nom du fichier sans .md>
auteur: Bruno Charest
creation_date: <ISO 8601 avec timezone America/Toronto>
modification_date: <ISO 8601 avec timezone America/Toronto>
catégorie: ""
tags: []
aliases: []
status: en-cours
publish: false
favoris: false
template: false
task: false
archive: false
draft: false
private: false
---
Caractéristiques techniques :
- Utilisation de la librairie
yamlpour préserver les types et l'ordre - Mutex en mémoire pour éviter les écritures concurrentes
- Écriture atomique (temp file + rename) pour éviter la corruption
- Format de date : ISO 8601 avec offset
-04:00(America/Toronto)
Exemple d'utilisation :
import { enrichFrontmatterOnOpen } from './ensureFrontmatter.mjs';
const result = await enrichFrontmatterOnOpen('/path/to/note.md');
if (result.modified) {
console.log('File was enriched');
// Trigger reindex in Meilisearch
}
2. Intégration dans l'endpoint GET /api/files
Le middleware d'enrichissement est appelé automatiquement lors de la lecture d'un fichier Markdown :
app.get('/api/files', async (req, res) => {
// ... validation du path ...
if (!isExcalidraw && ext === '.md') {
const enrichResult = await enrichFrontmatterOnOpen(abs);
if (enrichResult.modified) {
// Trigger Meilisearch reindex
upsertFile(abs).catch(err => console.warn(err));
}
return res.send(enrichResult.content);
}
// ... reste du code ...
});
3. Indexation Meilisearch
Les propriétés booléennes favoris, template et task sont extraites et indexées pour permettre un filtrage rapide :
Fichier : server/meilisearch-indexer.mjs
return {
id: safeId,
path: rel,
// ... autres propriétés ...
favoris: fm.favoris === true,
template: fm.template === true,
task: fm.task === true
};
Configuration Meilisearch : server/meilisearch.client.mjs
filterableAttributes: [
'tags',
'file',
'path',
'parentDirs',
'properties.*',
'year',
'month',
'favoris', // ← Nouveau
'template', // ← Nouveau
'task' // ← Nouveau
]
4. Endpoint des compteurs
Route : GET /api/quick-links/counts
Retourne les compteurs pour chaque type de Quick Link :
{
"favorites": 12,
"templates": 5,
"tasks": 8
}
Implémentation :
app.get('/api/quick-links/counts', async (req, res) => {
const [favoritesResult, templatesResult, tasksResult] = await Promise.all([
index.search('', { filter: 'favoris = true', limit: 0 }),
index.search('', { filter: 'template = true', limit: 0 }),
index.search('', { filter: 'task = true', limit: 0 })
]);
res.json({
favorites: favoritesResult.estimatedTotalHits || 0,
templates: templatesResult.estimatedTotalHits || 0,
tasks: tasksResult.estimatedTotalHits || 0
});
});
Frontend
1. Composant Quick Links (src/app/features/quick-links/quick-links.component.ts)
Améliorations :
- Chargement dynamique des compteurs depuis l'API
- Affichage des compteurs à côté de chaque lien
- Mise à jour automatique au chargement du composant
Template :
<li>
<button (click)="select('favorites')" class="...">
<span class="flex items-center gap-2">
<span>❤️</span>
<span>Favorites</span>
</span>
<span *ngIf="counts" class="text-xs text-gray-500 font-medium">
({{ counts.favorites }})
</span>
</button>
</li>
2. Gestion des filtres (src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts)
Nouvelle propriété :
quickLinkFilter: 'favoris' | 'template' | 'task' | null = null;
Gestion des clics :
onQuickLink(_id: string) {
if (_id === 'favorites') {
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = 'favoris';
this.listQuery = '';
// Switch to list view on mobile/tablet
}
// ... similaire pour templates et tasks ...
}
3. Filtrage dans la liste (src/app/features/list/notes-list.component.ts)
Ajout du filtre :
quickLinkFilter = input<'favoris' | 'template' | 'task' | null>(null);
filtered = computed(() => {
// ... autres filtres ...
if (quickLink) {
list = list.filter(n => {
const props = (n as any).properties || {};
return props[quickLink] === true;
});
}
return [...list].sort((a, b) => (score(b) - score(a)));
});
Tests
Tests unitaires (server/ensureFrontmatter.test.mjs)
Exécution :
node server/ensureFrontmatter.test.mjs
Couverture :
- ✓ Ajout de front-matter sur fichier vierge
- ✓ Idempotence (pas de modification sur 2e passage)
- ✓ Préservation des propriétés existantes
- ✓ Ordre correct des clés
- ✓ Absence de lignes vides dans la front-matter
- ✓ Types booléens corrects
- ✓ Types tableau corrects pour tags/aliases
- ✓ Format de date ISO 8601 avec timezone
Tests e2e (e2e/frontmatter-quicklinks.spec.ts)
Exécution :
npm run test:e2e
Couverture :
- ✓ Enrichissement automatique à l'ouverture
- ✓ Affichage des compteurs dans Quick Links
- ✓ Filtrage par Favorites
- ✓ Filtrage par Templates
- ✓ Filtrage par Tasks
- ✓ Réinitialisation des filtres avec "All pages"
- ✓ Idempotence sur ouvertures multiples
Migration
Réindexation Meilisearch
Après déploiement, il est nécessaire de réindexer pour ajouter les nouveaux champs filtrables :
npm run meili:reindex
Cette commande :
- Reconfigure l'index avec les nouveaux
filterableAttributes - Réindexe tous les fichiers Markdown
- Extrait les propriétés
favoris,template,taskde chaque fichier
Enrichissement des fichiers existants
Les fichiers existants seront enrichis automatiquement lors de leur première ouverture. Pour enrichir tous les fichiers d'un coup, créer un script :
// scripts/enrich-all-notes.mjs
import { enrichFrontmatterOnOpen } from '../server/ensureFrontmatter.mjs';
import fg from 'fast-glob';
const files = await fg(['**/*.md'], { cwd: './vault', absolute: true });
for (const file of files) {
const result = await enrichFrontmatterOnOpen(file);
if (result.modified) {
console.log(`Enriched: ${file}`);
}
}
Cas limites gérés
- Fichier sans front-matter → Création du bloc complet
- Fichier avec front-matter partielle → Complétion sans écraser l'existant
- Propriétés custom → Préservées et placées après les propriétés requises
- Tags/aliases en chaîne → Normalisés en tableau
- Disque sans birthtime → Fallback sur ctime puis mtime
- Concurrence → Mutex en mémoire + écriture atomique
- Fichiers volumineux → Pas de réécriture si déjà conforme
Performance
- Enrichissement : ~5-10ms par fichier (lecture + parsing + écriture)
- Compteurs Quick Links : ~50-100ms (3 requêtes Meilisearch en parallèle)
- Filtrage : Instantané (index Meilisearch)
- Idempotence : Aucun coût sur fichiers déjà conformes (détection rapide)
Dépendances ajoutées
{
"dependencies": {
"yaml": "^2.x.x"
}
}
La librairie yaml est préférée à js-yaml car elle :
- Préserve l'ordre des clés
- Maintient les types natifs (booléens, tableaux)
- Supporte les commentaires YAML
- Offre un contrôle fin sur la sérialisation
Maintenance
Ajouter une nouvelle propriété requise
- Modifier
server/ensureFrontmatter.mjs:
const requiredProps = [
// ... propriétés existantes ...
['nouvelle_prop', 'valeur_par_defaut'],
];
- Mettre à jour les tests unitaires
- Réindexer Meilisearch si la propriété doit être filtrable
Modifier l'ordre des propriétés
Modifier simplement l'ordre dans le tableau requiredProps. L'enrichissement respectera automatiquement le nouvel ordre.
Troubleshooting
Les compteurs Quick Links sont à zéro
Cause : Meilisearch n'a pas été réindexé avec les nouveaux champs.
Solution :
npm run meili:reindex
Un fichier n'est pas enrichi
Cause possible : Le fichier n'a pas été ouvert depuis le déploiement.
Solution : Ouvrir le fichier dans l'interface, ou utiliser le script d'enrichissement global.
Les dates ne sont pas au bon fuseau horaire
Cause : Le serveur utilise un fuseau différent.
Solution : Modifier la constante TZ_OFFSET dans ensureFrontmatter.mjs.
Roadmap
- Support de fuseaux horaires configurables
- Interface d'édition de front-matter dans l'UI
- Validation de schéma YAML personnalisable
- Export/import de configurations de front-matter
- Statistiques d'utilisation des Quick Links
Auteur
Bruno Charest - Implémentation complète (backend + frontend + tests)
Date : Octobre 2025