feat: translate the site in french

This commit is contained in:
Bruno Carlin 2025-06-20 02:33:45 +02:00
parent 3e98ac15b6
commit b47b193b20
WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS.
GPG key ID: 8E254EA0FFEB9B6D
81 changed files with 1327 additions and 251 deletions

View file

@ -1,5 +1,13 @@
+++ ---
date = '{{ .Date }}' title: '{{ replace .File.ContentBaseName "-" " " | title }}'
draft = true slug: '{{ .File.ContentBaseName }}'
title = '{{ replace .File.ContentBaseName "-" " " | title }}' date: '{{ .Date }}'
+++ draft: true
categories: []
tags:
-
summary: |
entrer le résumé
description: |
entrer la description
---

View file

@ -217,6 +217,10 @@ body > footer p {
margin-right: 0; margin-right: 0;
margin-left: 18em; margin-left: 18em;
} }
.metadata p {
display: inline;
}
} }
@ -224,6 +228,15 @@ body > footer p {
* Content Styling * Content Styling
*/ */
.translations {
text-align: end;
margin-bottom: 0.3em;
}
.translations a {
text-decoration: none;
}
main > article { main > article {
text-align: justify; text-align: justify;
} }
@ -232,11 +245,6 @@ main > article > header {
margin-bottom: calc(2 * var(--pico-block-spacing-vertical)); margin-bottom: calc(2 * var(--pico-block-spacing-vertical));
} }
.metadata {
display: flex;
gap: 0.5em;
}
ul.tags, ul.tags li { ul.tags, ul.tags li {
list-style-type: none; list-style-type: none;
display: inline; display: inline;
@ -244,10 +252,6 @@ ul.tags, ul.tags li {
margin-bottom: 0; margin-bottom: 0;
} }
.tags li:not(:last-child)::after {
content: ", ";
}
ul > li { ul > li {
list-style-type: disc; list-style-type: disc;
} }
@ -261,11 +265,6 @@ h1 {
font-size: 1.7rem; font-size: 1.7rem;
} }
h1::before {
content: "# ";
color: var(--markup-color);
}
h2 { h2 {
font-size: 1.3rem; font-size: 1.3rem;
} }
@ -313,10 +312,21 @@ h6::before {
article header h1, article header p { article header h1, article header p {
margin-bottom: calc(0.1 * var(--pico-typography-spacing-vertical)); margin-bottom: calc(0.1 * var(--pico-typography-spacing-vertical));
margin-top: 0.3em;
} }
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { article header .category a {
font-family: var(--pico-font-family-sans-serif);
font-weight: 700;
line-height: var(--pico-line-height);
color: var(--pico-muted-color);
text-decoration: none;
font-size: 1rem;
margin-bottom: 0.5em;
}
h2 a, h3 a, h4 a, h5 a, h6 a {
color: var(--pico-muted-color); color: var(--pico-muted-color);
text-decoration: none; text-decoration: none;
margin-left: 0.4rem; margin-left: 0.4rem;

39
config/_default/hugo.yaml Normal file
View file

@ -0,0 +1,39 @@
baseURL: 'https://bcarlin.net/'
languageCode: en
title: 'Bruno Carlin'
#theme: ['bcarlin']
uglyUrls: true
copyright: '© 2025 Bruno Carlin'
summaryLength: 15
params:
author:
email: mail@bcarlin.net
name: Bruno Carlin
languages:
en:
weight: 10
languageName: English
fr:
weight: 20
languageName: Français
outputs:
home:
- html
section:
- html
- rss
taxonomy:
- html
term:
- html
- rss
outputFormats:
rss:
noUgly: false
markup:
highlight:
noClasses: false

View file

@ -1,36 +1,3 @@
baseURL: 'https://bcarlin.net/'
languageCode: 'en_US'
title: 'Bruno Carlin'
theme: ['bcarlin']
uglyUrls: true
copyright: '© 2025 Bruno Carlin'
summaryLength: 15
params:
author:
email: mail@bcarlin.net
name: Bruno Carlin
outputs:
home:
- html
section:
- html
- rss
taxonomy:
- html
term:
- html
- rss
outputFormats:
rss:
noUgly: false
markup:
highlight:
noClasses: false
menus:
main: main:
- name: 'Home' - name: 'Home'
pageRef: '/' pageRef: '/'
@ -66,3 +33,4 @@ menus:
class: "u-key" class: "u-key"
iconName: "key-fill" iconName: "key-fill"
iconTitle: "GPG Public Key" iconTitle: "GPG Public Key"

View file

@ -0,0 +1,36 @@
main:
- name: 'Accueil'
pageRef: '/'
weight: 10
params:
class: "u-url"
- name: 'Blog'
pageRef: '/blog'
weight: 20
secondary:
- name: '@bcarlin@hachyderm.io'
url: 'https://hachyderm.io/@bcarlin'
weight: 10
params:
rel: "me"
iconName: "mastodon-fill"
iconTitle: "Mastodon"
- name: 'LinkedIn'
url: 'https://www.linkedin.com/in/brunocarlin'
weight: 20
params:
rel: "me"
iconName: "linkedin-fill"
iconTitle: "LinkedIn"
- name: 'Clef GPG'
url: '/bcarlin.gpg'
weight: 30
params:
class: "u-key"
iconName: "key-fill"
iconTitle: "Clef publique GPG"

View file

@ -0,0 +1,8 @@
enableMissingTranslationPlaceholders: true
ignoreCache: true
cleanDestinationDir: true
buildDrafts: true
buildFuture: true
printI18nWarnings: true
printPathWarnings: true
printUnusedTemplates: true

12
content/_index.fr.md Normal file
View file

@ -0,0 +1,12 @@
---
title: 'Accueil'
date: 2025-06-03T23:00:00Z
draft: false
---
Bienvenue !
Je suis Bruno Carlin, un technologue fasciné par le développement de logiciels,
les langages de programmation et l'infrastructure qui les soutient. Je suis un
défenseur de longue date de l'open-source et un autohébergeur amateur, toujours
à la recherche de ce qui vient ensuite.

View file

@ -1,10 +1,11 @@
--- ---
title: Setup Nginx for Mediawiki title: Setup Nginx for Mediawiki
date: "2010-09-15T00:00:00+02:00"
tags:
- Nginx
- Mediawiki
slug: 1-setup-nginx-for-mediawiki slug: 1-setup-nginx-for-mediawiki
date: "2010-09-15T00:00:00+02:00"
categories: [DevOps]
tags:
- mediawiki
- nginx
summary: > summary: >
A simple configuration to serve Mediawiki with Nginx and FastCGI A simple configuration to serve Mediawiki with Nginx and FastCGI
--- ---

View file

@ -0,0 +1,47 @@
---
title: Configurer Nginx pour Mediawiki
slug: 1-configurer-nginx-pour-mediawiki
date: "2010-09-15T00:00:00+02:00"
categories: [DevOps]
tags:
- mediawiki
- nginx
summary: >
Une configuration simple de Nginx pour servir Mediawiki en FastCGI
---
Il y a deux semaines, j'ai migré un serveur d'Apache/mod_php vers nginx/php-fpm.
Ce n'est qu'aujourd'hui que j'ai réussi à éliminer tous les effets secondaires.
Le dernier en date :
Les fichiers statiques ne doivent pas passer par php-fpm, mais un simple test
sur les extensions est inefficace, car les URL comme
`http://serveur/File:nom_du_fichier.png` doivent être traitées par PHP.
Voici ma configuration finale, qui corrige toutes les erreurs que j'ai rencontrées :
```nginx
server {
listen 80;
server_name server_name;
index index.php;
root /path/to/www/;
# Serve static files with a far future expiration
# date for browser caches
location ^~ /images/ {
expires 1y;
}
location ^~ /skins/ {
expires 1y;
}
# Pass the request to php-cgi
location / {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
fastcgi_index index.php;
include fastcgi_params;
}
}
```

View file

@ -1,10 +1,13 @@
--- ---
title: Build the latest PgPool-II on Debian Etch title: Build the latest PgPool-II on Debian Etch
date: "2010-12-14T00:00:00+01:00" date: "2010-12-14T00:00:00+01:00"
tags: [Debian, PgPool-II] slug: 2-build-pgpool-on-debian-etch
slug: 2-build-pgpool-on-debian categories: [DevOps]
tags:
- Debian
- PgPool-II
summary: > summary: >
Building PgPool-II on RHEL 5.5 to avoid the "libpq is not installed or Building PgPool-II on Debian Etch and avoid the "libpq is not installed or
libpq is old" error libpq is old" error
--- ---

View file

@ -0,0 +1,35 @@
---
title: Compiler le dernier PgPool-II sur Debian Etch
slug: 2-compiler-pgpool-sur-debian-etch
date: "2010-12-14T00:00:00+01:00"
categories: [DevOps]
tags:
- Debian
- PgPool-II
summary: >
Compilation de PgPool-II sur RHEL 5.5 pour éviter l'erreur "libpq is not
installed or libpq is old"
---
Après avoir compilé PgPool-II sur Red Hat Enterprise Linux 5.5 sans aucun
problème, j'ai essayé de le compiler sur un nouveau Debian Etch. Seulement, je
ne voulais pas installer PostgreSQL 9.0, mais simplement l'extraire des
paquets binaires fournis par Entreprisedb (avec l'option `--extract-only 1`).
Quelles que soient les options que je passais à `./configure`, cela résultait en
la même erreur :
{{< highlight text >}}
checking for PQexecPrepared in -lpq... no
configure: error: libpq is not installed or libpq is old
{{< /highlight >}}
Voici la réponse : le paquet binaire contient la libpq avec le nom
`libcrypto.so.0.9.8` (le nom RHEL) lorsque pgpool recherche `libcrypto.so.6` sur
Debian. La même chose s'applique à `libssl`. Donc un simple
{{< highlight bash >}}
ln -s libcrypto.so.0.9.8 libcrypto.so.0.9.8
ln -s libssl.so.0.9.8 libssl.so.6
{{< /highlight >}}
avant votre `./configure` résoudra le problème !

View file

@ -1,7 +1,11 @@
--- ---
slug: 3-aptana-eclipse-and-xulrunner slug: 3-aptana-eclipse-and-xulrunner
title: Aptana Studio/Eclipse and Xulrunner title: Aptana Studio/Eclipse and Xulrunner
tags: [Aptana Studio, Eclipse, Xulrunner, Arch Linux] categories: [Tooling]
tags:
- Aptana Studio
- Eclipse
- Xulrunner
date: "2011-12-16T00:00:00+01:00" date: "2011-12-16T00:00:00+01:00"
summary: > summary: >
How to solve the "Unhandled event loop exception" error in Aptana Studio and Eclipse 3.7 with Xulrunner How to solve the "Unhandled event loop exception" error in Aptana Studio and Eclipse 3.7 with Xulrunner

View file

@ -0,0 +1,93 @@
---
slug: 3-aptana-eclipse-et-xulrunner
title: Aptana Studio/Eclipse et Xulrunner
categories: [Outils]
tags:
- Aptana Studio
- Eclipse
- Xulrunner
date: "2011-12-16T00:00:00+01:00"
summary: >
Comment résoudre l'erreur "Unhandled event loop exception" dans Aptana Studio
et Eclipse 3.7 avec Xulrunner
---
Depuis quelques mois, j'ai une erreur gênante dans Aptana Studio et Eclipse 3.7
(les paquets autonomes, pas ceux des dépôts) chaque fois que j'ai essayé de
faire une action git ou hg.
J'ai pu vivre avec jusqu'à maintenant, mais aujourd'hui, cela me dérangeait
vraiment.
L'erreur est :
{{< highlight text >}}
Unhandled event loop exception
No more handles [Unknown Mozilla path (MOZILLA_FIVE_HOME not set)]
{{< /highlight >}}
Le fichier log montrait la trace suivante :
{{< highlight text >}}
!ENTRY org.eclipse.ui 4 0 2011-12-16 17:17:30.825
!MESSAGE Unhandled event loop exception
!STACK 0
org.eclipse.swt.SWTError: No more handles [Unknown Mozilla path (MOZILLA_FIVE_HOME not set)]
at org.eclipse.swt.SWT.error(SWT.java:4109)
at org.eclipse.swt.browser.Mozilla.initMozilla(Mozilla.java:1739)
at org.eclipse.swt.browser.Mozilla.create(Mozilla.java:656)
at org.eclipse.swt.browser.Browser.<init>(Browser.java:119)
at com.aptana.git.ui.internal.actions.CommitDialog.createDiffArea(CommitDialog.java:237)
at com.aptana.git.ui.internal.actions.CommitDialog.createDialogArea(CommitDialog.java:158)
[...]
at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:620)
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:575)
at org.eclipse.equinox.launcher.Main.run(Main.java:1408)
at org.eclipse.equinox.launcher.Main.main(Main.java:1384)
{{< /highlight >}}
Pour faire court, après avoir lu
[beaucoup](https://bugs.archlinux.org/task/5149)
[de](https://bugs.archlinux.org/task/27130)
[posts](https://github.com/eclipse-color-theme/eclipse-color-theme/issues/50)
[à ce sujet](https://bbs.archlinux.org/viewtopic.php?id=129982)
[sur](http://forums.gentoo.org/viewtopic-t-827838-view-previous.html?sid=546c5717e2167c45d9b02f9f20ab36f4)
[ce](http://stackoverflow.com/questions/1017945/problem-with-aptana-studio-xulrunner-8-1)
[problème](http://www.eclipse.org/swt/faq.php#gtk64), il semblait qu'il
suffisait de donner le chemin vers Xulrunner à Aptana.
Sur mon Arch Linux, c'était :
{{< highlight bash >}}
export MOZILLA_FIVE_HOME=/usr/lib/xulrunner-8.0
{{< /highlight >}}
En essayant de démarrer Aptana Studio, j'ai eu une nouvelle erreur. Elle disait
simplement :
{{< highlight text >}}
XPCOM error -2147467261
{{< /highlight >}}
La solution est qu'Aptana Studio ne peut pas fonctionner avec la version de
Xulrunner dans les dépôts Arch Linux car elle est trop récente.
Pour résoudre ce problème, j'ai dû installer xulrunner 1.9.2 depuis AUR :
{{< highlight bash >}}
yaourt -S xulrunner192
{{< /highlight >}}
Le PKGBUILD était cassé ce matin et se terminait par une erreur 404 lors de la
récupération des sources. Si vous avez le même problème, voici un PKGBUILD mis à
jour : [PKGBUILD mis à jour](https://gist.github.com/1486851).
Enfin, j'ai mis :
{{< highlight bash >}}
-Dorg.eclipse.swt.browser.XULRunnerPath=/usr/lib/xulrunner-1.9.2
{{< /highlight >}}
à la fin du fichier `AptanaStudio3.ini` dans le dossier d'Aptana Studio. Pour le
paquet dans les dépôts Arch Linux, ce fichier est
`/usr/share/aptana/AptanaStudio3.ini`.

View file

@ -1,8 +1,11 @@
--- ---
tags: [Python, Buzhug, Database, Locks]
slug: 4-locking-buzhug slug: 4-locking-buzhug
title: Locking Buzhug title: Locking Buzhug
date: "2012-02-07T00:00:00+01:00" date: "2012-02-07T00:00:00+01:00"
categories: [Dev]
tags:
- Python
- Buzhug
summary: > summary: >
How to implement a cross-process, system-wide lock for Buzhug How to implement a cross-process, system-wide lock for Buzhug
--- ---
@ -58,7 +61,7 @@ As it only has to work on Linux, the
Vmfarms](http://blog.vmfarms.com/2011/03/cross-process-locking-and.html) fits Vmfarms](http://blog.vmfarms.com/2011/03/cross-process-locking-and.html) fits
perfectly. Here is a version slightly modified to make it a context manager : perfectly. Here is a version slightly modified to make it a context manager :
{{< highlight python >}} ```python
import fcntl import fcntl
class PsLock: class PsLock:
@ -87,83 +90,83 @@ class PsLock:
def __enter__(self): def __enter__(self):
self.acquire() self.acquire()
{{< /highlight >}} ```
The second step is to define a new class that inheritates from `buzhug.Base` The second step is to define a new class that inheritates from `buzhug.Base`
that uses `PsLock` (inspired by `TS_Base`): that uses `PsLock` (inspired by `TS_Base`):
{{< highlight python >}} ```python
import buzhug import buzhug
_lock = PsLock("/tmp/buzhug.lck") _lock = PsLock("/tmp/buzhug.lck")
class PS_Base(buzhug.Base): class PS_Base(buzhug.Base):
def create(self,*args,**kw): def create(self, *args, **kw):
with _lock: with _lock:
res = buzhug.Base.create(self,*args,**kw) res = buzhug.Base.create(self, *args, **kw)
return res return res
def open(self,*args,**kw): def open(self, *args, **kw):
with _lock: with _lock:
res = buzhug.Base.open(self,*args,**kw) res = buzhug.Base.open(self, *args, **kw)
return res return res
def close(self,*args,**kw): def close(self, *args, **kw):
with _lock: with _lock:
res = buzhug.Base.close(self,*args,**kw) res = buzhug.Base.close(self, *args, **kw)
return res return res
def destroy(self,*args,**kw): def destroy(self, *args, **kw):
with _lock: with _lock:
res = buzhug.Base.destroy(self,*args,**kw) res = buzhug.Base.destroy(self, *args, **kw)
return res return res
def set_default(self,*args,**kw): def set_default(self, *args, **kw):
with _lock: with _lock:
res = buzhug.Base.set_default(self,*args,**kw) res = buzhug.Base.set_default(self, *args, **kw)
return res return res
def insert(self,*args,**kw): def insert(self, *args, **kw):
with _lock: with _lock:
res = buzhug.Base.insert(self,*args,**kw) res = buzhug.Base.insert(self, *args, **kw)
return res return res
def update(self,*args,**kw): def update(self, *args, **kw):
with _lock: with _lock:
res = buzhug.Base.update(self,*args,**kw) res = buzhug.Base.update(self, *args, **kw)
return res return res
def delete(self,*args,**kw): def delete(self, *args, **kw):
with _lock: with _lock:
res = buzhug.Base.delete(self,*args,**kw) res = buzhug.Base.delete(self, *args, **kw)
return res return res
def cleanup(self,*args,**kw): def cleanup(self, *args, **kw):
with _lock: with _lock:
res = buzhug.Base.cleanup(self,*args,**kw) res = buzhug.Base.cleanup(self, *args, **kw)
return res return res
def commit(self,*args,**kw): def commit(self, *args, **kw):
with _lock: with _lock:
res = buzhug.Base.commit(self,*args,**kw) res = buzhug.Base.commit(self, *args, **kw)
return res return res
def add_field(self,*args,**kw): def add_field(self, *args, **kw):
with _lock: with _lock:
res = buzhug.Base.add_field(self,*args,**kw) res = buzhug.Base.add_field(self, *args, **kw)
return res return res
def drop_field(self,*args,**kw): def drop_field(self, *args, **kw):
with _lock: with _lock:
res = buzhug.Base.drop_field(self,*args,**kw) res = buzhug.Base.drop_field(self, *args, **kw)
return res return res
{{< /highlight >}} ```
Now I just use Now I just use
{{< highlight python >}} ```python
database = PS_Base( ... ) database = PS_Base( ... )
{{< /highlight >}} ```
And all the errors have vanished. And all the errors have vanished.

View file

@ -0,0 +1,177 @@
---
slug: 4-verrouiller-buzhug
title: Verrouiller de Buzhug
date: "2012-02-07T00:00:00+01:00"
categories: [Dev]
tags:
- Python
- Buzhug
summary: >
Comment implémenter un verrou inter-processus et système pour Buzhug
---
J'ai récemment décidé d'utiliser [Buzhug] pour un projet. À ce que je puisse en
juger, il s'est avéré efficace, rapide, facile à utiliser et à maintenir.
Cependant, j'ai rencontré quelques problèmes.
[Buzhug]: http://buzhug.sourceforge.net
## Les solutions simples sont souvent les meilleures
J'en suis venu à utiliser Buzhug pour les raisons suivantes :
- J'avais besoin d'une seule table
- Je ne voulais pas ajouter de dépendances supplémentaires au projet
- La taille de la table serait en moyenne de 5K entrées (sans dépasser
10k entrées en pic)
Et une raison supplémentaire (personnelle) :
- Je ne voulais pas me soucier de SQL. Vraiment pas. Pas question !
Cela ne me laissait qu'une option : une base de données embarquée en pur Python.
Après avoir considéré quelques bibliothèques, j'ai été séduit par la manière
dont l'interface de Buzhug est proche de la manipulation d'objets Python. Et les
benchmarks semblaient montrer qu'il est assez performant pour ce projet.
Après un rapide prototypage (1 jour), le choix était fait.
Puis vinrent quelques semaines de développement et les premiers tests de
charge...
## Et la réalité est revenue rapidement
Plusieurs fois par jour, l'application soutenue par cette base de données est
intensément utilisée :
- Elle peut être exécutée jusqu'à 50 fois simultanément dans des processus
Python séparés
- Chaque exécution effectue une opération de lecture et une opération
d'écriture/suppression
Cela provoque une condition de course sur les fichiers utilisés pour stocker les
données, et les écritures concurrentes corrompent la base de données.
L'utilisation de `buzhug.TS_Base` au lieu de `buzhug.Base` n'a rien résolu, car
le problème n'est pas lié aux threads, mais aux processus. Ce dont j'ai besoin
est un verrou inter-processus.
## Voici la solution
La première étape a été de trouver comment implémenter un verrou inter-processus
et système.
Comme cela doit seulement fonctionner sur Linux, la
[classe Lock donnée par Chris de
Vmfarms](http://blog.vmfarms.com/2011/03/cross-process-locking-and.html) convient
parfaitement. Voici une version légèrement modifiée pour en faire un gestionnaire de contexte :
```python
import fcntl
class PsLock:
"""
Adapté de :
http://blog.vmfarms.com/2011/03/cross-process-locking-and.html
"""
def __init__(self, filename):
self.filename = filename
self.handle = open(filename, 'w')
# Faites un OR bit à bit avec fcntl.LOCK_NB si vous avez besoin d'un verrou non bloquant
def acquire(self):
fcntl.flock(self.handle, fcntl.LOCK_EX)
def release(self):
fcntl.flock(self.handle, fcntl.LOCK_UN)
def __del__(self):
self.handle.close()
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
pass
self.release()
def __enter__(self):
self.acquire()
```
La deuxième étape consiste à définir une nouvelle classe qui hérite de
`buzhug.Base` et qui utilise `PsLock` (inspiré de `TS_Base`) :
```python
import buzhug
_lock = PsLock("/tmp/buzhug.lck")
class PS_Base(buzhug.Base):
def create(self, *args, **kw):
with _lock:
res = buzhug.Base.create(self, *args, **kw)
return res
def open(self, *args, **kw):
with _lock:
res = buzhug.Base.open(self, *args, **kw)
return res
def close(self, *args, **kw):
with _lock:
res = buzhug.Base.close(self, *args, **kw)
return res
def destroy(self, *args, **kw):
with _lock:
res = buzhug.Base.destroy(self, *args, **kw)
return res
def set_default(self, *args, **kw):
with _lock:
res = buzhug.Base.set_default(self, *args, **kw)
return res
def insert(self, *args, **kw):
with _lock:
res = buzhug.Base.insert(self, *args, **kw)
return res
def update(self, *args, **kw):
with _lock:
res = buzhug.Base.update(self, *args, **kw)
return res
def delete(self, *args, **kw):
with _lock:
res = buzhug.Base.delete(self, *args, **kw)
return res
def cleanup(self, *args, **kw):
with _lock:
res = buzhug.Base.cleanup(self, *args, **kw)
return res
def commit(self, *args, **kw):
with _lock:
res = buzhug.Base.commit(self, *args, **kw)
return res
def add_field(self, *args, **kw):
with _lock:
res = buzhug.Base.add_field(self, *args, **kw)
return res
def drop_field(self, *args, **kw):
with _lock:
res = buzhug.Base.drop_field(self, *args, **kw)
return res
```
Maintenant, j'utilise simplement
```python
database = PS_Base( ... )
```
Et toutes les erreurs ont disparu.

View file

@ -1,8 +1,12 @@
--- ---
tags: [Sublime Text 2]
slug: 5-automatically-open-sublime-text-projects-in-a-directory
title: Automatically open Sublime Text projects in a directory title: Automatically open Sublime Text projects in a directory
slug: 5-automatically-open-sublime-text-projects-in-a-directory
date: "2013-05-15T00:00:00+02:00" date: "2013-05-15T00:00:00+02:00"
categories:
- Tooling
tags:
- Sublime Text
- Bash
summary: > summary: >
How to automatically open Sublimetext with a file, a project or the current How to automatically open Sublimetext with a file, a project or the current
directory according to the context. directory according to the context.
@ -17,19 +21,19 @@ It ends up with one of the following commands :
- `subl .` - `subl .`
- `subl my-project.sublime-project` - `subl my-project.sublime-project`
Here is the snippet I added to my .bashrc file to have the `subl` Here is the snippet I added to my `.bashrc` file to have the `subl`
command automatically "guess" what I want. It does the following: command automatically "guess" what I want. It does the following:
- If a path is given (subl "my/file.txt"), it opens the file. - If a path is given (`subl "my/file.txt"`), it opens the file.
- If nothing is given and a .sublime-project file exists in the current - If nothing is given and a `.sublime-project` file exists in the current
directory, it opens it directory, it opens it
- If nothing is given and no .sublime-project file has been found, it - If nothing is given and no `.sublime-project` file has been found, it
opens the folder. opens the folder.
{{< highlight bash >}} ```bash
function project_aware_subl { function project_aware_subl {
project_file=$(ls *.sublime-project 2>/dev/null | head -n 1) project_file=$(ls *.sublime-project 2>/dev/null | head -n 1)
subl ${*:-${project_file:-.}} subl ${*:-${project_file:-.}}
} }
alias subl="project_aware_subl" alias subl="project_aware_subl"
{{< /highlight >}} ```

View file

@ -0,0 +1,39 @@
---
title: Ouvrir automatiquement les projets Sublime Text dans un répertoire
slug: 5-ouvrir-automatiquement-les-projets-sublime-text-dans-un-repertoire
date: "2013-05-15T00:00:00+02:00"
categories:
- Outils
tags:
- Sublime Text
- Bash
summary: >
Comment ouvrir automatiquement Sublime Text avec un fichier, un projet ou le
répertoire courant selon le contexte.
---
J'ai l'habitude de lancer Sublime Text 2 depuis la ligne de commande pour
travailler, selon le cas, sur le contenu d'un répertoire ou sur un projet
(matérialisé par un fichier `*.sublime-project`).
J'utilise l'une des commandes suivantes :
- `subl .`
- `subl mon-projet.sublime-project`
Voici la fonction que j'ai ajoutée à mon fichier `.bashrc` pour que la commande
`subl` "devine" automatiquement ce que je veux. Il fait ce qui suit :
- Si un chemin est donné (`subl "mon/fichier.txt"`), il ouvre le fichier.
- Si rien n'est donné et qu'un fichier `.sublime-project` existe dans le
répertoire courant, il l'ouvre.
- Si rien n'est donné et qu'aucun fichier `.sublime-project` n'a été trouvé, il
ouvre le dossier.
```bash
function project_aware_subl {
project_file=$(ls *.sublime-project 2>/dev/null | head -n 1)
subl ${*:-\${project_file:-.}}
}
alias subl="project_aware_subl"
```

View file

@ -2,8 +2,13 @@
title: Discourse without Docker title: Discourse without Docker
slug: 6-discourse-without-docker slug: 6-discourse-without-docker
date: "2016-06-27T00:00:00+02:00" date: "2016-06-27T00:00:00+02:00"
tags: [discourse, docker] categories:
summary: Detailed instructions on how to install Discourse and plugins without Docker. - DevOps
tags:
- discourse
- docker
summary: >
Detailed instructions on how to install Discourse and plugins without Docker.
--- ---
{{< warning >}} {{< warning >}}
@ -277,8 +282,7 @@ WantedBy=multi-user.target
### Discourse ### Discourse
For Discourse, just create the service unit for Puma. Create the file For Discourse, just create the service unit for Puma. Create the file
`/etc/systemd/system/discourse.service` with the `/etc/systemd/system/discourse.service` with the following content:
following content:
```ini ```ini
[Unit] [Unit]
@ -325,13 +329,13 @@ Restart Discourse:
systemctl restart discourse systemctl restart discourse
``` ```
What can go wrong? If if I do not give any solution here, it is always What can go wrong? If I do not give any solution here, it is always
recoverable (hence the backups!). recoverable (hence the backups!).
- The database migration failed (restore the database with your backup, - The database migration failed (restore the database with your backup,
fix the problem and try again!) fix the problem and try again!)
- The plugins are not compatible with the latest version (rollback to - The plugins are not compatible with the latest version (rollback to
the previous working solution and wit for them to be compatible) the previous working version and wait for them to be compatible)
## Plugins ## Plugins

View file

@ -0,0 +1,402 @@
---
title: Discourse sans Docker
slug: 6-discourse-sans-docker
date: "2016-06-27T00:00:00+02:00"
categories:
- DevOps
tags:
- discourse
- docker
summary: >
Instructions détaillées sur la façon d'installer Discourse et ses plugins sans
Docker.
---
{{< warning >}}
La seule méthode officielle est [avec Docker]. Vous pourriez ne pas obtenir de
support de Discourse en suivant cette méthode.
[avec Docker]: http://blog.discourse.org/2014/04/install-discourse-in-under-30-minutes/
{{< /warning >}}
L'équipe derrière [Discourse] a choisi de ne publier que des images Docker de
leur logiciel. La raison derrière cela est : il est plus facile de ne supporter
qu'une seule configuration. Je ne discuterai pas de cela. C'est leur choix.
Cependant, je n'aime pas utiliser Docker pour déployer des applications en
production. Vraiment pas. Si vous êtes comme moi, voici les étapes que
j'ai utilisées pour l'installer et le configurer.
J'utilise des serveurs Debian en production, donc les étapes ci-dessous sont
toutes orientées Debian.
{{< note >}}
Ceci n'est pas destiné à être un guide complet. Beaucoup de commandes et
fichiers de configuration pourraient avoir besoin d'être adaptés à votre
environnement.
Il ne traite même pas de sujets importants en production tels que
la sécurité. Cela est laissé comme exercice au lecteur.
{{< /note >}}
## Installation
Discourse est une application Rails. Elle peut être installée comme
n'importe quelle autre application Rails :
Tout d'abord, Discourse utilise Redis et PostgreSQL (ou du moins,
je préfère utiliser Postgres). J'utilise également Nginx comme proxy pour
l'application. Installez les dépendances externes :
```sh
# Ajoutez le dépôt pour Redis
echo "deb http://packages.dotdeb.org jessie all" > /etc/apt/sources.list.d/dotdeb.list
wget https://www.dotdeb.org/dotdeb.gpg -O - | apt-key add -
# Ajoutez le dépôt pour PostgreSQL :
echo "deb http://apt.postgresql.org/pub/repos/apt/ jessie-pgdg main" > /etc/apt/sources.list.d/postgresql.list
wget -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
apt-get update
apt-get install postgresql-9.5 redis-server nginx
```
Ensuite, créez une base de données pour l'application. Entrez dans l'interface
de commande de postgres :
```sh
su - postgres -c psql
```
et entrez les commandes suivantes :
```sql
CREATE DATABASE discourse;
CREATE USER discourse;
ALTER USER discourse WITH ENCRYPTED PASSWORD 'password';
ALTER DATABASE discourse OWNER TO discourse;
\connect discourse
CREATE EXTENSION hstore;
CREATE EXTENSION pg_trgm;
```
Ensuite, vous pouvez cloner le code de Discourse :
```sh
git clone https://github.com/discourse/discourse.git /chemin/vers/discourse
#
# Optionnellement, basculez sur une étiquette spécifique
cd /chemin/vers/discourse
git checkout v1.5.3
```
Ensuite, allez dans le répertoire principal de l'application, et configurez-la
comme n'importe quelle application Rails :
```bash
# Optionnellement, configurez rvm avec ruby 1.9.3 minimum (j'utilise 2.3.0)
rvm install 2.3.0
rvm use 2.3.0
# installez les dépendances
cd /chemin/vers/discourse
RAILS_ENV=production bundle install
```
Il est temps de configurer l'application.
Ici, Discourse a une petite particularité : la configuration de production se
trouve dans le fichier `./config/discourse.conf`.
Créez ce fichier :
```bash
cp config/discourse_defaults.conf config/discourse.conf
```
Et modifiez-le avec votre configuration. Les principales zones d'intérêt sont
la configuration pour la base de données et pour le serveur de messagerie :
```ini
# adresse hôte pour le serveur de base de données
# Ceci est défini à vide pour qu'il essaie d'utiliser les sockets en premier
db_host = localhost
# port du serveur de base de données, pas besoin de le définir
db_port = 5432
# nom de la base de données exécutant discourse
db_name = discourse
# nom d'utilisateur accédant à la base de données
db_username = discourse
# mot de passe utilisé pour accéder à la base de données
db_password = password
```
et pour le serveur SMTP (dans cet exemple, nous utilisons Gmail) :
```ini
# adresse du serveur smtp utilisé pour envoyer des emails
smtp_address = smtp.gmail.com
# port du serveur smtp utilisé pour envoyer des emails
smtp_port = 587
# domaine passé au serveur smtp
smtp_domain = gmail.com
# nom d'utilisateur pour le serveur smtp
smtp_user_name = votre-adresse@gmail.com
# mot de passe pour le serveur smtp
smtp_password = password
# mécanisme d'authentification smtp
smtp_authentication = plain
# activer le chiffrement TLS pour les connexions smtp
smtp_enable_start_tls = true
```
Maintenant, nous pouvons préparer Discourse pour la production :
```bash
RAILS_ENV=production bundle exec rake db:migrate
RAILS_ENV=production bundle exec rake assets:precompile
```
Il est temps de démarrer l'application. J'utilise généralement Puma pour
déployer les applications Rails.
Créez le fichier `config/puma.rb` dans le répertoire de Discourse. Le contenu
suivant devrait suffire (pour plus d'informations, voir
[la documentation de Puma]) :
```ruby
#!/usr/bin/env puma
application_path = '/home/discuss.waarp.org/discourse'
directory application_path
environment 'production'
daemonize false
pidfile "#{application_path}/tmp/pids/puma.pid"
state_path "#{application_path}/tmp/pids/puma.state"
bind "unix://#{application_path}/tmp/sockets/puma.socket"
```
À partir de là, l'application peut être exécutée avec la commande suivante :
```bash
bundle exec puma -C config/puma.rb
```
Enfin, configurez Nginx pour transférer les requêtes à Discourse. Créez le
fichier `/etc/nginx/conf.d/discourse.conf` avec le contenu suivant :
```nginx
upstream discourse {
server unix:/chemin/vers/discourse/tmp/sockets/puma.socket;
}
server {
listen 80;
server_name example.com;
location / {
try_files $uri @proxy;
}
location @proxy {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://discourse;
}
}
```
Votre forum avec Discourse est configuré !
## Gestion des services
Selon vos habitudes de travail, vous pouvez ajouter des unités systemd pour
exécuter Discourse.
Il nécessite au moins deux service :
1. Sidekiq, qui est utilisé pour traiter les tâches de fond asynchrones
2. Rails, pour Discourse lui-même.
Avec les services configurés, les services peuvent être démarrés/arrêtés/activés
avec les commandes `systemctl`.
Mais avant cela, si vous utilisez RVM, vous devez créer un wrapper pour
l'environnement (ruby local, et gemset optionnel) utilisé par Discourse :
```bash
rvm wrapper 2.3.0 systemd bundle
```
Cela crée un exécutable dans `$rvm_bin_path` que vous pouvez appeler
à la place de bundle, et qui chargera automatiquement le bon environnement.
### Sidekiq
Tout d'abord, créez une configuration pour Sidekiq. Créez le fichier
`config/sidekiq.yml` dans votre projet Discourse avec le contenu
suivant (pour plus d'informations, voir [la documentation de Sidekiq]) :
```yaml
---
:concurrency: 5
:pidfile: tmp/pids/sidekiq.pid
staging:
:concurrency: 10
production:
:concurrency: 20
:queues:
- default
- critical
- low
```
Ensuite, créez l'unité de service pour Sidekiq. Créez le fichier
`/etc/systemd/system/discourse-sidekiq.service` avec le
contenu suivant :
```ini
[Unit]
Description=service sidekiq de discourse
After=multi-user.target
[Service]
WorkingDirectory=/chemin/vers/discourse
Environment=RAILS_ENV=production
ExecStart=/chemin/vers/rvm/.rvm/bin/systemd_bundle exec sidekiq -C config/sidekiq.yml
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
```
### Discourse
Pour Discourse, créez simplement l'unité de service pour Puma. Créez le fichier
`/etc/systemd/system/discourse.service` avec le contenu suivant :
```ini
[Unit]
Description=service discourse
After=discourse-sidekiq.service
Requires=discourse-sidekiq.service
[Service]
WorkingDirectory=/chemin/vers/discourse
Environment=RAILS_ENV=production
ExecStart=/chemin/vers/rvm/.rvm/bin/systemd_bundle exec puma -C config/puma.rb
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
```
## Mises à jour
Les mises à jour sont encore plus faciles :
Lisez d'abord les notes de version.
Faites ensuite des sauvegardes du code et de la base de données.
Maintenant, vous pouvez basculer vers la nouvelle version :
```bash
cd /chemin/vers/discourse
git checkout vX.X.X
```
Installez les nouvelles dépendances, exécutez les migrations et reconstruisez les
assets :
```bash
RAILS_ENV=production bundle install
RAILS_ENV=production bundle exec rake db:migrate
RAILS_ENV=production bundle exec rake assets:precompile
```
Redémarrez Discourse :
```bash
systemctl restart discourse
```
Que peut-il mal se passer ? Si je ne donne aucune solution ici, c'est toujours
récupérable (d'où les sauvegardes !).
- La migration de la base de données a échoué (restaurez la base de données avec
votre sauvegarde, corrigez le problème et réessayez !)
- Les plugins ne sont pas compatibles avec la dernière version (revenez à
la solution précédente fonctionnelle et attendez qu'ils soient compatibles)
## Plugins
Les plugins Discourse peuvent être gérés de la même manière.
### Installation de plugins
Installez le plugin avec l'URL de son dépôt :
```bash
cd /chemin/vers/discourse
RAILS_ENV=production bundle exec rake plugin:install[URL]
```
Installez les nouvelles dépendances, exécutez les migrations et reconstruisez les
assets :
```bash
RAILS_ENV=production bundle install
RAILS_ENV=production bundle exec rake db:migrate
RAILS_ENV=production bundle exec rake assets:precompile
```
Redémarrez Discourse :
```bash
systemctl restart discourse
```
### Mise à jour
Pour mettre à jour un plugin spécifique, utilisez la commande suivante :
```bash
RAILS_ENV=production bundle exec rake plugin:update[ID]
```
Vous pouvez également mettre à jour tous les plugins en une seule fois avec la commande :
```bash
RAILS_ENV=production bundle exec rake plugin:update_all
```
Ensuite, installez les nouvelles dépendances, exécutez les migrations et reconstruisez les
assets :
```bash
RAILS_ENV=production bundle install
RAILS_ENV=production bundle exec rake db:migrate
RAILS_ENV=production bundle exec rake assets:precompile
```
et redémarrez Discourse :
```bash
systemctl restart discourse
```
[Discourse]: http://www.discourse.org/
[Documentation de Sidekiq]: https://github.com/mperham/sidekiq/wiki/Advanced-Options
[Documentation de Puma]: https://github.com/puma/puma

View file

@ -2,7 +2,11 @@
title: 'Prepare for the Next Internet Outage' title: 'Prepare for the Next Internet Outage'
slug: '7-prepare-for-the-next-internet-outage' slug: '7-prepare-for-the-next-internet-outage'
date: '2025-06-14T04:05:48+02:00' date: '2025-06-14T04:05:48+02:00'
tags: [architecture, cloud] categories:
- DevOps
tags:
- architecture,
- cloud
summary: > summary: >
A reflection on recent internet outages and my takeways to build more A reflection on recent internet outages and my takeways to build more
resilient web services. resilient web services.
@ -59,7 +63,7 @@ least one of these vendors.
The cloud and API world we live in is great. It allows us to build fast, iterate The cloud and API world we live in is great. It allows us to build fast, iterate
quickly, test things and improve our solutions. You need authentication, use quickly, test things and improve our solutions. You need authentication, use
Subabase or Auth0. Online payment? There is Stripe or Paypal. Transactional Supabase or Auth0. Online payment? There is Stripe or Paypal. Transactional
emails? Sendgrid and MailChimp. Search? Algolia. The list can be long, but now, emails? Sendgrid and MailChimp. Search? Algolia. The list can be long, but now,
you can work on creating value. you can work on creating value.

View file

@ -0,0 +1,136 @@
---
title: Préparez-vous pour la prochaine panne d'Internet
slug: '7-preparez-vous-pour-la-prochaine-panne-d-internet'
date: '2025-06-14T04:05:48+02:00'
categories:
- DevOps
tags:
- architecture
- cloud
summary: >
Une réflexion sur les récentes pannes d'Internet et les enseignements que j'en
tire pour construire des services web plus résilients.
---
Jeudi dernier, [Internet est tombé en panne](https://mashable.com/article/google-down-cloudflare-twitch-character-ai-internet-outage).
Encore une fois. Oui, les médias ont transformé une panne de deux heures en une
crise mondiale pour faire du putaclic.
Ce qui a rendu cet incident significatif, ce n'est pas seulement la perturbation
de Google Cloud, mais aussi les centaines de sites web et d'applications qui
sont tombés en panne en même temps. Cela incluait certains grands noms comme
Cloudflare, qui utilise GCP pour certains de ses services. Cloudflare étant un
CDN, un cache et un proxy largement répandus, cela a créé un effet domino et a,
à son tour, fait tomber d'innombrables sites web.
Cela nous rappelle la fragile interconnection de notre monde numérique. Je ne
veux pas pointer du doigt, mais plutôt tirer des leçons de cet incident. Ce
n'était pas juste un incident ; cela a mis en lumière des principes
fondamentaux que, à l'ère du "tout en tant que service", nous avons peut-être
involontairement négligés.
Voici mes principaux enseignements.
## Ne mettez pas tous vos œufs dans le même panier de fournisseurs
Le cloud est devenu synonyme d'une mise à l'échelle infinie, d'un stockage
infini, d'une puissance de calcul infinie, d'une flexibilité infinie. Il est
construit sur la promesse de réduire les coûts (ce qui peut être vrai lorsqu'il
est utilisé correctement). Cependant, cela cache une vérité souvent négligée,
qui est aussi son plus grand risque : la dépendance à un seul fournisseur. La
récente panne a montré comment une panne d'un seul fournisseur, ou même d'un
composant de leur infrastructure, peut avoir un effet en cascade sur la plupart
des services.
Ajoutons à cela que
[AWS, Azure et Google Cloud Platform ont une part de marché combinée de 63 % en valeur](https://www.crn.com/news/cloud/2025/cloud-market-share-q1-2025-aws-dips-microsoft-and-google-show-growth?page=1&itc=refresh).
Même si votre entreprise n'utilise pas directement ces fournisseurs
d'infrastructure, il est probable que vous utilisiez des fournisseurs qui
dépendent d'eux, ou des fournisseurs qui pourraient dépendre d'eux. Oui, il est
probable que votre application SaaS dépende d'au moins l'un de ces fournisseurs.
**Ce que vous pouvez faire** :
* *Cartographiez vos dépendances* : Connaissez-vous vraiment tous les services
sur lesquels repose votre produit, directement et indirectement ? Quels IaaS,
PaaS, API, CDN, etc., utilisez-vous ? Lesquels ces services utilisent-ils à
leur tour ? Dépendez-vous de NpmJS pour construire votre produit ? Votre
application est-elle déployée avec une action Github ? Plus vous en savez,
mieux vous êtes préparé.
* *Faites une évaluation approfondie de vos fournisseurs* : Les garanties de
disponibilité (3 ? 4 ? 5 neufs ?) ne sont que du marketing. Prenez-les comme
tel. Quelle est l'architecture de votre fournisseur ? Son plan de continuité ?
Sa transparence sur les incidents ? Ce sont des critères bien plus importants.
* *Envisagez des stratégies multi-cloud* : Vous ne mettriez pas tous vos
serveurs dans le même centre de données ? Alors ne mettez pas toute votre
infrastructure chez le même fournisseur IaaS ! (Si vous le feriez, vous
devriez faire quelque chose à ce sujet !)
## Contrôlez vos données, contrôlez votre entreprise
Le monde du cloud et des API dans lequel nous vivons est formidable. Il nous
permet de construire rapidement, d'itérer rapidement, de tester des choses et
d'améliorer nos solutions. Vous avez besoin d'une authentification, utilisez
Supabase ou Auth0. Paiement en ligne ? Il y a Stripe ou Paypal. E-mails
transactionnels ? Sendgrid et MailChimp. Recherche ? Algolia. La liste peut être
longue, mais maintenant, vous pouvez travailler à créer de la valeur.
Pourtant, comme l'a montré cette panne, si ces services deviennent
indisponibles, vos utilisateurs pourraient être bloqués, ou votre application
pourrait cesser de fonctionner, indépendamment de la santé de votre propre
infrastructure. Cela peut entraîner une perte significative de contrôle sur les
opérations commerciales de base et l'accès aux données. Les services tiers SONT
des points de défaillance uniques !
**Ce que vous pouvez faire** :
* *Mécanismes de repli pour les services principaux* : Si un service devient
indisponible, comment le remplacez-vous ? Pouvez-vous développer une
alternative de repli ?
* *Mirroring de données robuste* : Assurez-vous d'avoir des sauvegardes
régulières et accessibles de vos données critiques, même si elles résident
principalement chez un tiers. Pouvez-vous les restaurer rapidement dans un
environnement différent si nécessaire ?
## Construire pour la résilience
La résilience a toujours été une conséquence de la redondance. Vous devriez
toujours avoir un système de secours qui peut assurer le service pendant que
votre système principal est en panne.
Mais il ne suffit pas d'avoir de la redondance. Votre application doit également
être conçue pour être tolérante aux pannes et utiliser tout ou partie du système
de secours lorsque nécessaire. Au moins, elle doit garantir que l'impact pour
vos utilisateurs soit le moindre possible : l'impossibilité d'envoyer un e-mail
ne doit jamais bloquer toute votre application.
**Ce que vous pouvez faire** :
* *Architectures distribuées* : Concevez vos systèmes avec des architectures de
type microservice. Déployez vos services sur plusieurs fournisseurs IaaS.
Répliquez les données critiques sur plusieurs fournisseurs. L'objectif est de
limiter l'impact de toute défaillance d'un seul composant.
* *Systèmes auto-cicatrisants* : Mettez en place des mécanismes qui peuvent
détecter automatiquement les défaillances, réacheminer le trafic ou redémarrer
les services sans intervention humaine. Plus votre système peut réagir
rapidement, moins une panne aura d'impact.
* *Concevoir pour l'échec* : N'attendez pas qu'un événement externe expose vos
faiblesses. Il sera trop tard. Ajoutez quelques tests de défaillance
automatisés à votre pipeline CI : que se passe-t-il si le client a une latence
de 5 secondes avec votre serveur ? Que se passe-t-il si la base de données est
indisponible ? Que se passe-t-il si un paiement ne peut pas être traité
immédiatement ? Quelle est l'*expérience utilisateur* lorsque quelque chose ne
va pas ? Ces problèmes ARRIVERONT.
## Conclusion
La prochaine panne arrivera. C'est certain. Peut-être pas aussi importante, mais
il y en aura qui affecteront votre entreprise.
Soyez préparé :
* Connaissez votre infrastructure, vos fournisseurs, leurs fournisseurs, etc.
* Évaluez les risques régulièrement. Votre application évolue, vos fournisseurs
aussi. Ce qui est vrai à un moment ne l'est plus au suivant.
* Prévoyez le pire des cas. Les incidents arriveront. Votre travail est de faire
en sorte que l'expérience utilisateur ne soit pas impactée.

View file

@ -1,6 +0,0 @@
+++
title = 'Blog'
date = 2025-06-04T23:00:00Z
draft = false
+++

View file

@ -0,0 +1,5 @@
---
title: Outils
translationKey: tooling
---

View file

@ -0,0 +1,4 @@
---
title: Tooling
translationKey: tooling
---

View file

20
i18n/en.yaml Normal file
View file

@ -0,0 +1,20 @@
posted: Posted on
license: |
The content of this site is licensed under
<a href="{{ .link }}" rel="license">{{ .name }}</a>
generated_with_hugo: |
Generated with <a href="https://gohugo.io">Hugo</a> using a custom theme.
logo: Logo
mobile_menu: Mobile Menu
toggle_menu: Toggle the Main Menu
open_menu: Open the main menu
close_menu: Close the menu
main_menu: Main Menu
recent_posts: Recent Posts
note: Note
warning: Warning
permalink_section: "Permalink to this section"
tags: Tags
information_icon: Information icon
warning_icon: Warning icon
also_available_in: "Also available in:"

20
i18n/fr.yaml Normal file
View file

@ -0,0 +1,20 @@
posted: Posté le
license: |
Le contenu de ce site est publié selon les termes de la licence
<a href="{{ .link }}" rel="license">{{ .name }}</a>
generated_with_hugo: |
Généré avec <a href="https://gohugo.io">Hugo</a> en utilisant un thème personnalisé.
logo: Logo
mobile_menu: Menu mobile
toggle_menu: Ouvrir le menu
open_menu: Ouvrir le menu
close_menu: Fermer le menu
main_menu: Menu principal
recent_posts: Posts récents
note: Note
warning: Attention
permalink_section: Permalien vers cette partie
tags: Tags
information_icon: Icône information
warning_icon: Icône attention
also_available_in: "Également disponible en :"

View file

View file

@ -1,6 +1,6 @@
<h{{ .Level }} id="{{ .Anchor }}" {{- with .Attributes.class }} class="{{ . }}" {{- end }}> <h{{ .Level }} id="{{ .Anchor }}" {{- with .Attributes.class }} class="{{ . }}" {{- end }}>
{{ .Text }} {{ .Text }}
<a href="#{{ .Anchor }}"> <a href="#{{ .Anchor }}">
{{- partial "icon.html" (dict "icon" "links-line" "label" "Permalink to this section") -}} {{- partial "icon.html" (dict "icon" "links-line" "label" (T "permalink_section")) -}}
</a> </a>
</h{{ .Level }}> </h{{ .Level }}>

View file

@ -2,14 +2,13 @@
<p> <p>
© {{ now.Year }} Bruno Carlin © {{ now.Year }} Bruno Carlin
<br> <br>
The content of this site is licensed under {{ T "license" (dict
<a href="https://creativecommons.org/licenses/by-nc/4.0/" rel="license"> "link" "https://creativecommons.org/licenses/by-nc/4.0/"
Creative Commons Attribution-NonCommercial 4.0 "name" "Creative Commons Attribution-NonCommercial 4.0 International"
International ) | safeHTML }}
</a>
{{partial "icon.html" (dict "icon" "creative-commons-fill" "label" "Creative Commons Logo")}} {{partial "icon.html" (dict "icon" "creative-commons-fill" "label" "Creative Commons Logo")}}
{{partial "icon.html" (dict "icon" "creative-commons-by-fill" "label" "Creative Commons Attribution Logo")}} {{partial "icon.html" (dict "icon" "creative-commons-by-fill" "label" "Creative Commons Attribution Logo")}}
{{partial "icon.html" (dict "icon" "creative-commons-nc-fill" "label" "Creative Commons Non Commercial Logo")}} {{partial "icon.html" (dict "icon" "creative-commons-nc-fill" "label" "Creative Commons Non Commercial Logo")}}
<br> <br>
Generated with <a href="https://gohugo.io">Hugo</a> using a custom theme. {{ T "generated_with_hugo" | safeHTML }}
</p> </p>

View file

@ -1,5 +1,15 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1"/>
{{ hugo.Generator }}
<title>{{ if .IsHome }}{{ site.Title }}{{ else }}{{ printf "%s | %s" .Title site.Title }}{{ end }}</title>
<meta name="description" content="{{ default .Summary .Description }}">
<link rel="canonical" href="{{ .Permalink }}"/>
{{- if .IsTranslated }}
{{- range .Translations }}
<link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .RelPermalink }}"/>
{{- end }}
{{- end }}
{{ with .Site.GetPage "/blog" }} {{ with .Site.GetPage "/blog" }}
{{- with .OutputFormats.Get "rss" }} {{- with .OutputFormats.Get "rss" }}
@ -19,12 +29,8 @@
<meta name="apple-mobile-web-app-title" content="bcarlin.net" /> <meta name="apple-mobile-web-app-title" content="bcarlin.net" />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<title>{{ if .IsHome }}{{ site.Title }}{{ else }}{{ printf "%s | %s" .Title site.Title }}{{ end }}</title>
<meta name="description" content="{{ default .Summary .Description }}">
<link rel="canonical" href="{{ .Permalink }}"/>
{{ template "_internal/opengraph.html" . }} {{ template "_internal/opengraph.html" . }}
{{ template "_internal/schema.html" . }} {{- template "_internal/schema.html" . }}
{{ partialCached "head/css.html" . }} {{ partialCached "head/css.html" . }}
{{ partialCached "head/js.html" . }} {{- partialCached "head/js.html" . }}

View file

@ -14,4 +14,6 @@
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- if not hugo.IsDevelopment }}
<script defer data-domain="bcarlin.net" src="//stats.bcarlin.net/js/script.js"></script> <script defer data-domain="bcarlin.net" src="//stats.bcarlin.net/js/script.js"></script>
{{- end }}

View file

@ -0,0 +1,22 @@
<a id="menu-close">
{{partial "icon.html" (dict "icon" "close-large-fill" "label" (T `close_menu`))}}
</a>
<nav id="main-menu" aria-label="{{ T `main_menu` }}">
<ul>
<li>
<a href="{{ .Site.Home.Permalink }}" class="contrast">
<img class="u-logo" src="/static/logo.svg" alt="{{T `logo`}}"/>
</a>
</li>
<li>
<span class="title p-name">
<a href="{{ .Site.Home.Permalink }}" class="contrast">{{ .Site.Title }}</a>
</span>
</li>
</ul>
{{ partial "menu.html" (dict "menuID" "main" "class" "menu-primary" "page" .) }}
{{ partial "menu.html" (dict "menuID" "secondary" "class" "menu-secondary" "page" .) }}
</nav>

View file

@ -0,0 +1,9 @@
{{- with $terms := .GetTerms "tags" }}
<p class="tags">
{{ partial "icon.html" (dict "icon" "price-tag-3-line" "label" (T "tags")) }}
{{- range $idx, $it := $terms }}
<a href="{{ $it.RelPermalink }}" rel="tag" class="p-category">{{ $it.LinkTitle }}</a>
{{- if ne $idx (sub $terms.Len 1) }}<span aria-hidden="true">, </span>{{ end }}
{{- end }}
</p>
{{- end }}

54
layouts/baseof.html Normal file
View file

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="{{ site.Language.LanguageCode }}" dir="{{ or site.Language.LanguageDirection `ltr` }}">
<head>
{{ partial "head.html" . }}
</head>
<body>
<nav class="mobile-header" aria-label="{{ T `mobile_menu` }}">
<ul>
<li>
<a href="{{ .Site.Home.Permalink }}">
<img src="/static/logo.svg" alt="{{ T `logo` }}"/>
</a>
</li>
</ul>
<ul>
<li>
<a href="{{ .Site.Home.Permalink }}" class="contrast">{{ .Site.Title }}</a>
</li>
</ul>
<ul>
<li>
<a
id="menu-toggle"
aria-controls="menu"
aria-expanded="false"
aria-label="{{ T `toggle_menu` }}"
>
{{partial "icon.html" (dict "icon" "menu-fill" "label" (T `open_menu`))}}
</a>
</li>
</ul>
</nav>
<header id="menu" class="h-card">
{{ partial "header.html" . }}
</header>
<main class="container">
{{- if .IsTranslated }}
{{- range .Translations }}
<div class="translations">
{{ T `also_available_in` }}
<a hreflang="{{ .Language.LanguageCode }}" href="{{ .RelPermalink }}" title="{{ .Language.LanguageName }}">
{{ if eq .Language.Lang "en" }}🇬🇧{{ else if eq .Language.Lang "fr" }}🇫🇷{{ else }}{{ .Language.LanguageName }}{{ end }}
</a>
</div>
{{- end }}
{{- end }}
{{ block "main" . }}{{ end }}
</main>
<footer class="container">
{{ partial "footer.html" . }}
</footer>
</body>
</html>

View file

@ -1,5 +1,5 @@
{{ $dateMachine := .Date | time.Format "2006-01-02T15:04:05-07:00" }} {{ $dateMachine := .Date | time.Format "2006-01-02T15:04:05-07:00" }}
{{ $dateHuman := .Date | time.Format "2006-01-02" }} {{ $dateHuman := .Date | time.Format ":date_short" }}
<article class="h-entry"> <article class="h-entry">
<p> <p>
<a class="u-url p-name" href="{{ .RelPermalink }}">{{ .LinkTitle }}</a> <a class="u-url p-name" href="{{ .RelPermalink }}">{{ .LinkTitle }}</a>

View file

@ -1,15 +1,24 @@
{{ define "main" }} {{ define "main" }}
{{ $dateMachine := .Date | time.Format "2006-01-02T15:04:05-07:00" }} {{ $dateMachine := .Date | time.Format "2006-01-02T15:04:05-07:00" }}
{{ $dateHuman := .Date | time.Format "2006-01-02" }} {{ $dateHuman := .Date | time.Format ":date_short" }}
<article class="h-entry"> <article class="h-entry">
<header> <header>
<h1 class="p-name">{{ .Title }}</h1> {{ with .GetTerms "categories" }}
<p class="category">
{{ range . }}
<a class="p-category" rel="tag" href="{{ .RelPermalink }}">{{ .LinkTitle }}</a>
{{ break }}
{{ end }}
</p>
{{ end }}
<h1 class="p-name">
{{ .Title }}
</h1>
<div class="metadata"> <div class="metadata">
<p> <p>
Posted on {{ T "posted" }}
<time class="dt-published" datetime="{{ $dateMachine }}">{{ $dateHuman }}</time> <time class="dt-published" datetime="{{ $dateMachine }}">{{ $dateHuman }}</time>
</p> </p>
<p> :: </p>
{{ partial "tags.html" . }} {{ partial "tags.html" . }}
</div> </div>
</header> </header>

View file

@ -7,7 +7,7 @@
{{ .Content }} {{ .Content }}
<section class="h-feed"> <section class="h-feed">
<h2 class="p-name">Recent posts</h2> <h2 class="p-name">{{ T "recent_posts" }}</h2>
{{- $posts := where .Site.RegularPages "Section" "blog" }} {{- $posts := where .Site.RegularPages "Section" "blog" }}
{{- range first 5 $posts }} {{- range first 5 $posts }}
{{ .Render "list-item" }} {{ .Render "list-item" }}

View file

@ -1,11 +1,11 @@
{{ define "main" }} {{ define "main" }}
{{ $dateMachine := .Date | time.Format "2006-01-02T15:04:05-07:00" }} {{ $dateMachine := .Date | time.Format "2006-01-02T15:04:05-07:00" }}
{{ $dateHuman := .Date | time.Format "2006-01-02" }} {{ $dateHuman := .Date | time.Format ":date_short" }}
<article class="h-entry"> <article class="h-entry">
<header> <header>
<h1 class="p-name">{{ .Title }}</h1> <h1 class="p-name">{{ .Title }}</h1>
<p> <p>
Posted on {{ T "posted" }}
<time class="dt-published" datetime="{{ $dateMachine }}">{{ $dateHuman }}</time> <time class="dt-published" datetime="{{ $dateMachine }}">{{ $dateHuman }}</time>
</p> </p>
</header> </header>
@ -13,6 +13,5 @@
<section class="e-content"> <section class="e-content">
{{ .Content }} {{ .Content }}
</section> </section>
{{ partial "terms.html" (dict "taxonomy" "tags" "page" .) }}
</article> </article>
{{ end }} {{ end }}

View file

@ -0,0 +1,7 @@
<aside class="admonition note">
<p class="title">
{{partial "icon.html" (dict "icon" "information-fill" "label" (T "information_icon"))}}
{{ T "note" }}
</p>
{{ .Inner | .Page.RenderString }}
</aside>

View file

@ -0,0 +1,7 @@
<aside class="admonition warning">
<p class="title">
{{partial "icon.html" (dict "icon" "error-warning-fill" "label" (T "warning_icon")) -}}
{{ T "warning" }}
</p>
{{ .Inner | .Page.RenderString }}
</aside>

View file

@ -6,7 +6,7 @@
{{ .Content }} {{ .Content }}
{{ range .Pages.ByDate }} {{ range .Pages.ByPublishDate.Reverse }}
{{ .Render "list-item" }} {{ .Render "list-item" }}
{{ end }} {{ end }}
</article> </article>

View file

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View file

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -1,5 +0,0 @@
+++
date = '{{ .Date }}'
draft = true
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
+++

View file

@ -1,9 +0,0 @@
+++
title = 'Home'
date = 2023-01-01T08:00:00-07:00
draft = false
+++
Laborum voluptate pariatur ex culpa magna nostrud est incididunt fugiat
pariatur do dolor ipsum enim. Consequat tempor do dolor eu. Non id id anim anim
excepteur excepteur pariatur nostrud qui irure ullamco.

View file

@ -1,24 +0,0 @@
baseURL = 'https://example.org/'
languageCode = 'en-US'
title = 'My New Hugo Site'
[menus]
[[menus.main]]
name = 'Home'
pageRef = '/'
weight = 10
[[menus.main]]
name = 'Posts'
pageRef = '/posts'
weight = 20
[[menus.main]]
name = 'Tags'
pageRef = '/tags'
weight = 30
[module]
[module.hugoVersion]
extended = false
min = '0.146.0'

View file

@ -1,20 +0,0 @@
<a id="menu-close">
{{partial "icon.html" (dict "icon" "close-large-fill" "label" "Close the menu")}}
</a>
<nav id="main-menu" aria-label="Main menu">
<ul>
<li>
<a href="/" class="contrast">
<img class="u-logo" src="/static/logo.svg" alt="Logo"/>
</a>
</li>
<li>
<span class="title p-name"><a href="/" class="contrast">Bruno Carlin</a></span>
</li>
</ul>
{{ partial "menu.html" (dict "menuID" "main" "class" "menu-primary" "page" .) }}
{{ partial "menu.html" (dict "menuID" "secondary" "class" "menu-secondary" "page" .) }}
</nav>

View file

@ -1,10 +0,0 @@
{{- with .GetTerms "tags" }}
<div>
{{ partial "icon.html" (dict "icon" "hashtag" "label" "Tags") }}
<ul class="tags">
{{- range . }}
<li><a href="{{ .RelPermalink }}" rel="tag" class="p-category">{{ .LinkTitle }}</a></li>
{{- end }}
</ul>
</div>
{{- end }}

View file

@ -1,38 +0,0 @@
<!DOCTYPE html>
<html lang="{{ site.Language.LanguageCode }}" dir="{{ or site.Language.LanguageDirection `ltr` }}">
<head>
{{ partial "head.html" . }}
</head>
<body>
<nav class="mobile-header" aria-label="Mobile menu">
<ul>
<li><a href="/"><img src="/static/logo.svg" alt="Logo"/></a></li>
</ul>
<ul>
<li><a href="/" class="contrast">Bruno Carlin</a></li>
</ul>
<ul>
<li>
<a
id="menu-toggle"
role="button"
aria-controls="menu"
aria-expanded="false"
aria-label="Toggle the main menu"
>
{{partial "icon.html" (dict "icon" "menu-fill" "label" "Open the menu")}}
</a>
</li>
</ul>
</nav>
<header id="menu" class="h-card">
{{ partial "header.html" . }}
</header>
<main class="container">
{{ block "main" . }}{{ end }}
</main>
<footer class="container">
{{ partial "footer.html" . }}
</footer>
</body>
</html>

View file

@ -1,4 +0,0 @@
<aside class="admonition note">
<p class="title">{{partial "icon.html" (dict "icon" "information-fill" "label" "information")}} Note</p>
{{ .Inner | .Page.RenderString }}
</aside>

View file

@ -1,4 +0,0 @@
<aside class="admonition warning">
<p class="title">{{partial "icon.html" (dict "icon" "error-warning-fill" "label" "warning")}} Warning</p>
{{ .Inner | .Page.RenderString }}
</aside>