Cela fait des mois que je souhaite créer un blog minimaliste, design, performant et optimisé SEO mais je ne suis jamais passé à l’action et pour cause, je manque cruellement de temps.
Dans cet article je vous partage mes réflexions et surtout un guide comme raccourci vous permettant de faire de même.
Pourquoi Ghost ?
Après avoir envisagé WordPress, Strapi, et même SvelteKIT avec de simples fichiers Markdown, je me suis tourné vers Ghost qui :
- est basé sur Node JS
- offre de remarquables performances
- propose du Markdown
- intègre nativement certaines fonctionnalités essentielles
Tout cela en restant ridiculement simple aussi bien dans l’utilisation que l’installation et la maintenance.
C’est également Open Source, robuste, minimaliste et surtout très agréable à utiliser.
En bref cela colle parfaitement à mes besoins et mes valeurs.
Installation de Ghost sur un VPS
Je déteste dépendre d’un service tiers sur lequel je n’ai pas la main à 100% mais je dois avouer que l’auto hébergement colle aussi bien avec mon côté bricoleur et…Mon envie de tout contrôler. Du coup un VPS avec mes petites applications en « self-hosted », imaginez bien que pour un autiste comme moi c’est le top.
Prérequis
- Pour suivre ce guide vous devez connaître les bases de l'éditeur de texte "nano" et avoir installé une version récente de docker sur votre machine.
- Être en capacité d’ajouter un enregistrement de type IPV4 (A) sur son nom de domaine pour le faire pointer sur le VPS.
- Comprendre et maîtriser l’utilisation du serveur web permettant le reverse proxy et le SSL comme Caddy, Apache ou encore Nginx.
Création du docker compose
- Connectez-vous sur votre serveur via SSH et commencez par créer un dossier pour Ghost sur lequel vous aller vous positionner.
mkdir ghost
cd ghost
- Utilisez nano pour créer un fichier "docker-compose.yml".
nano docker-compose.yml
- Insérez ceci dans le fichier :
services:
ghost:
image: ghost:5-alpine
restart: always
ports:
- 3000:2368
environment:
# see https://ghost.org/docs/config/#configuration-options
database__client: mysql
database__connection__host: db
database__connection__user: root
database__connection__password: <votre-mot-de-passe>
database__connection__database: ghost
# this url value is just an example, and is likely wrong for your environment!
url: https://<nom-de-domaine>.com
# contrary to the default mentioned in the linked documentation, this image defaults to NODE_ENV=production (so development mode needs to be explicitl>
#NODE_ENV: development
volumes:
- ghost:/var/lib/ghost/content
- ./config.production.json:/var/lib/ghost/config.production.json
db:
image: mysql:8.0
restart: always
environment:
MYSQL_ROOT_PASSWORD: <votre-mot-de-passe>
volumes:
- db:/var/lib/mysql
phpmya:
image: phpmyadmin/phpmyadmin:latest
container_name: phpmyadmin2
environment:
PMA_ARBITRARY: 1
PMA_HOST: db
UPLOAD_LIMIT: 20M
ports:
- 8080:80
depends_on:
- db
restart: always
volumes:
ghost:
db:
Ce docker compose inclus à la fois le CMS Ghost mais également la base de données MySQL sur laquelle il repose et également PhpMyAdmin qui peut permettre certaines choses qui ne sont normalement pas possible. Je pense notamment à la suppression d’un commentaire indésirable sans qu’un message du type « Ce commentaire a été supprimé » n’apparaisse.
Créer le fichier de configuration Ghost
Ce fichier de configuration
Utilisez nano pour créer un fichier "config.production.json".
nano config.production.json
Insérez ceci dans le fichier :
{
"url": "http://localhost:2368",
"server": {
"port": 2368,
"host": "::"
},
"mail": {
"transport": "Direct"
},
"logging": {
"transports": [
"file",
"stdout"
]
},
"process": "systemd",
"paths": {
"contentPath": "/var/lib/ghost/content"
}
}
Lancer l’app
Il est temps de lancer notre application !
Exécutez la commande suivante.
docker compose up -d
Bravo ! Votre Ghost fonctionne, vous pouvez en attester en visitant cette adresse : « <ip-de-votre-vps>:3000/ghost ».
Configurer votre DNS et votre « reverse proxy »
C’est pas le tout d’avoir Ghost, il faut maintenant lui associer un nom de domaine.
J’ajoute donc un enregistrement dans la zone DNS de mon nom de domaine de type « A » qui contient l’ip de mon serveur afin de le faire pointer vers le VPS.
timtiret.com. 14400 IN A 185.158.107.160
Et je modifie la configuration de caddy pour ajouter une redirection de port :
timtiret.com {
reverse_proxy localhost:3000 {
flush_interval -1
}
}
Après avoir redémarré Caddy, mon blog est accessible depuis mon nom de domaine !
Traduction du thème
En tant que bon français, l’idée de rendre accessible le dev à d’autres qui ne parlent pas anglais me plaît bien. Pour être cohérent, j’ai donc décidé de traduire les boutons d’abonnement du thème « solo » que j’ai choisi.
Dans ce but, on va se connecteur sur le conteneur docker, installer nano et modifier le fichier thème.
Entrer dans le Shell du conteneur
- On cherche l’id du conteneur où est Ghost
docker ps
- On entre dans le terminal du conteneur en mode interactif (-it)
docker exec -it <id-du-conteneur> /bin/sh
Installation de nano sur Alpine
- On met à jour les dossiers et on installe nano
apk update
apk add nano
Modification du thème Ghost
- On va dans le dossier du thème que l’on souhaite modifier
cd /var/lib/ghost/content/themes/<votre-theme>
- On édite le fichier voulu (ex : index.hbs)
nano index.hbs
Sortie et redémarrage du conteneur docker
- On sort du conteneur
exit
- On redémarre ghost
docker compose restart ghost
Configuration SMTP pour les e-mails transactionnels
En essayant de m’« abonner » à mon propre blog, je me suis rendu compte que ça ne fonctionnait pas.
Une erreur s’affichait, m’envoyant me faire cuire un œuf - et vérifier les logs…
Il s’agissait d’un e-mail transactionnel qui avait décidé de ne pas partir. Normal, je n’ai configuré aucune adresse e-mail.
Si vous n’avez pas déjà d’adresse e-mail disponible en SMTP facilement, je vous conseille d’utiliser le service Brevo qui offre 300 e-mails transactionnels par mois gratuitement. Pour ma part je dispose d’un « Mailu » lui aussi auto hébergé qui me permet de faire cela.

