ServicesÀ proposNotesContact Me contacter →
EN FR
Note

Parseur Markdown vers blocs Notion

Comment convertir du markdown au format bloc de l'API Notion en JavaScript, notamment la gestion des objets rich_text, de la limite des 2 000 caractères et du plafond de 100 blocs par requête.

Planté
automationdata engineering

Notion n’accepte pas le markdown brut. Pour ajouter du contenu par programmation à une page Notion, vous devez convertir ce contenu en un tableau d’objets « bloc » au format JSON spécifique de Notion. Il n’existe pas de point d’entrée d’import markdown natif.

Cela pose un vrai problème pour tout workflow qui génère ou extrait du markdown et souhaite le stocker dans Notion. Le Workflow RSS-vers-Notion n8n rencontre ce problème après l’étape de nettoyage par le LLM : il dispose d’un markdown propre, mais Notion ne l’accepte pas directement.

La solution est un parseur JavaScript personnalisé — environ 400 lignes de code — qui convertit le markdown en blocs Notion. Cette note explique le fonctionnement du format bloc de Notion, ce que fait le parseur et quels cas limites il doit gérer.

Fonctionnement de l’API bloc de Notion

Chaque élément de contenu dans Notion est un « bloc ». Un bloc a un type (paragraph, heading_1, bulleted_list_item, etc.) et un contenu spécifique à ce type. La plupart des types de blocs ont un tableau rich_text qui contient le contenu textuel réel sous forme de segments formatés.

Un bloc paragraph minimal ressemble à ceci :

{
"type": "paragraph",
"paragraph": {
"rich_text": [
{
"type": "text",
"text": {
"content": "Bonjour le monde"
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
}
}
]
}
}

Un paragraphe mêlant texte en gras et un lien nécessite plusieurs segments rich_text :

{
"type": "paragraph",
"paragraph": {
"rich_text": [
{
"type": "text",
"text": { "content": "Consultez la " },
"annotations": { "bold": false, "italic": false, ... }
},
{
"type": "text",
"text": {
"content": "documentation de l'API Notion",
"link": { "url": "https://developers.notion.com" }
},
"annotations": { "bold": true, "italic": false, ... }
}
]
}
}

Chaque changement de formatage — gras activé, gras désactivé, début de lien, fin de lien — nécessite un nouveau segment. Un paragraphe fortement formaté peut produire une douzaine d’objets rich_text.

Types de blocs gérés par le parseur

Le parseur markdown vers Notion doit couvrir chaque élément de niveau bloc susceptible d’apparaître dans un article nettoyé :

MarkdownType de bloc Notion
# Titreheading_1
## Titreheading_2
### Titreheading_3
#### H4+paragraph avec annotation bold
Paragraphe normalparagraph
- Élément de listebulleted_list_item
1. Élément de listenumbered_list_item
> Texte citéquote
```bloc de code```code
![alt](url)image
--- ou ***divider

Notion ne supporte que trois niveaux de titres. Les articles ont parfois des titres H4-H6 (en particulier les contenus techniques avec des sections profondément imbriquées). Le parseur les convertit en paragraphes en gras — ce n’est pas parfait, mais cela préserve le contenu et ne fait pas échouer l’appel API.

Analyse du formatage en ligne

Dans chaque bloc, le parseur doit identifier les spans de formatage en ligne et découper le texte en conséquence. Les patterns en ligne à détecter :

