Mon site perso fait peau neuve !
Je vous explique ici quelles étapes j’ai suivi pour effectuer la migration. Au boulot !
Créer son blog avec Quarto
- Ouvrir
RStudio
- Créer un nouveau projet
Create Quarto Blog
- Cocher
Create a git repository
Un blog Quarto vierge est ainsi créé contenant ces fichiers :
_quarto.yml
: Fichier du projet Quartoindex.qmd
: Page d’accueilabout.qmd
: Page “À propos”posts/
: Répertoire contenant les posts en Quarto Markdown (.qmd
)posts/_metadata.yml
: Options partagées des postsstyles.css
: CSS customisé pour le style du blogMyBlog.Rproj
: Raccourci d’ouverture du projet Quarto
Dans le fichier _quarto.yml
ajouter la ligne output-dir
pour renseigner le répertoire cible pour la génération du site :
project:
type: website
output-dir: docs
Enfin pour le générer et avoir le rendu du blog il suffit d’exécuter build > Render Website
.
Héberger son blog avec GitHub pages
Déployer le blog
Après avoir créé un nouveau dépôt sur GitHub à partir du .git
local, aller dans Settings > Pages
puis dans Build and deployment
et choisir comme source Deploy from a branch
. Puis sélectionner la branche main
et le dossier /docs
avant de valider avec save
. Après quelques minutes le lien vers la page du blog est généré : kevinpolisano.github.io
Nom de domaine customisé
L’hébergeur de mon site WP était o2switch, que je recommande pour la grande réactivité de leur service technique.
Du côté de ce registar voici les étapes à suivre :
- Dans
Espace client > Commander un service
choisirCommander un nom de domaine
(pour ma part :kevinpolisano.fr
) - Dans
Espace technique > Domaines configurés
remplirConfigurer un nom de domaine
(pour ma part :kevinpolisano.fr
) puis activerLet's Encrypt SSL
dans l’ongletSécurité
du cPanel. - Dans
Espace technique > Zone Editor
entrer les champs suivants (à adapter selon le blog) :
Nom | TLL | Type | Enregistrement |
---|---|---|---|
www.kevinpolisano.fr. | 14400 | CNAME | kevinpolisano.github.io |
kevinpolisano.fr. | 14400 | A | 185.199.108.153 |
kevinpolisano.fr. | 14400 | A | 185.199.109.153 |
kevinpolisano.fr. | 14400 | A | 185.199.110.153 |
kevinpolisano.fr. | 14400 | A | 185.199.111.153 |
- J’ai également créé un fichier
CNAME
à la racine du dépôt git, contenant la lignewww.kevinpolisano.fr
.
Du côté de GitHub Pages voici les étapes à suivre :
- Dans
Custom Domain
renseigner le nouveau nom de domainewww.kevinpolisano.fr
puis cliquer sursave
. À ce stade j’ai obtenu à tour de rôle les messages d’erreurs suivants :
DNS check unsuccessful
Both kevinpolisano.fr and its alternate name are improperly configured Domain does not resolve to the GitHub Pages server. For more information, see documentation (NotServedByPagesError).
DNS valid for primary
kevinpolisano.fr is improperly configured Domain does not resolve to the GitHub Pages server. For more information, see documentation (NotServedByPagesError).
J’ai du attendre quelques heures pour que la propagation du DNS soit effective. On peut suivre celle-ci sur DNS Checker en s’assurant pour le domaine racine
kevinpolisano.fr
que les enregistrementsA
pointent vers les IP GitHub Pages; et pour le sous-domainewww.kevinpolisano.fr
que l’enregistrementCNAME
pointe verskevinpolisano.github.io
.Une fois que le bouton
save
donneDNS check successful
on cocheEnforce HTTPS
et on vérifie qu’en tapantkevinpolisano.fr
dans la barre de navigateur on est bien redirigé vers le blog à l’adressehttps://www.kevinpolisano.fr/
Exporter ses articles Wordpress en HTML vers Markdown
J’ai expérimenté trois solutions :
- GitHub - SchumacherFM/wordpress-to-hugo-exporter
- GitHub - lonekorean/wordpress-export-to-markdown
- GitHub - palaniraja/blog2md
La première conserve certaines balises HTML (typiquement pour l’usage de la couleur et la mise en forme), tandis que les deux suivantes exportent en pur Markdown.
Wordpress to Hugo Exporter
La première solution consiste à uploader l’archive sur le site WP dans le dossier wp-content/plugins
à activer le plugin et utiliser Outils > Export Hugo
. Ce dernier n’a pas fonctionné donc j’ai utilisé le Terminal de mon serveur d’hébergement (O2switch) (comme expliqué ici) :
cd wp-content/plugins/wordpress-to-hugo-exporter/
php hugo-export-cli.php
Le script créé un fichier /tmp/wp-hugo.zip
(cela peut prendre quelques minutes). Dans le gestionnaire de fichiers (via le Cpanel d’O2switch) j’ai effectué une recherche du fichier, qui m’a indiquée que celui-ci se trouvait dans le dossier caché .cagefs/tmp/
.
Wordpress export to Markdown
Premièrement on effectue un export du contenu complet de WP au format export.xml
(dans Outils > Exporter
), que l’on place dans l’archive du code téléchargé. Puis on exécute le script :
npm install && node index.js
Blog2md
De même on exécute :
npm install && node index.js w export.xml out
Nettoyage des articles exportés, liens, images, …
Lorsque mes billets WP contenaient du texte brut, du code ou du \(\LaTeX\), j’ai utilisé l’export Markdown de Wordpress export to Markdown
; tandis que pour mes billets contenant une mise en forme travaillée (avec notamment l’usage de couleurs), j’ai utilisé l’export Markdown + HTML de Wordpress to Hugo Exporter
, qui a aussi le bon goût d’exporter toutes les images dans wp-content/uploads/
. Pour ces fichiers Markdown + HTML, j’ai écrit un script Python permettant entre autres de :
- Remplacer les liens HTML par des liens Markdown, pour les images en particulier par l’insertion de