Accéder au fichier de configuration Ghost
Connectez-vous à nouveau en SSH à votre VPS et accédez à nouveau au fichier de configuration créé plus tôt :
nano ghost/config.production.json
Configurer son serveur SMTP
Actuellement, voici à quoi ressemble l’objet email :
"mail": {
"transport": "Direct"
},
Pour utiliser un serveur SMTP, il suffit de le remplacer par quelque-chose qui ressemble à ceci :
"mail": {
"transport": "SMTP",
"options": {
"host": "<adresse-de-votre-serveur-smtp>",
"port": 465,
"secure": true,
"from": "Timothée Monnier <contact@timotion.fr>",
"auth": {
"user": "<contact@timotion.fr>",
"pass": "<votre-mot-de-passe>"
}
}
},
J’ai volontairement laissé mon prénom et mon nom pour plus de clarté mais sachez que ce qu’il y a dans le from est le nom qui s’affiche lorsqu’un utilisateur reçois un email.
Redémarrer Ghost
Pour appliquer la nouvelle configuration, vous pouvez redémarrer le conteneur Ghost.
cd ghost
docker compose restart ghost
Affichage de code
Nativement il est possible d'afficher du code mais c'est pas très beau, ni efficace, j'ai donc décider d'ajouter la coloration syntaxique ("syntax highlighting") et un bouton de copie.
Pour ce faire, il suffit d'injecter un outil nommé "Prismjs".
Coloration syntaxique
On se rend donc dans les paramètres de Ghost puis tout en bas dans la section "Advanced" sur le menu "Code injection".
Voici ce qu'il faut ajouter dans l'onglet "Site header".
<script src="https://cdn.jsdelivr.net/npm/prismjs/prism.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/plugins/autoloader/prism-autoloader.min.js" defer></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs/themes/prism-okaidia.min.css">
Ici, j'utilise le thème Okaidia que je préfère personnellement mais vous pouvez remplacer prism-okaidia.min.css
pour avoir un autre thème.
En voici la liste :
- Tomorrow :
prism-tomorrow.min.css
- Dark :
prism-dark.min.css
- Funky :
prism-funky.min.css
- Okaida :
prism-okaidia.min.css
- Twilight :
prism-twilight.min.css
- Coy :
prism-coy.min.css
- Solarized :
prism-solarizedlight.min.css
Bouton permettant la copie
De même, pour afficher le petit bouton "copy", voici le code à ajouter.
<script src='https://cdn.jsdelivr.net/npm/prismjs/plugins/toolbar/prism-toolbar.min.js' defer></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js" defer></script>
<style>
pre[class*="language-"],
code[class*="language-"] {
color: #d4d4d4;
font-size: 14px;
text-shadow: none;
font-family: Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono",
"Courier New", monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::selection,
code[class*="language-"]::selection,
pre[class*="language-"] *::selection,
code[class*="language-"] *::selection {
text-shadow: none;
background: #75a7ca;
}
@media print {
pre[class*="language-"],
code[class*="language-"] {
text-shadow: none;
}
}
pre[class*="language-"] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
background: #1e1e1e;
}
:not(pre) > code[class*="language-"] {
padding: 0.1em 0.3em;
border-radius: 0.3em;
color: #db4c69;
background: #f9f2f4;
}
pre[data-line] {
position: relative;
}
pre[class*="language-"] > code[class*="language-"] {
position: relative;
z-index: 1;
}
.line-highlight {
position: absolute;
left: 0;
right: 0;
padding: inherit 0;
margin-top: 1em;
background: #f7ebc6;
box-shadow: inset 5px 0 0 #f7d87c;
z-index: 0;
pointer-events: none;
line-height: inherit;
white-space: pre;
}
pre[class*="language-"].line-numbers {
position: relative;
padding-left: 3.8em;
counter-reset: linenumber;
}
pre[class*="language-"].line-numbers > code {
position: relative;
white-space: inherit;
}
.line-numbers .line-numbers-rows {
position: absolute;
pointer-events: none;
top: 0;
font-size: 100%;
left: -3.8em;
width: 3em;
letter-spacing: -1px;
border-right: 1px solid #999;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.line-numbers-rows > span {
display: block;
counter-increment: linenumber;
}
.line-numbers-rows > span:before {
content: counter(linenumber);
color: #999;
display: block;
padding-right: 0.8em;
text-align: right;
}
div.code-toolbar {
position: relative;
}
div.code-toolbar > .toolbar {
position: absolute;
top: 0.3em;
right: 0.2em;
transition: opacity 0.3s ease-in-out;
opacity: 0.5;
}
div.code-toolbar:hover > .toolbar {
opacity: 1;
}
div.code-toolbar:focus-within > .toolbar {
opacity: 1;
}
div.code-toolbar > .toolbar .toolbar-item {
display: inline-block;
}
div.code-toolbar > .toolbar a {
cursor: pointer;
}
div.code-toolbar > .toolbar button {
background: none;
border: 0;
color: inherit;
font: inherit;
line-height: normal;
overflow: visible;
padding: 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
div.code-toolbar > .toolbar a,
div.code-toolbar > .toolbar button,
div.code-toolbar > .toolbar span {
color: #bbb;
font-size: 0.8em;
padding: 0 0.5em;
box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.2);
border-radius: 0.5em;
}
</style>
J'avoue, le CSS ne vient pas de moi et je ne me suis pas amusé à l'analyser mais je l'ai légèrement modifié et je trouve que cela rend bien.
Merci pour votre lecture.