Chirpy site statique dossiers
Se connecter sur la VM jekyll (vm105)
Chirpy
Sauvegardes
Sauvegarde le dossier ~/chirpy
1
cp -a ~/chirpy /sharenfs/vm105/chirpy2026-09-30
Arrêt chirpy
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 chirpy
Configuration _config.yml
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
Dossier _notes
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.Créer le plugin _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 
def fix_images(text)
text.gsub(%r{(!\[[^\]]*\]\()(/images/[^)"]*)\)}) do
"#{Regexp.last_match(1)}#{Regexp.last_match(2).gsub(' ', '%20')})"
end
end
end
end
Sidebar
Dans Chirpy, les entrées de la sidebar sont les fichiers du dossier _tabs/, classés selon le champ order. Accueil est codé en dur et reste toujours en premier.
1- Créer l’onglet
_tabs/notes.md :
1
2
3
4
5
6
---
title: Notes
icon: fas fa-book
order: 3
permalink: /notes/
---
L’icône est n’importe quelle icône Font Awesome gratuite (fas fa-book, fas fa-folder-open, fas fa-sticky-note…).
2- Décaler les autres onglets
Par défaut, Chirpy utilise Catégories = 1, Tags = 2, Archives = 3, À propos = 4. Pour obtenir l’ordre voulu :
| Fichier | order |
|---|---|
_tabs/categories.md |
1 |
_tabs/tags.md |
2 |
_tabs/notes.md |
3 |
_tabs/archives.md |
4 |
_tabs/about.md |
5 |
3- Éviter le conflit avec l’index généré
L’onglet a le permalink /notes/, comme l’index racine créé par le plugin. Il faut donc que le plugin ne crée pas de page en double et injecte la liste des dossiers et des notes dans l’onglet. Dans generate, remplace la boucle finale par :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pages = notes.map(&:page)
tabs = site.collections.values.flat_map(&:docs).to_h { |d| [d.url, d] }
folders.each do |key, folder|
next if indexed.include?(key)
page = build_index(folder, notes, folders)
if (doc = tabs[page.data['permalink']])
doc.content = "#{page.content}\n\n#{doc.content}" # l'onglet existant reçoit la liste
else
pages << page
end
end
pages.each { |p| site.pages << p }
Le corps de _tabs/notes.md peut rester vide, ou contenir un texte d’introduction qui sera placé sous la liste.
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
178
179
180
181
182
183
184
# 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)
tabs = site.collections.values.flat_map(&:docs).to_h { |d| [d.url, d] }
folders.each do |key, folder|
next if indexed.include?(key)
page = build_index(folder, notes, folders)
if (doc = tabs[page.data['permalink']])
doc.content = "#{page.content}\n\n#{doc.content}" # l'onglet existant reçoit la liste
else
pages << page
end
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 
def fix_images(text)
text.gsub(%r{(!\[[^\]]*\]\()(/images/[^)"]*)\)}) do
"#{Regexp.last_match(1)}#{Regexp.last_match(2).gsub(' ', '%20')})"
end
end
end
end
À savoir
- Le libellé vient du
titledu fichier. Chirpy le remplace par la traduction du fichier de locale s’il en existe une pour ce nom d’onglet, ce qui n’est pas le cas denotes. - Je n’ai pas testé l’injection dans le document d’onglet. Si
doc.urlne correspond pas, la page d’index sera créée en double : le build affichera alors un avertissement de permalink dupliqué. - Un onglet sans ce plugin (lien direct vers un autre site) n’est pas possible dans Chirpy sans modifier
_includes/sidebar.html. Ici ce n’est pas nécessaire, puisque/notes/est une page du site.
Lancement service chirpy
Lancement du service jekyll chirpy, script /usr/local/bin/start-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
35
#!/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')] Construction: jekyll build"
"$JEKYLL_BIN" build
echo "[$(date '+%F %T')] Démarrage path utilisateur $PATH_NAME"
systemctl --user start "$PATH_NAME" || true
echo "[$(date '+%F %T')] Démarrage service utilisateur $SERVICE_NAME"
systemctl --user start "$SERVICE_NAME" || true
echo "[$(date '+%F %T')] Fin $0"
exit 0
echo "[$(date '+%F %T')] rsync vers $RSYNC_DEST"
echo "rsync -a --delete -e \"ssh $SSH_OPTS\" \"$RSYNC_SRC\" \"$RSYNC_DEST\""
rsync -a --delete -e "ssh $SSH_OPTS" "$RSYNC_SRC" "$RSYNC_DEST"
Le rendre exécutable
1
sudo chmod +x /usr/local/bin/start-chirpy
Exécuter: start-chirpy
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.