ObsiViewer/docs/FRONTMATTER_QUICKLINKS.md

9.6 KiB

Front-matter Enrichment & Quick Links

Vue d'ensemble

Cette fonctionnalité ajoute deux améliorations majeures à ObsiViewer :

  1. 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.

  2. 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 yaml pour 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

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 :

  1. Reconfigure l'index avec les nouveaux filterableAttributes
  2. Réindexe tous les fichiers Markdown
  3. Extrait les propriétés favoris, template, task de 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

  1. Fichier sans front-matter → Création du bloc complet
  2. Fichier avec front-matter partielle → Complétion sans écraser l'existant
  3. Propriétés custom → Préservées et placées après les propriétés requises
  4. Tags/aliases en chaîne → Normalisés en tableau
  5. Disque sans birthtime → Fallback sur ctime puis mtime
  6. Concurrence → Mutex en mémoire + écriture atomique
  7. 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

  1. Modifier server/ensureFrontmatter.mjs :
const requiredProps = [
  // ... propriétés existantes ...
  ['nouvelle_prop', 'valeur_par_defaut'],
];
  1. Mettre à jour les tests unitaires
  2. 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

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