en ayant préalablement copié l’image couranteimg
(privée de sa dimension) du dossieruploads/
vers le dossierimages/
du répertoire courant correspondant au billet. - Remplacer les footnotes HTML par des footnotes Markdown.
- Remplacer les blocs de code HTML par des balises de code Markdown (ici précisant le language Matlab)
import re
import os
import shutil
# Dossiers source et destination pour les images
= "../uploads"
UPLOADS_DIR = "images"
IMAGES_DIR = "index_prev.qmd"
INPUT_FILE = "index.qmd"
OUTPUT_FILE
# Créer le dossier images s'il n'existe pas
if not os.path.exists(IMAGES_DIR):
os.makedirs(IMAGES_DIR)
# Lire le fichier d'entrée
with open(INPUT_FILE, "r", encoding="utf-8") as file:
= file.read()
content
# Fonction pour traiter les images
def process_images(content):
= re.compile(
image_pattern r'<a [^>]*?href="[^"]*?/([^/"]+)"[^>]*?><img [^>]*?src="[^"]*?/([^/"]+)"[^>]*?></a>'
)
def replace_image(match):
= match.group(1)
image_file = os.path.splitext(image_file)
base_name, ext
# Vérifier si le nom contient les dimensions WxH
= re.compile(r"^(?P<name>.+)-\d+x\d+$")
dimension_pattern = dimension_pattern.match(base_name)
dimension_match
if dimension_match:
= dimension_match.group("name") # Nom sans dimensions
base_name = base_name + ext
original_image_file = os.path.join(UPLOADS_DIR, original_image_file)
original_path
# Si l'image sans dimensions existe, utiliser celle-ci
if os.path.exists(original_path):
= original_image_file
image_file
# Copier l'image
= os.path.join(UPLOADS_DIR, image_file)
src_path = os.path.join(IMAGES_DIR, image_file)
dest_path if os.path.exists(src_path):
shutil.copy2(src_path, dest_path)
return f""
return image_pattern.sub(replace_image, content)
# Fonction pour traiter les blocs de code
def process_code_blocks(content):
= re.compile(
code_block_pattern r'<pre class="brush:[^"]+">(.*?)</pre>', re.DOTALL
)
def replace_code_block(match):
= match.group(1).replace(">", ">").replace("<", "<").replace("&", "&")
code_content return f"```matlab\n{code_content}\n```"
return code_block_pattern.sub(replace_code_block, content)
# Fonction pour traiter les footnotes
def process_footnotes(content):
= re.compile(
footnote_body_pattern r'<sup id="(?P<id>[^"]+)"><a href="#[^"]+" title="(?P<title>[^"]+)"[^>]*>\d+</a></sup>'
)
def replace_footnote_body(match):
= match.group("id")
footnote_id = match.group("title").replace("'", "'").replace(""", '"')
footnote_content = f"[^{footnote_id}]"
markdown_footnote = f"[^{footnote_id}]: {footnote_content}"
markdown_definition return markdown_footnote, markdown_definition
= content.split("\n\n") # Diviser en paragraphes
paragraphs = []
updated_paragraphs
for paragraph in paragraphs:
= list(footnote_body_pattern.finditer(paragraph))
matches if matches:
= []
definitions for match in matches:
= replace_footnote_body(match)
footnote_markdown, footnote_definition = paragraph.replace(match.group(0), footnote_markdown)
paragraph
definitions.append(footnote_definition)
updated_paragraphs.append(paragraph)
updated_paragraphs.extend(definitions)else:
updated_paragraphs.append(paragraph)
return "\n\n".join(updated_paragraphs)
# Fonction pour supprimer les balises <li id=X>
def remove_list_items(content):
= re.compile(r'<li id="[^"]+">.*?</li>', re.DOTALL)
list_item_pattern return list_item_pattern.sub("", content)
# Fonction pour supprimer l’indentation des balises de paragraphes
def remove_paragraph_indentation(content):
= re.compile(r"\s*<(/?p)>")
paragraph_pattern return paragraph_pattern.sub(r"<\1>", content)
# Appliquer les transformations
= process_images(content)
content = process_code_blocks(content)
content = process_footnotes(content)
content = remove_list_items(content)
content = remove_paragraph_indentation(content)
content
# Écrire le fichier de sortie
with open(OUTPUT_FILE, "w", encoding="utf-8") as file:
file.write(content)
print(f"Transformation terminée. Résultat enregistré dans {OUTPUT_FILE}.")
Bien sûr j’ai aussi modifié les exports à la main lorsque je repérais des rectifications ponctuelles (non automatisables). L’export en pur Markdown a quant à lui le bon goût de renseigner les liens cassés, ce qui permet au passage de faire un nettoyage des liens morts témoins du pourrissement des liens du web. À l’avenir, je vais tâcher de privilégier la bibliographie (.bib
) renseignant les métadonnées des sources (titre, journal, date, …) pour que celles-ci restent identifiables en cas de liens morts, et donc potentiellement trouvables ailleurs si l’article a été hébergé à une autre adresse.
Gestion des commentaires
J’ai opté pour giscus, qui est un dispositif relativement léger, basé sur les discussions GitHub, open-source et sans publicité. Le seul bémol est qu’il faut disposer d’un compte Github pour être autorisé à commenter. L’avantage par ailleurs est que l’on est moins sujet aux spams et aux commentaires injurieux (l’authentification, plus coûteuse en temps, en dissuadent plus d’un).
Mise en place de la fonctionnalité
Pour activer les commentaires il faut remplir la page giscus détaillant la marche à suivre :
- Installer l’application giscus
- Activer les discussions Github dans l’onglet
Settings > Features
du dépôt en cochant la caseDiscussions
. - Renseigner le dépôt public (dans mon cas
kevinpolisano/blog
) ainsi que les propriétés escomptées.
La balise HTML générée est la suivante dans mon cas :
<script src="https://giscus.app/client.js"
data-repo="kevinpolisano/blog"
data-repo-id="R_kgDOM9ReCA"
data-category="Announcements"
data-category-id="DIC_kwDOM9ReCM4CmGgm"
data-mapping="pathname"
data-strict="0"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="bottom"
data-theme="preferred_color_scheme"
data-lang="fr"
data-loading="lazy"
crossorigin="anonymous"
async>
</script>
Ce qui me permet de remplir les options de commentaires dans le fichier _quarto.yml
comme suit :
project:
type: website
output-dir: docs
website:
title: "Blog de Kévin Polisano"
navbar:
right:
- about.qmd
comments:
giscus:
repo: "kevinpolisano/blog"
repo-id: "R_kgDOM9ReCA"
category: "Announcements"
category-id: "DIC_kwDOM9ReCM4CmGgm"
mapping: "pathname"
reactions-enabled: true
input-position: "bottom"
theme: "light"
language: "fr"
loading: "lazy"
format:
html:
theme: flatly
css: styles.css
editor: visual
Extraction des commentaires Wordpress
On commence par extraire les commentaires dans un fichier comments.csv
à partir de export.xml
grâce à ce script python qui parse les commentaires :
import xml.etree.ElementTree as ET
import csv
from datetime import datetime
# Chemin vers le fichier XML exporté
= "wp.xml"
xml_file = "comments.csv"
output_file
# Charger le fichier XML
= ET.parse(xml_file)
tree = tree.getroot()
root
# Namespace de WordPress
= {"wp": "http://wordpress.org/export/1.2/"}
ns
# Extraire les commentaires
= []
comments for item in root.findall(".//item"):
= item.find("title").text
post_title = item.find("link").text
post_url for comment in item.findall("wp:comment", ns):
= comment.find("wp:comment_author", ns).text
author = comment.find("wp:comment_content", ns).text
content = comment.find("wp:comment_date", ns).text
date
comments.append([post_title, post_url, author, content, date])
# Trier les commentaires par date croissante
=lambda x: datetime.strptime(x[4], "%Y-%m-%d %H:%M:%S"))
comments.sort(key
# Écrire dans un fichier CSV
with open(output_file, "w", newline="", encoding="utf-8") as f:
= csv.writer(f, delimiter='$')
writer "Post Title", "Post URL", "Author", "Comment", "Date"])
writer.writerow([
writer.writerows(comments)
print(f"Commentaires exportés dans {output_file}")
Création d’un Token GitHub
Voici les étapes pour créer un token personnel GitHub avec l’autorisation pour gérer les discussions (comme les commentaires pour Giscus) :
- Allez dans
Settings > Developer Settings
(général pas dans un dépôt) puisPersonal access tokens > Tokens (classic)
. - Sélectionnez
Generate new token > Generate new token (classic)
. - Donnez un nom au token :
Giscus Discussions Token
. - Définissez une durée d’expiration : choisissez une durée (par exemple 30 jours) ou
No expiration
pour un usage prolongé. - Sélectionnez les autorisations nécessaires :
public_repo
: Accéder aux dépôts publicsread:discussion
: Lire les discussions.write:discussion
: Ajouter ou modifier des discussions.
- Générer et sauvegarder le token (par exemple, dans un gestionnaire de mots de passe)
- Dans les paramètres de votre projet où Giscus est configuré, ajoutez le token comme variable d’environnement dans
Settings > Secrets and variables > Actions
avec le nomGH_TOKEN
(ou un autre nom significatif) et collez le token.
Création des discussions GitHub
Voici un script Python qui utilise l’API graphql
pour créer automatiquement toutes les discussions avec les titres correspondant au pathname
(pour être raccord avec mapping: pathname
dans _quarto.yml
).
import os
import requests
# Configuration
= "ghp_" # Remplacez par votre token GitHub
GITHUB_TOKEN = "kevinpolisano" # Votre nom d'utilisateur GitHub
REPO_OWNER = "blog" # Nom du dépôt GitHub
REPO_NAME = "DIC_kwDOM9ReCM4CmGgm" # ID de la catégorie des discussions
CATEGORY_ID = "/posts/" # Dossier contenant vos articles Quarto
POSTS_DIR
= "https://api.github.com/graphql"
GITHUB_GRAPHQL_URL
# Fonction pour envoyer une requête GraphQL
def send_graphql_query(query, variables=None):
= {
headers "Authorization": f"Bearer {GITHUB_TOKEN}",
"Content-Type": "application/json",
}= {"query": query, "variables": variables}
payload = requests.post(GITHUB_GRAPHQL_URL, json=payload, headers=headers)
response
response.raise_for_status()return response.json()
# Vérifie si une discussion existe déjà pour un post
def discussion_exists(title):
= """
query query($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
discussions(first: 100) {
nodes {
title
}
}
}
}
"""
= {"owner": REPO_OWNER, "name": REPO_NAME}
variables = send_graphql_query(query, variables)
result = result.get("data", {}).get("repository", {}).get("discussions", {}).get("nodes", [])
discussions
return any(d["title"] == title for d in discussions)
def get_repository_id():
= """
query query($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
id
}
}
"""
= {"owner": REPO_OWNER, "name": REPO_NAME}
variables = send_graphql_query(query, variables)
result return result.get("data", {}).get("repository", {}).get("id", None)
# Crée une discussion pour un post donné
def create_discussion(title, repository_id):
= """
mutation mutation($title: String!, $body: String!, $categoryId: ID!, $repositoryId: ID!) {
createDiscussion(input: {title: $title, body: $body, categoryId: $categoryId, repositoryId: $repositoryId}) {
discussion {
id
title
url
}
}
}
"""
= {
variables "title": title,
"body": "Discussion for this post",
"categoryId": CATEGORY_ID,
"repositoryId": repository_id
}
print(f"Tentative de création de la discussion pour : {title}")
= send_graphql_query(mutation, variables)
result print("Réponse GitHub :", result) # Debugging
if "errors" in result:
print(f"Échec : {result['errors']}")
else:
= result.get("data", {}).get("createDiscussion", {}).get("discussion", {})
discussion if discussion:
print(f"Discussion créée : {discussion['title']} ({discussion['url']})")
else:
print(f"Échec de la création pour {title}")
# Parcours des fichiers posts/ et création des discussions
if __name__ == "__main__":
= get_repository_id()
repository_id if not repository_id:
print("Impossible de récupérer l'ID du dépôt. Vérifie ton token et le nom du repo.")
1)
exit(
for folder in os.listdir(POSTS_DIR):
= os.path.join(POSTS_DIR, folder)
post_path if os.path.isdir(post_path): # Vérifie que c'est bien un dossier
= f"posts/{folder}/"
title if discussion_exists(title):
print(f"Discussion déjà existante pour : {title}")
else:
print(f"Création de la discussion pour : {title}")
create_discussion(title, repository_id)
À noter que j’ai du déjà faire apparaître la section commentaire en local (avec quarto preview
) pour chaque post en défilant, afin que le fichier html
contienne la portion de code suivante :
<script src="https://giscus.app/client.js"
data-repo="kevinpolisano/blog"
data-repo-id="R_kgDOM9ReCA"
data-category="Announcements"
data-category-id="DIC_kwDOM9ReCM4CmGgm"
data-mapping="pathname"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="bottom"
data-theme="light"
data-lang="fr"
crossorigin="anonymous"
data-loading="lazy" async="">
</script>
Pour lister l’ensemble des discussions GitHub et afficher leur IDs correspondant j’utilise le script suivant :
import requests
# Configuration
= "ghp_" # Remplacez par votre token GitHub
GITHUB_TOKEN = "kevinpolisano" # Nom d'utilisateur ou organisation GitHub
REPO_OWNER = "blog" # Nom du dépôt GitHub
REPO_NAME = "https://api.github.com/graphql"
GITHUB_GRAPHQL_URL
# Fonction pour envoyer une requête GraphQL
def send_graphql_query(query, variables=None):
= {
headers "Authorization": f"Bearer {GITHUB_TOKEN}",
"Content-Type": "application/json",
}= {"query": query, "variables": variables}
payload = requests.post(GITHUB_GRAPHQL_URL, json=payload, headers=headers)
response
response.raise_for_status()return response.json()
# Requête pour récupérer les discussions avec categoryId
def fetch_discussions(repo_owner, repo_name):
= """
query query($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
discussions(first: 50) {
nodes {
id
title
url
category {
id
name
}
}
}
}
}
"""
= {"owner": repo_owner, "name": repo_name}
variables = send_graphql_query(query, variables)
result return result.get("data", {}).get("repository", {}).get("discussions", {}).get("nodes", [])
# Exécuter la requête
if __name__ == "__main__":
= fetch_discussions(REPO_OWNER, REPO_NAME)
discussions for discussion in discussions:
= discussion["category"]["id"]
category_id = discussion["category"]["name"]
category_name print(f"Title: {discussion['title']}")
print(f"ID: {discussion['id']}")
print(f"URL: {discussion['url']}")
print(f"Category ID: {category_id} (Name: {category_name})")
print("-" * 40)
Ajouter les commentaires existants
Parmi mes anciens billets, peu possédaient des commentaires, à l’exception de quelques uns qui en affichaient plus d’une centaine. Le script qui suit s’applique donc à un billet donné, mais la procédure est facilement automatisable sur tous les billets si vous avez conservé une correspondance entre le post_url
de l’extraction de commentaires et le pathname
du titre de la discussion.
import csv
import requests
import time
from datetime import datetime
# Configurations
= "ghp_" # À remplacer par votre TOKEN
GITHUB_TOKEN = "kevinpolisano"
REPO_OWNER = "blog"
REPO_NAME = "D_kwDOM9ReCM4AfHpQ"
DISCUSSION_ID = "Comprendre son bulletin de paie" # À remplacer par le nom du billet (ou le POST_URL)
POST_TITLE = "comments.csv" # Fichier stockant les commentaires
CSV_FILE = "https://api.github.com/graphql"
GITHUB_GRAPHQL_URL
# Nombre maximum de tentatives par commentaire
= 3
MAX_RETRIES = 5 # secondes
RETRY_DELAY
# Fonction pour envoyer une requête GraphQL
def send_graphql_query(query, variables=None):
= {
headers "Authorization": f"Bearer {GITHUB_TOKEN}",
"Content-Type": "application/json",
}= {"query": query, "variables": variables}
payload
for attempt in range(1, MAX_RETRIES + 1):
try:
= requests.post(GITHUB_GRAPHQL_URL, json=payload, headers=headers)
response if response.status_code == 200:
return response.json()
else:
print(f"Erreur {response.status_code} : {response.text} (tentative {attempt}/{MAX_RETRIES})")
# Attendre avant de réessayer
time.sleep(RETRY_DELAY) except requests.RequestException as e:
print(f"Erreur de connexion : {e} (tentative {attempt}/{MAX_RETRIES})")
time.sleep(RETRY_DELAY)
return None # Échec après toutes les tentatives
# Fonction pour poster un commentaire
def post_comment(discussion_id, body):
= """
mutation mutation($discussionId: ID!, $body: String!) {
addDiscussionComment(input: {discussionId: $discussionId, body: $body}) {
comment {
id
body
}
}
}
"""
= {"discussionId": discussion_id, "body": body}
variables = send_graphql_query(mutation, variables)
result
if result:
return result.get("data", {}).get("addDiscussionComment", {}).get("comment")
return None # Échec
# Fonction pour convertir la date en toutes lettres (en français)
def convert_date(date_str):
try:
= datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
date_obj return date_obj.strftime('%d %B %Y à %Hh%M')
except ValueError as e:
print(f"Erreur de conversion de la date : {date_str} ({e})")
return date_str # Retourne la date brute si erreur
# Importation des commentaires
def import_comments(csv_file, post_title, discussion_id):
print(f"Début de l'importation des commentaires pour : {post_title}")
= [] # Stocke les commentaires non importés
failed_comments
try:
with open(csv_file, "r", encoding="utf-8") as file:
= csv.DictReader(file, delimiter='$')
reader for row in reader:
try:
if row.get("Post Title") == post_title:
= row.get("Comment")
comment = row.get("Author", "Inconnu")
author = row.get("Date", "")
date = convert_date(date)
date
if comment:
= f"{comment}\n\n*Commenté par **{author}**, le {date}.*"
formatted_comment print(f"Importation : {formatted_comment[:50]}...") # Affiche le début du commentaire
= post_comment(discussion_id, formatted_comment)
result if result:
print(f"Commentaire importé avec succès (ID {result['id']})")
else:
print("Échec de l'importation, commentaire ajouté à la liste des échecs.")
failed_comments.append(row)else:
print(" Aucun commentaire trouvé dans cette ligne.")
else:
print(f"Ignoré : {row.get('Post Title')}")
except Exception as e:
print(f"Erreur sur une ligne : {e}")
failed_comments.append(row)
except FileNotFoundError:
print(f"Fichier introuvable : {csv_file}")
return
except Exception as e:
print(f"Erreur de lecture du CSV : {e}")
return
# Sauvegarde des commentaires non importés
if failed_comments:
= "failed_comments.csv"
failed_file with open(failed_file, "w", newline="", encoding="utf-8") as f:
= csv.DictWriter(f, fieldnames=["Post Title", "Post URL", "Author", "Comment", "Date"], delimiter='$')
writer
writer.writeheader()
writer.writerows(failed_comments)print(f"{len(failed_comments)} commentaires non importés. Sauvegardés dans {failed_file}.")
# Exécution
if __name__ == "__main__":
import_comments(CSV_FILE, POST_TITLE, DISCUSSION_ID)
Parfois il y a un échec d’importation lorsqu’il y a beaucoup de commentaires, certainement du à un quota d’utilisation de l’API, dans ce cas les commentaires qui n’ont pas été importés sont sauvegardés dans un fichier failed_comments.csv
, et il suffit de relancer le script sur ce fichier (à renseigner dans CSV_FILE
) autant de fois que nécessaire.
Finaliser la migration
Redirection des URL
Dans le fichier .htaccess
à la racine du serveur où j’hébergeais mon ancien blog (kevinpolisano.com
), il faut temporairement effectuer des redirections des anciennes URL de mes billets vers les nouvelles, comme ceci par exemple :
Redirection 301 /analyse-des-arguments-de-sophie-cluzel-opposee-a-la-deconjugualisation-de-laah/ https://www.kevinpolisano.fr/posts/2021-03-07-analyse-des-arguments-de-sophie-cluzel-opposee-a-la-deconjugualisation-de-laah/
Et pour toutes les autres URL non spécifiées :
RedirectMatch 301 ^/(.*)$ https://kevinpolisano.fr/
Mise à jour dans Google Search Console
Vérification de propriété
- Depuis Google Search Console, télécharger le fichier
googleXXXXXXXXXXXX.html
. - Placer ce fichier directement à la racine du dossier Quarto (là où se trouve
index.qmd
ou_quarto.yml
). - Ajouter une ligne dans
_quarto.yml
pour inclure le fichier dans le site généré :
resources:
- googleXXXXXXXXXXXX.html
- Faire un build avec
quarto render
puis déployer avecgit commit/push
et vérifier que la pagehttps://www.kevinpolisano.fr/googleXXXXXXXXXXXX.html
est en ligne. - Valider sur Google Search Console pour terminer la vérification de propriété.
Cette étape permet de vérifier que je suis bien propriétaire du domaine kevinpolisano.fr
.
Sitemap
Le fichier sitemap.xml
sert en continu à indiquer à Google quelles pages existent sur mon site, afin d’améliorer le référencement (SEO). Pour générer ce fichier (ainsi qu’un fichier robots.txt
) il faut ajouter dans le fichier _quarto.yml
:
website:
site-url: https://www.kevinpolisano.fr
page-navigation: true
puis recompiler, déployer, et vérifier que https://www.kevinpolisano.fr/sitemap.xml
et https://www.kevinpolisano.fr/robots.txt
sont accessibles.
Enfin dans Google Search Console dans le menu Sitemaps
à gauche, ajouter sitemap.xml
.
Google Analytics
- Créer un compte sur Google Analytics
- Créer une propriété
- Donner un nom (ex: “Blog Quarto”)
- Choisir le fuseau horaire et la devise
- Choisir Web comme plateforme.
- Indique l’URL du site :
https://www.kevinpolisano.fr
On obtient un ID de mesure du type : G-XXXXXXXXXX
que l’on ajoute à _quarto.yml
:
website:
title: "Blog de Kévin Polisano"
site-url: https://www.kevinpolisano.fr
page-navigation: true
google-analytics: G-XXXXXXXXXX
Cette méthode ajoute automatiquement le script de tracking dans le <head>
du site HTML.
Conclusion
La conversion de mon ancien blog WordPress en un site statique généré avec Quarto est maintenant terminée. À mes yeux un blog statique possède plusieurs avantages :
- Performance du site : pas de base de données à interroger, temps de chargement plus court, moins de dépendances serveur…
- Sécurité : pas de surface d’attaque serveur (thèmes ou plugins WordPress vulnérables)
- Contrôle du contenu et portabilité : fichiers markdown lisibles à vie, indépendants de tout CMS; migration possible vers un autre générateur statique (Hugo, Jekyll…) et possibilité de rédiger localement sans dépendance à un service tiers; gestion facile des médias; structure logique des fichiers, qui sont versionnés avec git.
- Automatisation et reproductibilité : intégration fluide avec GitHub Actions et GitHub Pages; insertion de code reproductible (R, Python, Julia, Bash) avec Quarto, parfait pour des billets scientifiques.
- Gratuité et perenité : hébergé gratuitement sur GitHub Pages, seul le nom de domaine nécessite un achat si l’on en veut un customisable (
kevinpolisano.fr
plutôt que celui par défautkevinpolisano.github.io
).
La principale restriction réside dans la limite de stockage offerte par le dépôt Git (5 Go) et surtout le déploiement du site avec GitHub Pages (1 Go). Raison pour laquelle j’utilise deux dépôts GitHub : le premier qui héberge à proprement parlé le blog Quarto contenant les posts et les images; le second qui stocke des documents PDF annexes, et qui est également déployé avec GitHub Pages pour obtenir des URL plus jolies et des PDF visualisables dans le navigateur.
L’alternative aurait consisté à conserver mon hébergement chez o2switch et transférer avec GitHub Actions le site généré par Quarto via FTP. Cela a l’avantage d’offrir un espace de stockage bien plus conséquent, de centraliser le blog et les assets au même endroit et donc d’avoir une URL plus consistante avec le même nom de domaine. J’y reviendrai peut-être, mais pour le moment la solution proposée dans ce billet satisfait mes attentes pour un blogging du dimanche…