feat: translate the site in french
|
@ -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
|
||||||
|
---
|
||||||
|
|
|
@ -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
|
@ -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
|
|
@ -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"
|
||||||
|
|
36
config/_default/menus.fr.yaml
Normal 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"
|
||||||
|
|
8
config/development/hugo.yaml
Normal 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
|
@ -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.
|
|
@ -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
|
||||||
---
|
---
|
47
content/blog/001-setup-nginx-for-mediawiki/index.fr.md
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
|
@ -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
|
||||||
---
|
---
|
||||||
|
|
35
content/blog/002-build-pgpool-on-debian/index.fr.md
Normal 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 !
|
|
@ -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
|
93
content/blog/003-aptana-eclipse-and-xulrunner/index.fr.md
Normal 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`.
|
|
@ -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.
|
177
content/blog/004-locking-buzhug/index.fr.md
Normal 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.
|
|
@ -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 >}}
|
```
|
|
@ -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"
|
||||||
|
```
|
|
@ -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
|
||||||
|
|
402
content/blog/006-discourse-without-docker/index.fr.md
Normal 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
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
|
@ -1,6 +0,0 @@
|
||||||
+++
|
|
||||||
title = 'Blog'
|
|
||||||
date = 2025-06-04T23:00:00Z
|
|
||||||
draft = false
|
|
||||||
+++
|
|
||||||
|
|
5
content/categories/outils/_index.fr.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Outils
|
||||||
|
translationKey: tooling
|
||||||
|
---
|
||||||
|
|
4
content/categories/tooling/_index.en.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Tooling
|
||||||
|
translationKey: tooling
|
||||||
|
---
|
20
i18n/en.yaml
Normal 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
|
@ -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 :"
|
|
@ -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 }}>
|
|
@ -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>
|
|
@ -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" . }}
|
|
@ -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 }}
|
22
layouts/_partials/header.html
Normal 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>
|
||||||
|
|
9
layouts/_partials/tags.html
Normal 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
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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" }}
|
|
@ -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 }}
|
7
layouts/shortcodes/note.html
Normal 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>
|
7
layouts/shortcodes/warning.html
Normal 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>
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
{{ .Content }}
|
{{ .Content }}
|
||||||
|
|
||||||
{{ range .Pages.ByDate }}
|
{{ range .Pages.ByPublishDate.Reverse }}
|
||||||
{{ .Render "list-item" }}
|
{{ .Render "list-item" }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</article>
|
</article>
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
@ -1,5 +0,0 @@
|
||||||
+++
|
|
||||||
date = '{{ .Date }}'
|
|
||||||
draft = true
|
|
||||||
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
|
|
||||||
+++
|
|
|
@ -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.
|
|
|
@ -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'
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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 }}
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|