// Gras : **texte** ou __texte__
const boldPattern = /\*\*(.*?)\*\*|__(.*?)__/g;
// Italique : *texte* ou _texte_
const italicPattern = /\*(.*?)\*|_(.*?)_/g;
// Code en ligne : `code`
const codePattern = /`([^`]+)`/g;
// Lien : [texte](url)
const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;

Le parseur parcourt chaque ligne, correspond à ces patterns et construit le tableau rich_text en découpant la chaîne à chaque limite de formatage. Le texte entre les spans formatés devient un segment ordinaire ; les spans formatés deviennent des segments avec les annotations appropriées.

Le formatage imbriqué (gras italique, lien gras) nécessite de combiner les annotations de plusieurs patterns. Un segment à l’intérieur des marqueurs ** et _ obtient bold: true, italic: true.

La limite des 2 000 caractères

Notion a une limite stricte de 2 000 caractères par élément rich_text. Cela importe pour les longs paragraphes — les articles techniques ont parfois des paragraphes denses qui dépassent cette limite.

Le parseur inclut une fonction splitLongBlocks qui s’exécute après la conversion initiale :

function splitLongBlocks(blocks) {
const result = [];
for (const block of blocks) {
if (!block[block.type]?.rich_text) {
result.push(block);
continue;
}
// Calculer le nombre total de caractères dans tous les segments
const totalChars = block[block.type].rich_text
.reduce((sum, segment) => sum + segment.text.content.length, 0);
if (totalChars <= 2000) {
result.push(block);
continue;
}
// Découper en morceaux, en respectant les limites de segment si possible
// Revient à découper au milieu d'un segment pour les blocs de code
result.push(...splitBlock(block, 2000));
}
return result;
}

La logique de découpage essaie d’abord de couper aux limites de segment (entre les éléments rich_text). Si un seul segment est lui-même plus long que 2 000 caractères (courant dans les blocs de code), il découpe la chaîne de contenu à 2 000 caractères et crée un nouveau segment.

La limite de 100 blocs par requête

L’API « append block children » de Notion accepte un maximum de 100 blocs par requête. Les articles plus longs peuvent facilement produire plus de blocs que cela.

Le workflow gère cela en prenant les 100 premiers blocs. C’est un compromis pragmatique : la plupart des articles tiennent dans 100 blocs, et le contenu coupé est généralement la fin des articles les plus longs — souvent des sections de conclusion et des liens « connexes » qui apportent moins de valeur de toute façon.

Une approche plus sophistiquée consisterait à découper en plusieurs appels API : blocs 1 à 100, puis 101 à 200, etc. L’implémentation actuelle reste simple et cette simplicité est défendable pour une base de connaissances personnelle.

Caractères échappés et cas limites

Le markdown extrait du web est plus désordonné que le markdown que vous écrivez vous-même. Quelques cas limites spécifiques que le parseur gère :

Underscores échappés : \_comme\_ceci\_ apparaît dans les contenus extraits lorsque le HTML d’origine contenait des underscores qu’un convertisseur markdown a échappés de manière défensive. Le parseur les déséchappe avant le traitement.

Blockquotes vides : une ligne > sans contenu produit un bloc de citation vide dans Notion qui a une apparence maladroite. Les citations vides sont ignorées.

Images liées : [![alt](image-url)](link-url) — une image enveloppée dans un lien — apparaît occasionnellement dans les contenus extraits. Le parseur les désenveloppe pour n’obtenir que le bloc image, puisque Notion ne supporte pas les images liées de toute façon.

Caractères de formatage parasites : des lignes contenant uniquement * * * ou --- dans diverses combinaisons provenant de mauvais convertisseurs markdown. Ceux-ci sont convertis en blocs divider plutôt que de déclencher des erreurs d’analyse.

Structure de sortie

Le parseur retourne non seulement les blocs mais aussi des métadonnées de débogage :

{
children: blocksToSend, // Tableau d'objets bloc Notion (max 100)
meta: {
model: "gpt-4o-mini", // Transmis depuis l'étape LLM
created: "...", // Horodatage
total_blocks: 45, // Blocs avant le découpage des longs
split_blocks: 47, // Blocs après le découpage (certains longs en ont produit 2)
blocks_sent: 47 // Ce qui est réellement envoyé à Notion
}
}

Les métadonnées sont utiles pour déboguer pourquoi un article s’affiche mal dans Notion : vous pouvez voir si le problème provient de trop de blocs coupés (total_blocks élevé) ou si le découpage s’est produit de manière inattendue (grand écart entre total_blocks et split_blocks).

Pourquoi ne pas utiliser une bibliothèque existante ?

Des bibliothèques existent pour la conversion markdown vers Notion (comme @tryfabric/martian pour Node.js). Le parseur personnalisé existe parce que :

  1. Le workflow s’exécute dans le sandbox JavaScript de n8n, qui dispose d’un ensemble limité de modules disponibles
  2. Un parseur personnalisé peut gérer les cas limites spécifiques des contenus extraits qu’une bibliothèque générale pourrait ne pas traiter
  3. Le format de sortie peut être ajusté pour correspondre aux besoins spécifiques de ce workflow

Si vous créez une application autonome en dehors de n8n, l’utilisation d’une bibliothèque établie est probablement le meilleur choix. Si vous êtes dans un nœud Code n8n, vous aurez probablement besoin de quelque chose comme ceci.

Connexes