# NecroBlog CLI : ressusciter des articles perdus avec la Wayback Machine

> Comment j’ai utilisé la Wayback Machine et C# pour retrouver 60 articles disparus, récupérer leurs images et les convertir proprement en Markdown.

- Auteur: Jérôme Giacomini
- Langue: fr
- URL canonique: [NecroBlog CLI : ressusciter des articles perdus avec la Wayback Machine](https://jeromegiacomini.net/fr/articles/2026/08/25/necroblog-recuperer-des-articles-de-blog-perdus-avec-la-wayback-machine)
- Publié le: 2026-08-25
- Dernière modification: 2026-09-05
- Thèmes: C#, .NET, Wayback Machine, Markdown, Open source

Quand j’ai décidé de remettre ce blog en ligne, j’ai retrouvé un petit problème : j’avais perdu mon backup de l’ancien blog… Mes articles avaient disparu.

C’était sans compter sur la Wayback Machine.

Autrement dit, Internet n’oublie jamais… sauf parfois le CSS, les images et précisément la page dont vous avez besoin.

J’aurais pu ouvrir chaque capture à la main, copier le texte, corriger les liens et recommencer quelques dizaines de fois. J’ai préféré la solution raisonnable : écrire un outil en C# qui interroge des archives vieilles de plusieurs années et espérer que toutes les pages respectent encore leur structure HTML d’origine.

J’ai donc créé [NecroBlog CLI](https://github.com/jgiacomini/NecroBlogCLI), un outil en ligne de commande dont le rôle tient en quatre verbes :

```text
Discover → Inspect → Recover → Report
```

Son objectif est de retrouver les anciens articles de `jeromegiacomini.net` et de mon profil sur `blogs.infinitesquare.com`, d’en extraire le vrai contenu, puis de les reconstruire sous forme de fichiers `.md` avec leurs métadonnées et, lorsque cela reste possible, leurs images.

![NecroBlog CLI transforme une page archivée en fichier Markdown](/media/2026/necroblog-recuperer-des-articles-de-blog-perdus-avec-la-wayback-machine/images/necroblog-cli.png)

Et oui, on est en 2026 : le Markdown est le format du moment.

Oui, j’ai appelé le projet **NecroBlog**. À partir du moment où l’on passe ses soirées à relever des billets morts, autant assumer le thème jusqu’au bout.

## La Wayback Machine n’est pas une liste d’articles

La première difficulté n’est pas de télécharger une page. Elle consiste à savoir quelles pages ont existé.

Pour cela, NecroBlog utilise l’API **CDX** de la Wayback Machine. CDX est le catalogue des captures : pour une URL donnée — qui peut contenir un joker — il retourne notamment l’URL d’origine, la date de la capture, le type de contenu, le code HTTP et une empreinte du document.

La construction de la requête ressemble à ceci dans `WaybackClient` :

```csharp
var query = new StringBuilder("https://web.archive.org/cdx/search/cdx?");
query.Append("url=").Append(Uri.EscapeDataString(url));
query.Append("&output=json");
query.Append("&fl=urlkey,timestamp,original,mimetype,statuscode,digest,length");

foreach (var filter in filters ?? [])
{
    query.Append("&filter=").Append(Uri.EscapeDataString(filter));
}

query.Append("&collapse=digest");

var body = await GetStringWithRetryAsync(query.ToString(), ct);
var captures = CdxParser.ParseJson(body);
```

Le `collapse=digest` permet d’écarter une partie des captures strictement identiques. C’est important : un article archivé vingt fois n’est toujours qu’un article, même s’il a reçu beaucoup plus de sauvegardes que de commentaires.

Il faut ensuite normaliser les URL pour ne pas traiter séparément les variantes en `http`, `https`, avec ou sans `www`, avec un slash final ou quelques paramètres de tracking oubliés là par une ancienne newsletter.

Enfin, toutes les URL archivées ne sont pas des articles. Un WordPress contient aussi des pages d’accueil, des catégories, des tags, des flux RSS, des pages de pagination, des scripts et une quantité surprenante de chemins qui n’ont aucune envie de devenir du Markdown. NecroBlog classe donc chaque URL et ne conserve comme candidates que celles qui correspondent aux structures des anciens blogs.

Pour mon WordPress, un article suivait par exemple ce modèle :

```csharp
[GeneratedRegex(
    @"^/Blog/\d{4}/\d{2}/\d{2}/[^/]+/?$",
    RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex ArticlePathRegex();
```

Ce n’est pas universel, mais c’est volontaire. Pour récupérer proprement un site disparu, connaître sa structure vaut souvent mieux qu’un algorithme persuadé que la page « Mot de passe oublié » est un excellent billet technique.

## Choisir une capture qui existe vraiment

CDX peut annoncer plusieurs captures pour une même page. NecroBlog écarte celles qui ne sont pas du HTML, les erreurs HTTP et les enregistrements de type `revisit`, puis privilégie une capture récente.

Il subsiste une subtilité charmante : une capture peut être présente dans le catalogue mais impossible à télécharger. Le catalogue dit « oui », le serveur de replay dit « non », et vous voilà à négocier avec un fantôme.

L’outil essaie donc plusieurs captures au lieu de tout abandonner à la première erreur :

```csharp
foreach (var capture in candidates.Take(6))
{
    html = await _wayback.FetchCaptureHtmlAsync(
        capture.Timestamp,
        candidate.OriginalUrl,
        cancellationToken);

    if (!string.IsNullOrWhiteSpace(html))
    {
        usedTimestamp = capture.Timestamp;
        break;
    }
}
```

Pour récupérer le document, j’utilise une URL de replay avec le suffixe `id_` :

```csharp
public static string BuildRawReplay(string timestamp, string originalUrl) =>
    $"https://web.archive.org/web/{timestamp}id_/{originalUrl}";
```

Ce suffixe demande les octets archivés sans la barre de navigation Wayback ni la réécriture habituelle de la page. C’est beaucoup plus pratique pour analyser le HTML. Mon outil cherche des articles, pas le code de l’interface de la machine à remonter le temps.

Le client est conçu pour bien gérer les réponses **`429`** et **`503`**. J’y ai également ajouté un délai entre chaque appel et une attente progressive entre les nouvelles tentatives afin d’éviter d’être bloqué par les limitations du site.

## Extraire l’article sans emporter tout le cimetière

Une fois le HTML récupéré, il ne suffit pas de supprimer les balises. La page contient encore le menu, le pied de page, les boutons de partage, les commentaires et parfois des scripts ajoutés par la Wayback Machine.

J’ai créé une chaîne d’extracteurs. Elle essaie d’abord l’extracteur propre au site, puis utilise un extracteur générique en dernier recours :

```csharp
_extractors =
[
    new JeromeGiacominiExtractor(),
    new InfiniteSquareExtractor(),
    new GenericArticleExtractor(),
];

var extractor = _extractors
    .FirstOrDefault(e => e.CanHandle(sourceId, html))
    ?? _extractors[^1];

return extractor.Extract(html, pageUrl);
```

Pour l’ancien WordPress, l’extracteur recherche principalement un élément `article`, puis son bloc `.entry-content`. Il récupère aussi le titre, la date et l’auteur depuis le HTML ou les métadonnées Open Graph et JSON-LD.

Cette séparation a deux avantages. Elle évite de remplir les billets avec la navigation du site et elle permet de signaler un résultat **suspect** lorsque le titre manque ou que le contenu extrait est anormalement court. Dans ce cas, NecroBlog ne complète pas les trous avec beaucoup d’imagination et un aplomb remarquable : il inscrit l’article dans le rapport pour vérification manuelle.

## Passer du HTML au Markdown sans massacrer le code

Le contenu extrait traverse ensuite un convertisseur HTML vers Markdown écrit pour le projet. Il prend en charge les titres, paragraphes, listes, tableaux, citations, liens, images, balises de code en ligne et blocs `<pre><code>`.

Les anciens articles utilisaient parfois **SyntaxHighlighter**, qui indiquait le langage dans une classe comme `brush: csharp`. Le convertisseur détecte cette information et produit une clôture Markdown typée :

````markdown
```csharp
public void Foo()
{
    Console.WriteLine("Je revis !");
}
```
````

Préserver les blocs de code était prioritaire. Un paragraphe légèrement mal espacé reste lisible ; un exemple C# dont les caractères ont été encodés trois fois devient rapidement de l’art contemporain.

Les liens réécrits par la Wayback Machine sont également ramenés vers leur URL d’origine. Cela évite que chaque lien interne continue de faire un détour par une capture datée alors que l’article vient justement d’être remis en ligne.

Enfin, le fichier est écrit avec un front matter YAML qui conserve la provenance :

```csharp
sb.AppendLine("---");
sb.AppendLine($"title: {Quote(article.Title ?? article.Slug)}");
sb.AppendLine($"date: {Quote(article.Date ?? "")}");
sb.AppendLine($"author: {Quote(article.Author ?? "")}");
sb.AppendLine($"original_url: {Quote(article.OriginalUrl)}");
sb.AppendLine($"archive_url: {Quote(article.ArchiveUrl ?? "")}");
sb.AppendLine($"wayback_timestamp: {Quote(article.WaybackTimestamp ?? "")}");
sb.AppendLine("---");
```

Je peux ainsi savoir d’où vient chaque billet, quelle capture a été retenue et quelles autres captures étaient connues. Les dates manquantes restent vides : lorsqu’on restaure des archives, une absence d’information est préférable à une fausse certitude très bien formatée.

## Les images : le vrai niveau difficile

Les pages HTML ont souvent été archivées correctement. Les images, elles, ont parfois décidé de suivre un autre chemin spirituel.

Dans les archives de mes anciens blogs, la Wayback Machine avait conservé **beaucoup moins d’images que de pages HTML**. Une page archivée ne constitue pas une photographie complète du site : son document HTML et chacune de ses ressources sont capturés séparément. Il est donc tout à fait possible de retrouver un article avec ses balises `<img>`, mais aucune copie du fichier auquel elles font référence. On possède alors le cadre, la légende et l’emplacement de la photo — il ne manque que ce détail assez secondaire qu’est la photo elle-même.

Plusieurs raisons peuvent expliquer ces absences. Certaines images étaient hébergées sur un autre domaine ou un CDN, d’autres étaient chargées tardivement par JavaScript, et le serveur pouvait aussi refuser la requête ou ne plus répondre au moment du passage du robot d’archivage. Les fichiers binaires, plus lourds que le HTML, n’ont pas non plus été capturés à chaque visite. Le fait qu’un article apparaisse dans CDX ne garantit donc absolument pas que toutes ses illustrations y figurent également.

C’est ce qui rend leur récupération plus difficile : pour un article, je dispose généralement de plusieurs captures parmi lesquelles choisir ; pour une image, il n’existe parfois aucune capture, quelle que soit la date essayée.

Pour chacune des images référencées dans le Markdown, NecroBlog cherche une capture dédiée dans CDX et la télécharge avec le suffixe `im_`, qui demande les octets de l’image. Le fichier est ensuite enregistré localement et son URL est remplacée dans le billet.

Quand la Wayback Machine n’a aucune copie exploitable, l’outil tente successivement :

- l’URL d’origine sur le web encore accessible ;
- `archive.today` ;
- les index et fichiers WARC de Common Crawl.

La première source qui retourne de vrais octets gagne, et sa provenance est conservée. Si aucune ne répond, l’image reste signalée comme manquante sans faire échouer l’article complet. Perdre une capture d’écran de Visual Studio 2015 est triste, mais pas au point d’enterrer avec elle trois pages de texte et deux exemples C#.

## Utiliser NecroBlog CLI

Dans sa version actuelle, NecroBlog CLI connaît uniquement les deux sources pour lesquelles je l’ai créé : `jeromegiacomini.net` et `blogs.infinitesquare.com`. Il ne suffit donc pas de lui passer l’adresse de n’importe quel blog en paramètre en espérant que la nécromancie fasse le reste.

Pour l’utiliser avec votre ancien blog, il faut commencer par [forker le projet sur GitHub](https://github.com/jgiacomini/NecroBlogCLI), cloner votre fork, puis ajouter votre propre source dans le code. Le projet cible .NET 10.

```bash
git clone https://github.com/votre-compte/NecroBlogCLI.git
cd NecroBlogCLI
dotnet build
```

### Déclarer votre source

Les sources sont définies dans `Models/SourceDefinition.cs`. Une source indique à NecroBlog les domaines à rechercher dans la Wayback Machine et surtout la forme d’une URL d’article. Il faut ajouter la nouvelle entrée dans le tableau retourné par la méthode `BuildAll()`.

Pour un ancien blog fictif dont les articles utilisaient des URL comme `https://monancienblog.fr/blog/2020/06/15/mon-article/`, la définition pourrait ressembler à ceci :

```csharp
new SourceDefinition
{
    Id = "monblog",
    DisplayName = "monancienblog.fr",
    Author = "Votre nom",
    SeedUrls =
    [
        "monancienblog.fr/*",
        "www.monancienblog.fr/*",
    ],
    OwnHosts =
    [
        "monancienblog.fr",
        "www.monancienblog.fr",
    ],
    ArticlePathRegex = MonBlogArticlePathRegex(),
},
```

Il faut ensuite ajouter l’expression régulière correspondant au chemin des articles :

```csharp
[GeneratedRegex(
    @"^/blog/\d{4}/\d{2}/\d{2}/[^/]+/?$",
    RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex MonBlogArticlePathRegex();
```

`SeedUrls` indique les domaines que l’API CDX doit explorer. `OwnHosts` permet de reconnaître les liens internes, tandis que `ArticlePathRegex` évite de prendre les catégories, les tags, les flux RSS et l’inévitable page de contact pour des articles.

Cette expression doit naturellement être adaptée à votre ancien moteur de blog. Si vos articles ressemblaient plutôt à `/posts/mon-article` ou `/archives/42`, c’est ce format qu’il faut décrire. Une bonne expression régulière à cette étape épargne beaucoup de tri manuel ensuite.

### Adapter l’extraction du contenu si nécessaire

NecroBlog essaie ensuite d’extraire le contenu avec son extracteur générique. Avant de lancer une récupération complète, la commande `inspect` permet de vérifier ce qu’il reconnaît sur une URL précise :

```bash
dotnet run -- inspect "https://monancienblog.fr/blog/2020/06/15/mon-article/"
```

Elle affiche notamment le titre, la date, l’auteur, le sélecteur HTML retenu, la longueur du contenu et le nombre de blocs de code ou d’images détectés. Avec l’option `--save-dir`, elle peut aussi enregistrer le HTML et le Markdown produits pour les examiner tranquillement :

```bash
dotnet run -- inspect "https://monancienblog.fr/blog/2020/06/15/mon-article/" --save-dir ./inspect-out
```

Si l’extracteur générique récupère le menu, oublie la moitié de l’article ou considère le pied de page comme une conclusion particulièrement inspirée, il faut créer un extracteur propre à l’ancien site. Celui-ci implémente `IArticleExtractor` et sélectionne précisément le conteneur du titre, de la date, de l’auteur et du contenu. Il doit ensuite être ajouté dans `ExtractorPipeline`, avant `GenericArticleExtractor` :

```csharp
_extractors =
[
    new JeromeGiacominiExtractor(),
    new InfiniteSquareExtractor(),
    new MonBlogExtractor(),
    new GenericArticleExtractor(),
];
```

L’extracteur générique reste ainsi le dernier recours, tandis que votre extracteur est utilisé dès que le `sourceId` vaut `monblog`.

### Lancer la récupération

Une fois la source ajoutée et une URL validée avec `inspect`, je conseille de commencer par quelques articles grâce à `--limit` :

```bash
dotnet run -- discover --source monblog
dotnet run -- recover --source monblog --download-images true --limit 5
dotnet run -- report
```

`discover` construit le catalogue local à partir de la nouvelle source. `recover` extrait et convertit les cinq premiers articles, tandis que `report` récapitule les contenus suspects, les échecs et les images absentes.

Si le résultat est satisfaisant, il suffit de relancer la récupération sans limite :

```bash
dotnet run -- recover --source monblog --download-images true --resume
```

La commande de récupération est reprenable avec `--resume`. Les téléchargements sont mis en cache et l’échec d’un article n’interrompt pas les suivants. Cette propriété devient vite essentielle : une restauration complète dure assez longtemps pour que le réseau, un serveur distant ou votre ordinateur décide de participer au test de résilience.

## Le résultat de la résurrection

Cette opération m’a permis de réintégrer dans ce blog :

- **52 articles** provenant de mon ancien site `jeromegiacomini.net` ;
- **8 articles uniques** publiés sur `blogs.infinitesquare.com` ;
- **12 images** retrouvées et stockées localement.

J’ai aussi écarté 17 republications pour ne conserver qu’une seule version de chaque texte. Au total, **60 articles uniques** ont retrouvé une vie en Markdown.

Tout n’a pas pu être récupéré parfaitement. Certaines images restent perdues et quelques métadonnées n’existent simplement plus. Mais les textes, les exemples de code et leur provenance sont de nouveau stockés dans un format lisible, versionnable et beaucoup moins dépendant d’un moteur de blog particulier.

## Ce que j’en retiens

La Wayback Machine est une ressource extraordinaire, mais elle ne transforme pas automatiquement un ancien site en archive propre. Pour obtenir un résultat réutilisable, il faut distinguer le catalogue des captures de leur contenu, normaliser les URL, connaître un minimum la structure du site, prévoir plusieurs captures de secours et accepter de signaler ce que l’on ne sait pas.

Le Markdown constitue ensuite une bonne destination : les fichiers sont simples, portables, faciles à relire et à conserver dans Git. Si le moteur actuel du blog disparaît un jour, je devrais donc pouvoir migrer plus facilement.

Enfin, en théorie.

Sinon, je suppose que je n’aurai plus qu’à écrire **NecroBlog CLI 2 : le retour du retour des morts**.

Happy coding 🙂
