Générer un site statique dossiers

Générer un site statique dossiers

Notes › _DIFFUSION

Générer un site statique à partir de dossiers contenant des fichiers markdown .md, garder les liens vers les images

Se connecter sur la VM jekyll (vm105)

Chirpy

Sauvegardes

Sauvegarde le dossier ~/chirpy

1
cp -a ~/chirpy /sharenfs/vm105/chirpy2026-09-30

Arrêt du service jekyll chirpy, script /usr/local/bin/stop-chirpy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env bash
set -euo pipefail

SERVICE_NAME="chirpy.service"
PATH_NAME="chirpy-rsync.path"
SITE_DIR="$HOME/chirpy"
JEKYLL_BIN="/home/debjek/gems/bin/jekyll"
RSYNC_SRC="/home/debjek/chirpy/_site/"
RSYNC_DEST="yick@192.168.0.205:/sharenfs/rnmkcy/chirpy/"
SSH_OPTS="-p 55205 -i /home/debjek/.ssh/cwwk-ed25519 -o StrictHostKeyChecking=accept-new"

echo "[$(date '+%F %T')] Passage dans $SITE_DIR"
cd "$SITE_DIR"

export PATH="/home/debjek/gems/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export GEM_HOME="/home/debjek/gems"
export GEM_PATH="/home/debjek/gems"

echo "[$(date '+%F %T')] Arrêt path utilisateur $PATH_NAME"
systemctl --user stop "$PATH_NAME" || true

echo "[$(date '+%F %T')] Arrêt service utilisateur $SERVICE_NAME"
systemctl --user stop "$SERVICE_NAME" || true

echo "[$(date '+%F %T')] Attente que $SERVICE_NAME soit vraiment stoppé"
for _ in {1..30}; do
  if systemctl --user is-active --quiet "$SERVICE_NAME"; then
    sleep 1
  else
    break
  fi
done
echo "[$(date '+%F %T')] Supprimer dossier _site"
sudo rm -r /home/debjek/chirpy/_site

Le rendre exécutable

1
sudo chmod +x /usr/local/bin/stop-chirpy

Exécuter: stop-chirpy

1
2
3
4
5
[2026-09-30 17:11:33] Passage dans /home/debjek/chirpy
[2026-09-30 17:11:33] Arrêt path utilisateur chirpy-rsync.path
[2026-09-30 17:11:33] Arrêt service utilisateur chirpy.service
[2026-09-30 17:11:33] Attente que chirpy.service soit vraiment stoppé
[2026-09-30 17:11:33] Supprimer dossier _site

Modifier la configuration

1-Modifier le fichier $HOME/chirpy/_config.yml

1
2
3
4
5
6
7
markdown_folders:
  dirs: [_notes]         # plusieurs dossiers possibles, ils sont fusionnés dans une seule arborescence
  base_url: /notes
  root_title: Notes      # titre de l'index racine
  breadcrumb: true       # fil d'Ariane dans le contenu
  defaults:
    layout: page

2-Créer le dossier _notes

1
mkdir -p $HOME/chirpy/_notes

Copier quelques dossiers et fichiers dans ce répertoire

Plugin markdown_folders.rb

Voici la version complète du plugin. Elle génère un index par dossier (sous-dossiers et notes) et un fil d’Ariane sur chaque page.

_plugins/markdown_folders.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# frozen_string_literal: true

require 'yaml'
require 'date'

