feat: translate the site in french
|
@ -1,5 +1,13 @@
|
|||
+++
|
||||
date = '{{ .Date }}'
|
||||
draft = true
|
||||
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
|
||||
+++
|
||||
---
|
||||
title: '{{ replace .File.ContentBaseName "-" " " | title }}'
|
||||
slug: '{{ .File.ContentBaseName }}'
|
||||
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-left: 18em;
|
||||
}
|
||||
|
||||
.metadata p {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -224,6 +228,15 @@ body > footer p {
|
|||
* Content Styling
|
||||
*/
|
||||
|
||||
.translations {
|
||||
text-align: end;
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.translations a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
main > article {
|
||||
text-align: justify;
|
||||
}
|
||||
|
@ -232,11 +245,6 @@ main > article > header {
|
|||
margin-bottom: calc(2 * var(--pico-block-spacing-vertical));
|
||||
}
|
||||
|
||||
.metadata {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
ul.tags, ul.tags li {
|
||||
list-style-type: none;
|
||||
display: inline;
|
||||
|
@ -244,10 +252,6 @@ ul.tags, ul.tags li {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tags li:not(:last-child)::after {
|
||||
content: ", ";
|
||||
}
|
||||
|
||||
ul > li {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
@ -261,11 +265,6 @@ h1 {
|
|||
font-size: 1.7rem;
|
||||
}
|
||||
|
||||
h1::before {
|
||||
content: "# ";
|
||||
color: var(--markup-color);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
@ -313,10 +312,21 @@ h6::before {
|
|||
|
||||
article header h1, article header p {
|
||||
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);
|
||||
text-decoration: none;
|
||||
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:
|
||||
- name: 'Home'
|
||||
pageRef: '/'
|
||||
|
@ -66,3 +33,4 @@ menus:
|
|||
class: "u-key"
|
||||
iconName: "key-fill"
|
||||
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
|
||||
date: "2010-09-15T00:00:00+02:00"
|
||||
tags:
|
||||
- Nginx
|
||||
- Mediawiki
|
||||
slug: 1-setup-nginx-for-mediawiki
|
||||
date: "2010-09-15T00:00:00+02:00"
|
||||
categories: [DevOps]
|
||||
tags:
|
||||
- mediawiki
|
||||
- nginx
|
||||
summary: >
|
||||
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
|
||||
date: "2010-12-14T00:00:00+01:00"
|
||||
tags: [Debian, PgPool-II]
|
||||
slug: 2-build-pgpool-on-debian
|
||||
slug: 2-build-pgpool-on-debian-etch
|
||||
categories: [DevOps]
|
||||
tags:
|
||||
- Debian
|
||||
- PgPool-II
|
||||
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
|
||||
---
|
||||
|
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
|
||||
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"
|
||||
summary: >
|
||||
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
|
||||
title: Locking Buzhug
|
||||
date: "2012-02-07T00:00:00+01:00"
|
||||
categories: [Dev]
|
||||
tags:
|
||||
- Python
|
||||
- Buzhug
|
||||
summary: >
|
||||
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
|
||||
perfectly. Here is a version slightly modified to make it a context manager :
|
||||
|
||||
{{< highlight python >}}
|
||||
```python
|
||||
import fcntl
|
||||
|
||||
class PsLock:
|
||||
|
@ -87,12 +90,12 @@ class PsLock:
|
|||
|
||||
def __enter__(self):
|
||||
self.acquire()
|
||||
{{< /highlight >}}
|
||||
```
|
||||
|
||||
The second step is to define a new class that inheritates from `buzhug.Base`
|
||||
that uses `PsLock` (inspired by `TS_Base`):
|
||||
|
||||
{{< highlight python >}}
|
||||
```python
|
||||
import buzhug
|
||||
|
||||
_lock = PsLock("/tmp/buzhug.lck")
|
||||
|
@ -158,12 +161,12 @@ class PS_Base(buzhug.Base):
|
|||
with _lock:
|
||||
res = buzhug.Base.drop_field(self, *args, **kw)
|
||||
return res
|
||||
{{< /highlight >}}
|
||||
```
|
||||
|
||||
Now I just use
|
||||
|
||||
{{< highlight python >}}
|
||||
```python
|
||||
database = PS_Base( ... )
|
||||
{{< /highlight >}}
|
||||
```
|
||||
|
||||
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
|
||||
slug: 5-automatically-open-sublime-text-projects-in-a-directory
|
||||
date: "2013-05-15T00:00:00+02:00"
|
||||
categories:
|
||||
- Tooling
|
||||
tags:
|
||||
- Sublime Text
|
||||
- Bash
|
||||
summary: >
|
||||
How to automatically open Sublimetext with a file, a project or the current
|
||||
directory according to the context.
|
||||
|
@ -17,19 +21,19 @@ It ends up with one of the following commands :
|
|||
- `subl .`
|
||||
- `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:
|
||||
|
||||
- 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 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
|
||||
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.
|
||||
|
||||
{{< highlight bash >}}
|
||||
```bash
|
||||
function project_aware_subl {
|
||||
project_file=$(ls *.sublime-project 2>/dev/null | head -n 1)
|
||||
subl ${*:-${project_file:-.}}
|
||||
}
|
||||
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
|
||||
slug: 6-discourse-without-docker
|
||||
date: "2016-06-27T00:00:00+02:00"
|
||||
tags: [discourse, docker]
|
||||
summary: Detailed instructions on how to install Discourse and plugins without Docker.
|
||||
categories:
|
||||
- DevOps
|
||||
tags:
|
||||
- discourse
|
||||
- docker
|
||||
summary: >
|
||||
Detailed instructions on how to install Discourse and plugins without Docker.
|
||||
---
|
||||
|
||||
{{< warning >}}
|
||||
|
@ -277,8 +282,7 @@ WantedBy=multi-user.target
|
|||
### Discourse
|
||||
|
||||
For Discourse, just create the service unit for Puma. Create the file
|
||||
`/etc/systemd/system/discourse.service` with the
|
||||
following content:
|
||||
`/etc/systemd/system/discourse.service` with the following content:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
|
@ -325,13 +329,13 @@ 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!).
|
||||
|
||||
- The database migration failed (restore the database with your backup,
|
||||
fix the problem and try again!)
|
||||
- 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
|
||||
|
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'
|
||||
slug: '7-prepare-for-the-next-internet-outage'
|
||||
date: '2025-06-14T04:05:48+02:00'
|
||||
tags: [architecture, cloud]
|
||||
categories:
|
||||
- DevOps
|
||||
tags:
|
||||
- architecture,
|
||||
- cloud
|
||||
summary: >
|
||||
A reflection on recent internet outages and my takeways to build more
|
||||
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
|
||||
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,
|
||||
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 }}>
|
||||
{{ .Text }}
|
||||
<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>
|
||||
</h{{ .Level }}>
|
|
@ -2,14 +2,13 @@
|
|||
<p>
|
||||
© {{ now.Year }} Bruno Carlin
|
||||
<br>
|
||||
The content of this site is licensed under
|
||||
<a href="https://creativecommons.org/licenses/by-nc/4.0/" rel="license">
|
||||
Creative Commons Attribution-NonCommercial 4.0
|
||||
International
|
||||
</a>
|
||||
{{ T "license" (dict
|
||||
"link" "https://creativecommons.org/licenses/by-nc/4.0/"
|
||||
"name" "Creative Commons Attribution-NonCommercial 4.0 International"
|
||||
) | safeHTML }}
|
||||
{{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-nc-fill" "label" "Creative Commons Non Commercial Logo")}}
|
||||
<br>
|
||||
Generated with <a href="https://gohugo.io">Hugo</a> using a custom theme.
|
||||
{{ T "generated_with_hugo" | safeHTML }}
|
||||
</p>
|
|
@ -1,5 +1,15 @@
|
|||
<meta charset="utf-8">
|
||||
<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 .OutputFormats.Get "rss" }}
|
||||
|
@ -19,12 +29,8 @@
|
|||
<meta name="apple-mobile-web-app-title" content="bcarlin.net" />
|
||||
<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/schema.html" . }}
|
||||
{{- template "_internal/schema.html" . }}
|
||||
|
||||
{{ partialCached "head/css.html" . }}
|
||||
{{ partialCached "head/js.html" . }}
|
||||
{{- partialCached "head/js.html" . }}
|
|
@ -14,4 +14,6 @@
|
|||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if not hugo.IsDevelopment }}
|
||||
<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" }}
|
||||
{{ $dateHuman := .Date | time.Format "2006-01-02" }}
|
||||
{{ $dateHuman := .Date | time.Format ":date_short" }}
|
||||
<article class="h-entry">
|
||||
<p>
|
||||
<a class="u-url p-name" href="{{ .RelPermalink }}">{{ .LinkTitle }}</a>
|
|
@ -1,15 +1,24 @@
|
|||
{{ define "main" }}
|
||||
{{ $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">
|
||||
<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">
|
||||
<p>
|
||||
Posted on
|
||||
{{ T "posted" }}
|
||||
<time class="dt-published" datetime="{{ $dateMachine }}">{{ $dateHuman }}</time>
|
||||
</p>
|
||||
<p> :: </p>
|
||||
{{ partial "tags.html" . }}
|
||||
</div>
|
||||
</header>
|
|
@ -7,7 +7,7 @@
|
|||
{{ .Content }}
|
||||
|
||||
<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" }}
|
||||
{{- range first 5 $posts }}
|
||||
{{ .Render "list-item" }}
|
|
@ -1,11 +1,11 @@
|
|||
{{ define "main" }}
|
||||
{{ $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">
|
||||
<header>
|
||||
<h1 class="p-name">{{ .Title }}</h1>
|
||||
<p>
|
||||
Posted on
|
||||
{{ T "posted" }}
|
||||
<time class="dt-published" datetime="{{ $dateMachine }}">{{ $dateHuman }}</time>
|
||||
</p>
|
||||
</header>
|
||||
|
@ -13,6 +13,5 @@
|
|||
<section class="e-content">
|
||||
{{ .Content }}
|
||||
</section>
|
||||
{{ partial "terms.html" (dict "taxonomy" "tags" "page" .) }}
|
||||
</article>
|
||||
{{ 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 }}
|
||||
|
||||
{{ range .Pages.ByDate }}
|
||||
{{ range .Pages.ByPublishDate.Reverse }}
|
||||
{{ .Render "list-item" }}
|
||||
{{ end }}
|
||||
</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>
|