module Jekyll
  # Transforme des dossiers de .md (avec ou sans front matter) en pages,
  # avec une page d'index automatique par dossier et un fil d'Ariane.
  class MarkdownFoldersGenerator < Generator
    safe true
    priority :low

    Note = Struct.new(:names, :slugs, :folder_names, :folder_slugs, :index, :page,
                      keyword_init: true)

    def generate(site)
      cfg         = site.config['markdown_folders'] || {}
      @site       = site
      @base_url   = (cfg['base_url'] || '/notes').chomp('/')
      @defaults   = cfg['defaults'] || { 'layout' => 'page' }
      @root_title = cfg['root_title'] || 'Notes'
      @breadcrumb = cfg.fetch('breadcrumb', true)

      notes = Array(cfg['dirs'] || '_notes').flat_map { |d| load_dir(d) }
      return if notes.empty?

      folders = collect_folders(notes)
      indexed = notes.select(&:index).map { |n| n.folder_slugs.join('/') }

      pages = notes.map(&:page)
      folders.each do |key, folder|
        next if indexed.include?(key)

        pages << build_index(folder, notes, folders)
      end

      pages.each { |p| site.pages << p }
    end

    private

    # --- Lecture des fichiers -------------------------------------------

    def load_dir(dir)
      root = File.join(@site.source, dir)
      return [] unless Dir.exist?(root)

      Dir.glob(File.join(root, '**', '*.md')).sort.filter_map do |path|
        names = path.sub("#{root}/", '').sub(/\.md\z/, '').split('/')
        next if names.any? { |s| s.start_with?('.') } # .obsidian, .trash…

        build_note(path, names)
      end
    end

    def build_note(path, names)
      raw  = File.read(path, mode: 'r:bom|utf-8')
      data = {}
      body = raw

      if (m = Jekyll::Document::YAML_FRONT_MATTER_REGEXP.match(raw))
        begin
          data = YAML.safe_load(m[1], permitted_classes: [Date, Time]) || {}
          body = m.post_match
        rescue Psych::SyntaxError => e
          Jekyll.logger.warn 'markdown_folders:', "front matter invalide dans #{path} (#{e.message})"
        end
      end

      slugs        = names.map { |s| Utils.slugify(s, mode: 'latin') }
      folder_names = names[0..-2]
      folder_slugs = slugs[0..-2]
      index        = names.last.casecmp?('index')

      crumb   = breadcrumb(folder_names, folder_slugs, link_last: !index)
      content = fix_images(body)
      content = "#{crumb}\n\n#{content}" unless crumb.empty?

      if index
        permalink = url(folder_slugs)
        title     = folder_names.last || @root_title
        name      = folder_slugs.last || 'index'
      else
        permalink = url(slugs)
        title     = names.last
        name      = slugs.last
      end

      page = make_page(name, permalink, title, content, data)
      Note.new(names: names, slugs: slugs, folder_names: folder_names,
               folder_slugs: folder_slugs, index: index, page: page)
    end

    # --- Index de dossiers ----------------------------------------------

    def collect_folders(notes)
      folders = {}
      notes.each do |n|
        (0..n.folder_slugs.size).each do |i|
          slugs = n.folder_slugs[0...i]
          folders[slugs.join('/')] ||= { names: n.folder_names[0...i], slugs: slugs }
        end
      end
      folders
    end

    def build_index(folder, notes, folders)
      names = folder[:names]
      slugs = folder[:slugs]
      depth = slugs.size

      subs = folders.values
                    .select { |f| f[:slugs].size == depth + 1 && f[:slugs][0...depth] == slugs }
                    .sort_by { |f| f[:names].last.downcase }
      files = notes.reject(&:index)
                   .select { |n| n.folder_slugs == slugs }
                   .sort_by { |n| n.names.last.downcase }

      lines = []
      crumb = breadcrumb(names, slugs, link_last: false)
      lines << crumb << '' unless crumb.empty?

      unless subs.empty?
        lines << '## Dossiers' << ''
        subs.each { |f| lines << "- [#{f[:names].last}](#{href(f[:slugs])})" }
        lines << ''
      end

      unless files.empty?
        lines << '## Notes' << ''
        files.each { |n| lines << "- [#{n.names.last}](#{href(n.slugs)})" }
        lines << ''
      end

      name = slugs.last || 'index'
      make_page(name, url(slugs), names.last || @root_title, lines.join("\n"), {})
    end

    # --- Utilitaires ----------------------------------------------------

    def make_page(name, permalink, title, content, data)
      page = PageWithoutAFile.new(@site, @site.source, '', "#{name}.md")
      page.content = content
      page.data.merge!(@defaults)
      page.data['title']     = title
      page.data['permalink'] = permalink
      page.data.merge!(data) # le front matter, s'il existe, l'emporte
      page
    end

    def url(slugs)
      slugs.empty? ? "#{@base_url}/" : "#{@base_url}/#{slugs.join('/')}/"
    end

    def href(slugs)
      "#{@site.baseurl}#{url(slugs)}"
    end

    def breadcrumb(names, slugs, link_last:)
      return '' unless @breadcrumb

      items = [[@root_title, []]] + names.each_index.map { |i| [names[i], slugs[0..i]] }
      return '' if items.size == 1 && !link_last

      items.each_with_index.map do |(label, s), i|
        i == items.size - 1 && !link_last ? label : "[#{label}](#{href(s)})"
      end.join(' › ')
    end

    # kramdown ne comprend pas les espaces dans ![](/images/Nom%20Image.png)
    def fix_images(text)
      text.gsub(%r{(!\[[^\]]*\]\()(/images/[^)"]*)\)}) do
        "#{Regexp.last_match(1)}#{Regexp.last_match(2).gsub(' ', '%20')})"
      end
    end
  end
end

Comportement

1
2
3
4
5
6
7
8
_notes/
  Linux/
    Installation EndeavourOS.md    → /notes/linux/installation-endeavouros/
    Réseau/
      Bridge.md                    → /notes/linux/reseau/bridge/
  Divers/
    index.md                       → /notes/divers/   (remplace l'index automatique)
    Idées.md                       → /notes/divers/idees/
  • /notes/, /notes/linux/ et /notes/linux/reseau/ reçoivent chacun un index automatique. Il liste les sous-dossiers, puis les notes, et est trié alphabétiquement sans tenir compte de la casse.
  • Si un dossier contient un index.md, il prend la place de l’index généré, y compris à la racine (_notes/index.md). Son front matter éventuel est respecté.
  • Le fil d’Ariane est de la forme Notes › Linux › Réseau, et le dernier élément est en texte simple sur les pages d’index.
  • Les liens tiennent compte de baseurl.

Points à valider

  • Ajoute une entrée dans le menu ou un lien vers /notes/ pour accéder à l’arborescence, car ces pages ne figurent pas dans la sidebar de Chirpy.
  • Si le fil d’Ariane de la barre du haut de Chirpy te suffit, mets breadcrumb: false. Je n’ai pas vérifié s’il s’affiche sur ces pages.
  • Deux noms qui donnent le même slug dans le même dossier (par exemple Idée et Idee) produiront le même permalink. Il faut en renommer un.
  • Comme précédemment, le code n’a pas été exécuté : valide avec ta séquence habituelle (_site_before, build propre avec --trace, diff -rq), après redémarrage du service systemd.