This workflow follows the Datatable → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"//": "Jobby Markdown Editor - jobby-pdf-dyn.json - Dynamic PDF generator workflow",
"updatedAt": "2026-06-03T11:51:05.014Z",
"createdAt": "2026-05-29T09:56:13.800Z",
"id": "dSX8vdBy3PnPdiOU",
"name": "Jobby - PDF - dyn",
"description": "retrieves a MD file and returns a printed version in PDF",
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "jobby-pdf",
"authentication": "headerAuth",
"options": {
"responseCode": {
"values": {}
}
}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-656,
-80
],
"id": "55f844ef-c35d-46d3-92b5-23285e3fd934",
"name": "Webhook",
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// 1. R\u00e9cup\u00e9ration du CV et des deux nouvelles propri\u00e9t\u00e9s Notion\nconst properties = $('Webhook').item.json.body?.data?.properties || {};\nconst richTextArray = properties.CV?.rich_text || [];\n\n// Extraction propre depuis Get PageID\nconst company = $('Get PageID').first().json.company || 'company';\nconst jobTitle = $('Get PageID').first().json.jobTitle || 'job';\nconst increment = $('Get PageID').first().json.increment || '01';\n\nlet mdText = richTextArray.map(block => block.plain_text || '').join('');\n\n// Fallback: Parser Markdown robuste align\u00e9 sur marked.js\nfunction robustMarkdownToHtml(md) {\n const lines = md.split(/\\r?\\n/);\n let htmlOutput = [];\n let inList = false;\n\n for (let line of lines) {\n let trimmed = line.trim();\n if (!/^[-\\*]\\s+/.test(trimmed) && inList) {\n htmlOutput.push('</ul>');\n inList = false;\n }\n if (trimmed === '---' || trimmed === '***') {\n htmlOutput.push('<hr />');\n continue;\n }\n if (trimmed.startsWith('>')) {\n let content = line.replace(/^>\\s*/, '').trim().replace(/^\"(.*)\"$/, '$1');\n htmlOutput.push(`<blockquote>${content}</blockquote>`);\n continue;\n }\n if (trimmed.startsWith('# ')) { htmlOutput.push(`<h1>${trimmed.substring(2)}</h1>`); continue; }\n if (trimmed.startsWith('## ')) { htmlOutput.push(`<h2>${trimmed.substring(3)}</h2>`); continue; }\n if (trimmed.startsWith('### ')) { htmlOutput.push(`<h3>${trimmed.substring(4)}</h3>`); continue; }\n if (trimmed.startsWith('#### ')) { htmlOutput.push(`<h4>${trimmed.substring(5)}</h4>`); continue; }\n if (trimmed.startsWith('##### ')) { htmlOutput.push(`<h5>${trimmed.substring(6)}</h5>`); continue; }\n if (trimmed.startsWith('###### ')) { htmlOutput.push(`<h6>${trimmed.substring(7)}</h6>`); continue; }\n\n if (/^[-\\*]\\s+/.test(trimmed)) {\n if (!inList) { htmlOutput.push('<ul>'); inList = true; }\n htmlOutput.push(`<li>${trimmed.replace(/^[-\\*]\\s+/, '')}</li>`);\n continue;\n }\n if (trimmed !== '') {\n if (line.includes('\u2022') || line.includes('\u00b7')) {\n htmlOutput.push(`<p style=\"text-align: justify; text-justify: inter-word;\">${line}</p>`);\n } else {\n htmlOutput.push(`<p>${line}</p>`);\n }\n }\n }\n if (inList) htmlOutput.push('</ul>');\n\n let finalBody = htmlOutput.join('\\n');\n finalBody = finalBody.replace(/\\*\\*(.*?)\\*\\*/g, '<strong>$1</strong>');\n finalBody = finalBody.replace(/\\*(.*?)\\*/g, '<em>$1</em>');\n finalBody = finalBody.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\">$1</a>');\n return finalBody;\n}\n\n// Fonction utilitaire pour nettoyer les caract\u00e8res sp\u00e9ciaux du futur nom de fichier\nfunction slugify(text) {\n return text\n .toString()\n .toLowerCase()\n .normalize('NFD') // Supprime les accents\n .replace(/[\\u0300-\\u036f]/g, '')\n .trim()\n .replace(/\\s+/g, '-') // Remplace les espaces par des tirets\n .replace(/[^a-z0-9\\-]/g, ''); // Supprime le reste\n}\n\nconst finalFileName = \"javarre-\" + slugify(company) + \"-\" + slugify(jobTitle);\n\n// 3. RECUPERATION CONFIG & CSS DE DATA TABLE\nconst config = JSON.parse($('Read Config from Table').first().json.value);\nconst templatesCss = $('Read CSS from Table').first().json.value;\n\n// 4. PARSER MARKDOWN AVEC LE MEME COMPILATEUR QUE L'EDITEUR (MARKED.JS VIA CDN)\nlet compiledHtml;\ntry {\n const cdnUrl = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';\n const response = await fetch(cdnUrl);\n if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);\n const markedText = await response.text();\n const evalGlobal = new Function(markedText + '\\nreturn marked;');\n const marked = evalGlobal();\n \n marked.setOptions({\n gfm: true,\n breaks: true\n });\n \n let processedMd = mdText;\n processedMd = processedMd.replace(/:accent\\[([^\\]]+)\\]/g, '<span class=\"resume-accent\">$1</span>');\n processedMd = processedMd.replace(/:muted\\[([^\\]]+)\\]/g, '<span class=\"resume-muted\">$1</span>');\n \n compiledHtml = marked.parse(processedMd);\n \n // Post-process styling for paragraph tags with separators/bullets to justify\n compiledHtml = compiledHtml.replace(/<p>((?:(?!<\\/p>).)*(?:[\u2022\u00b7])(?:(?!<\\/p>).)*)<\\/p>/g, '<p style=\"text-align: justify; text-justify: inter-word;\">$1</p>');\n} catch (error) {\n console.warn('Fallback: Failed to load marked.js from CDN, using robustMarkdownToHtml', error);\n compiledHtml = robustMarkdownToHtml(mdText);\n compiledHtml = compiledHtml.replace(/:accent\\[([^\\]]+)\\]/g, '<span class=\"resume-accent\">$1</span>');\n compiledHtml = compiledHtml.replace(/:muted\\[([^\\]]+)\\]/g, '<span class=\"resume-muted\">$1</span>');\n}\n\n// Traitement du bloc contact si pr\u00e9sent\ncompiledHtml = compiledHtml.replace(/\\\\\\[CONTACT\\s*:\\s*([^\\]]+)\\\\\\]/gi, (match, contents) => {\n const parts = contents.split('|').map(p => p.trim());\n const formattedParts = parts.map(part => {\n if (part.includes('@') && !part.includes(' ')) {\n return `<a href=\"mailto:${part}\">∂</a>`.replace('∂', part);\n }\n if (part.startsWith('http://') || part.startsWith('https://')) {\n const cleanUrl = part.replace(/^https?:\\/\\/(www\\.)?/, '');\n return `<a href=\"${part}\" target=\"_blank\">&cleanUrl;</a>`.replace('&cleanUrl;', cleanUrl);\n }\n return `<span>${part}</span>`;\n });\n return `<div class=\"resume-contact-bar\">&parts;</div>`.replace('&parts;', formattedParts.join(' \u2022 '));\n});\n\n// 5. LAYOUT 2 COLUMNS RESTRUCTURING\nlet finalHtml = compiledHtml;\nif (config.layoutMode === '2-column') {\n const parts = compiledHtml.split(/(?=<h[23]\\b)/i);\n const headerHtml = parts[0];\n let mainHtml = '';\n let sidebarHtml = '';\n \n for (let i = 1; i < parts.length; i++) {\n const part = parts[i];\n if (part.toLowerCase().startsWith('<h2')) {\n mainHtml += part;\n } else if (part.toLowerCase().startsWith('<h3')) {\n sidebarHtml += part;\n }\n }\n \n finalHtml = `\n <div class=\"resume-header\">\n ${headerHtml}\n </div>\n <div class=\"resume-columns ${config.sidebarPosition === 'left' ? 'sidebar-left' : ''}\">\n <div class=\"resume-main-col\">\n ${mainHtml}\n </div>\n <div class=\"resume-sidebar-col\" style=\"background-color: ${config.sidebarBg}; color: ${config.sidebarText};\">\n ${sidebarHtml}\n ${config.showVersion ? `<div class=\"resume-version-sidebar\">v${increment}</div>` : ''}\n </div>\n </div>\n `;\n} else {\n if (config.showVersion) {\n finalHtml = finalHtml + `<div class=\"resume-version-footer\">v${increment}</div>`;\n }\n}\n\n// 6. GENERATE INLINE CSS VARIABLES\nconst inlineVariables = `\n:root {\n --resume-font-family: ${config.fontFamily};\n --resume-font-size: ${config.fontSize}px;\n --resume-line-height: ${config.lineHeight};\n --resume-heading-scale: ${config.headingScale};\n --resume-margin-x: ${config.marginX}px;\n --resume-margin-y: ${config.marginY}px;\n --resume-section-spacing: ${config.sectionSpacing}px;\n --resume-color-bg: ${config.colorBg || '#ffffff'};\n --resume-color-headings: ${config.colorHeadings};\n --resume-color-body: ${config.colorBody};\n --resume-color-links: ${config.colorLinks};\n --resume-color-accent: ${config.colorAccent};\n --resume-sidebar-bg: ${config.sidebarBg || '#2d3748'};\n --resume-sidebar-text: ${config.sidebarText || '#ffffff'};\n}`;\n\n// 7. ASSEMBLE STANDALONE DOCUMENT\nconst standaloneHtml = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${finalFileName}</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,600;0,700;1,400&family=Raleway:wght@300;400;500;600;700;800&family=Merriweather:ital,wght@0,300;0,400;0,700;1,300&family=JetBrains+Mono:wght@400;500;700&family=Lora:ital,wght@0,400;0,500;0,600;1,400&display=swap\" rel=\"stylesheet\">\n <style>\n ${inlineVariables}\n ${templatesCss}\n @media print {\n body {\n display: block !important;\n width: 100% !important;\n height: auto !important;\n background: #ffffff !important;\n }\n .a4-sheet {\n width: 100% !important;\n margin: 0 !important;\n box-shadow: none !important;\n }\n }\n body {\n background-color: var(--resume-color-bg, #ffffff);\n margin: 0;\n padding: 0;\n display: flex;\n justify-content: center;\n }\n .a4-sheet {\n box-shadow: none !important;\n border-radius: 0 !important;\n margin: 0 auto;\n }\n </style>\n</head>\n<body>\n <article class=\"a4-sheet\" id=\"resume-output\">\n ${finalHtml}\n </article>\n</body>\n</html>`;\n\nreturn [\n {\n json: {\n compiledBody: standaloneHtml,\n pdfFileName: finalFileName,\n printBackground: \"true\",\n marginTop: \"0in\",\n marginBottom: \"0in\",\n marginLeft: \"0in\",\n marginRight: \"0in\"\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
240,
-80
],
"id": "ba4574e0-c63b-4d8c-b043-d21b29954aaf",
"name": "G\u00e9n\u00e9ration du HTML & Style CSS"
},
{
"parameters": {
"method": "POST",
"url": "={{ $node[\"Notion : Initialiser l'upload\"].json.upload_url }}",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "notionApi",
"sendQuery": true,
"queryParameters": {
"parameters": []
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Notion-Version",
"value": "2022-06-28"
}
]
},
"sendBody": true,
"contentType": "multipart-form-data",
"bodyParameters": {
"parameters": [
{
"parameterType": "formBinaryData",
"name": "file",
"inputDataFieldName": "={{ $node[\"(Gotemberg) PDF\"].binary.data }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1360,
-80
],
"id": "425d0195-9b44-4887-ad6c-a86a6af34806",
"name": "Notion : Envoyer le binaire",
"credentials": {
"notionApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"method": "POST",
"url": "https://api.notion.com/v1/file_uploads",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "notionApi",
"sendQuery": true,
"queryParameters": {
"parameters": []
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Notion-Version",
"value": " 2022-06-28"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"mode\": \"single_part\",\n \"filename\": \"{{ $('Get PageID').item.json.outputPdf }}\",\n \"content_type\": \"application/pdf\"\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1136,
-80
],
"id": "85ce1de6-67c4-4e7b-be47-b4b4c3a58616",
"name": "Notion : Initialiser l'upload",
"credentials": {
"notionApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// 1. R\u00e9cup\u00e9ration des donn\u00e9es brutes du Webhook\nconst body = $('Webhook').item.json.body?.data || {};\nconst properties = body.properties || {};\n\nconst pageId = body.id;\n\n// 2. Extraction de Company (Type: select)\nlet company = 'company';\nif (properties.Company && properties.Company.select) {\n company = properties.Company.select.name || 'company';\n}\n\n// 3. Extraction de Job Title / Title (Type: title) - Tol\u00e8re l'ancien et le nouveau nom\nlet jobTitle = 'job';\nconst targetProperty = properties.Title || properties[\"Job Title\"];\n\nif (targetProperty && targetProperty.title && targetProperty.title.length > 0) {\n jobTitle = targetProperty.title.plain_text || $input.first().json.body.data.properties[\"Job Title\"].title[0].plain_text || 'job';\n}\n\n// 4. Fonction locale slugify pour nettoyer les cha\u00eenes\nfunction slugify(text) {\n return text\n .toString()\n .toLowerCase()\n .normalize('NFD') // Supprime les accents\n .replace(/[\\u0300-\\u036f]/g, '')\n .trim()\n .replace(/\\s+/g, '-') // Remplace les espaces par des tirets\n .replace(/[^a-z0-9\\-]/g, ''); // \u00c9limine le reste\n}\n\n// 5. Forge du nom de fichier final personnalis\u00e9\nconst outputPdf = `javarre-${slugify(company)}_${slugify(jobTitle)}.pdf`;\n\n// 6. Retour des variables propres\nreturn [{ \n json: { \n pageId: pageId,\n company: company,\n jobTitle: jobTitle,\n outputPdf: outputPdf\n } \n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-432,
-80
],
"id": "3ad811d9-5ff0-4e35-97c3-231e5d5b3553",
"name": "Get PageID"
},
{
"parameters": {
"method": "PATCH",
"url": "=https://api.notion.com/v1/pages/{{ $node[\"Get PageID\"].json.pageId }}",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "notionApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Notion-Version",
"value": "2026-03-11"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"properties\": {\n \"PDF\": {\n \"type\": \"files\",\n \"files\": [\n {\n \"type\": \"file_upload\",\n \"file_upload\": { \n \"id\": \"{{ $('Notion : Initialiser l\\'upload').item.json.id }}\" \n },\n \"name\": \"{{ $json.filename }}\"\n }\n ]\n }\n }\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1584,
-80
],
"id": "aee20dc4-18f3-4b2b-a575-1e9066f32baa",
"name": "Attach Bin to page",
"credentials": {
"notionApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"method": "POST",
"url": "http://gotenberg:3000/forms/chromium/convert/html",
"sendBody": true,
"contentType": "multipart-form-data",
"bodyParameters": {
"parameters": [
{
"parameterType": "formBinaryData",
"name": "index.html",
"inputDataFieldName": "indexHtml"
},
{
"name": "printBackground",
"value": "true"
},
{
"name": "marginTop",
"value": "0in"
},
{
"name": "marginBottom",
"value": "0in"
},
{
"name": "marginLeft",
"value": "0in"
},
{
"name": "marginRight",
"value": "0in"
},
{
"name": "preferCssPageSize",
"value": "true"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
912,
-80
],
"id": "d28e993d-2353-418d-904b-cd4b6d1ac7d2",
"name": "(Gotemberg) PDF"
},
{
"parameters": {
"operation": "toText",
"sourceProperty": "myRawContent",
"binaryPropertyName": "indexHtml",
"options": {
"addBOM": false,
"encoding": "utf8",
"fileName": "=index.html"
}
},
"type": "n8n-nodes-base.convertToFile",
"typeVersion": 1.1,
"position": [
688,
-80
],
"id": "d43058ca-042b-4786-85ec-7551ff479f79",
"name": "Convert to File"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "1fb1391e-c916-48ff-ae50-b4da474a5ea1",
"name": "myRawContent",
"value": "={{ $json.compiledBody }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
464,
-80
],
"id": "ba499321-0a26-4a49-b389-3540fa4eed82",
"name": "Template Design CV"
},
{
"parameters": {
"operation": "get",
"dataTableId": {
"__rl": true,
"value": "OtWoNuYUmoj7knoz",
"mode": "id"
},
"matchType": "allConditions",
"filters": {
"conditions": [
{
"keyName": "key",
"keyValue": "config"
}
]
}
},
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1,
"position": [
-208,
-80
],
"id": "a4c9b321-0a26-4a49-b389-3540fa4eed10",
"name": "Read Config from Table"
},
{
"parameters": {
"operation": "get",
"dataTableId": {
"__rl": true,
"value": "OtWoNuYUmoj7knoz",
"mode": "id"
},
"matchType": "allConditions",
"filters": {
"conditions": [
{
"keyName": "key",
"keyValue": "css"
}
]
}
},
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1,
"position": [
16,
-80
],
"id": "ba499321-0a26-4a49-b389-3540fa4eed20",
"name": "Read CSS from Table"
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Get PageID",
"type": "main",
"index": 0
}
]
]
},
"G\u00e9n\u00e9ration du HTML & Style CSS": {
"main": [
[
{
"node": "Template Design CV",
"type": "main",
"index": 0
}
]
]
},
"Notion : Envoyer le binaire": {
"main": [
[
{
"node": "Attach Bin to page",
"type": "main",
"index": 0
}
]
]
},
"Notion : Initialiser l'upload": {
"main": [
[
{
"node": "Notion : Envoyer le binaire",
"type": "main",
"index": 0
}
]
]
},
"Get PageID": {
"main": [
[
{
"node": "Read Config from Table",
"type": "main",
"index": 0
}
]
]
},
"(Gotemberg) PDF": {
"main": [
[
{
"node": "Notion : Initialiser l'upload",
"type": "main",
"index": 0
}
]
]
},
"Convert to File": {
"main": [
[
{
"node": "(Gotemberg) PDF",
"type": "main",
"index": 0
}
]
]
},
"Template Design CV": {
"main": [
[
{
"node": "Convert to File",
"type": "main",
"index": 0
}
]
]
},
"Read Config from Table": {
"main": [
[
{
"node": "Read CSS from Table",
"type": "main",
"index": 0
}
]
]
},
"Read CSS from Table": {
"main": [
[
{
"node": "G\u00e9n\u00e9ration du HTML & Style CSS",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": false
},
"staticData": null,
"meta": {
"templateCredsSetupCompleted": true
},
"versionId": "a9ab848d-0d02-43a2-8728-ca1997482ade",
"activeVersionId": "a9ab848d-0d02-43a2-8728-ca1997482ade",
"versionCounter": 65,
"triggerCount": 1,
"shared": [
{
"updatedAt": "2026-05-29T09:56:13.805Z",
"createdAt": "2026-05-29T09:56:13.805Z",
"role": "workflow:owner",
"workflowId": "dSX8vdBy3PnPdiOU",
"projectId": "GfFdmZTqGEJQkrXG",
"project": {
"updatedAt": "2026-05-11T09:44:44.757Z",
"createdAt": "2026-05-11T09:34:45.695Z",
"id": "GfFdmZTqGEJQkrXG",
"name": "\u00c9ole Wind <megazef@gmail.com>",
"type": "personal",
"icon": null,
"description": null,
"creatorId": "2792484d-cba3-4156-adba-fbc49134eb55"
}
}
],
"tags": [
{
"updatedAt": "2026-05-29T11:31:39.972Z",
"createdAt": "2026-05-29T11:31:39.972Z",
"id": "5zynHpPHQi9PRMyD",
"name": "jobby"
},
{
"updatedAt": "2026-05-28T12:28:46.096Z",
"createdAt": "2026-05-28T12:28:46.096Z",
"id": "Ft2TXB4BVRBQRwkV",
"name": "PDF"
},
{
"updatedAt": "2026-05-28T12:28:49.321Z",
"createdAt": "2026-05-28T12:28:49.321Z",
"id": "XmFLRaZ2ZU05mmMo",
"name": "Gotemberg"
}
],
"activeVersion": {
"updatedAt": "2026-06-03T11:51:05.017Z",
"createdAt": "2026-06-03T11:51:05.017Z",
"versionId": "a9ab848d-0d02-43a2-8728-ca1997482ade",
"workflowId": "dSX8vdBy3PnPdiOU",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "jobby-pdf",
"authentication": "headerAuth",
"options": {
"responseCode": {
"values": {}
}
}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-656,
-80
],
"id": "55f844ef-c35d-46d3-92b5-23285e3fd934",
"name": "Webhook",
"webhookId": "ce00eb54-6844-400c-9aa6-8c1ceb15d940",
"credentials": {
"httpHeaderAuth": {
"id": "QDm4dLNTsiXmLpmM",
"name": "Header Auth account"
}
}
},
{
"parameters": {
"jsCode": "// 1. R\u00e9cup\u00e9ration du CV et des deux nouvelles propri\u00e9t\u00e9s Notion\nconst properties = $('Webhook').item.json.body?.data?.properties || {};\nconst richTextArray = properties.CV?.rich_text || [];\n\n// Extraction propre depuis Get PageID\nconst company = $('Get PageID').first().json.company || 'company';\nconst jobTitle = $('Get PageID').first().json.jobTitle || 'job';\n\nlet mdText = richTextArray.map(block => block.plain_text || '').join('');\n\n// Fallback: Parser Markdown robuste align\u00e9 sur marked.js\nfunction robustMarkdownToHtml(md) {\n const lines = md.split(/\\r?\\n/);\n let htmlOutput = [];\n let inList = false;\n\n for (let line of lines) {\n let trimmed = line.trim();\n if (!/^[-\\*]\\s+/.test(trimmed) && inList) {\n htmlOutput.push('</ul>');\n inList = false;\n }\n if (trimmed === '---' || trimmed === '***') {\n htmlOutput.push('<hr />');\n continue;\n }\n if (trimmed.startsWith('>')) {\n let content = line.replace(/^>\\s*/, '').trim().replace(/^\"(.*)\"$/, '$1');\n htmlOutput.push(`<blockquote>${content}</blockquote>`);\n continue;\n }\n if (trimmed.startsWith('# ')) { htmlOutput.push(`<h1>${trimmed.substring(2)}</h1>`); continue; }\n if (trimmed.startsWith('## ')) { htmlOutput.push(`<h2>${trimmed.substring(3)}</h2>`); continue; }\n if (trimmed.startsWith('### ')) { htmlOutput.push(`<h3>${trimmed.substring(4)}</h3>`); continue; }\n if (trimmed.startsWith('#### ')) { htmlOutput.push(`<h4>${trimmed.substring(5)}</h4>`); continue; }\n if (trimmed.startsWith('##### ')) { htmlOutput.push(`<h5>${trimmed.substring(6)}</h5>`); continue; }\n if (trimmed.startsWith('###### ')) { htmlOutput.push(`<h6>${trimmed.substring(7)}</h6>`); continue; }\n\n if (/^[-\\*]\\s+/.test(trimmed)) {\n if (!inList) { htmlOutput.push('<ul>'); inList = true; }\n htmlOutput.push(`<li>${trimmed.replace(/^[-\\*]\\s+/, '')}</li>`);\n continue;\n }\n if (trimmed !== '') {\n if (line.includes('\u2022') || line.includes('\u00b7')) {\n htmlOutput.push(`<p style=\"text-align: justify; text-justify: inter-word;\">${line}</p>`);\n } else {\n htmlOutput.push(`<p>${line}</p>`);\n }\n }\n }\n if (inList) htmlOutput.push('</ul>');\n\n let finalBody = htmlOutput.join('\\n');\n finalBody = finalBody.replace(/\\*\\*(.*?)\\*\\*/g, '<strong>$1</strong>');\n finalBody = finalBody.replace(/\\*(.*?)\\*/g, '<em>$1</em>');\n finalBody = finalBody.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\">$1</a>');\n return finalBody;\n}\n\n// Fonction utilitaire pour nettoyer les caract\u00e8res sp\u00e9ciaux du futur nom de fichier\nfunction slugify(text) {\n return text\n .toString()\n .toLowerCase()\n .normalize('NFD') // Supprime les accents\n .replace(/[\\u0300-\\u036f]/g, '')\n .trim()\n .replace(/\\s+/g, '-') // Remplace les espaces par des tirets\n .replace(/[^a-z0-9\\-]/g, ''); // Supprime le reste\n}\n\nconst finalFileName = `javarre-${slugify(company)}-${slugify(jobTitle)}`;\n\n// 3. RECUPERATION CONFIG & CSS DE DATA TABLE\nconst config = JSON.parse($('Read Config from Table').first().json.value);\nconst templatesCss = $('Read CSS from Table').first().json.value;\n\n// 4. PARSER MARKDOWN AVEC LE MEME COMPILATEUR QUE L'EDITEUR (MARKED.JS VIA CDN)\nlet compiledHtml;\ntry {\n const cdnUrl = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';\n const response = await fetch(cdnUrl);\n if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);\n const markedText = await response.text();\n const evalGlobal = new Function(markedText + '\\nreturn marked;');\n const marked = evalGlobal();\n \n marked.setOptions({\n gfm: true,\n breaks: true\n });\n \n let processedMd = mdText;\n processedMd = processedMd.replace(/:accent\\[([^\\]]+)\\]/g, '<span class=\"resume-accent\">$1</span>');\n processedMd = processedMd.replace(/:muted\\[([^\\]]+)\\]/g, '<span class=\"resume-muted\">$1</span>');\n \n compiledHtml = marked.parse(processedMd);\n \n // Post-process styling for paragraph tags with separators/bullets to justify\n compiledHtml = compiledHtml.replace(/<p>((?:(?!<\\/p>).)*(?:[\u2022\u00b7])(?:(?!<\\/p>).)*)<\\/p>/g, '<p style=\"text-align: justify; text-justify: inter-word;\">$1</p>');\n} catch (error) {\n console.warn('Fallback: Failed to load marked.js from CDN, using robustMarkdownToHtml', error);\n compiledHtml = robustMarkdownToHtml(mdText);\n compiledHtml = compiledHtml.replace(/:accent\\[([^\\]]+)\\]/g, '<span class=\"resume-accent\">$1</span>');\n compiledHtml = compiledHtml.replace(/:muted\\[([^\\]]+)\\]/g, '<span class=\"resume-muted\">$1</span>');\n}\n\n// Traitement du bloc contact si pr\u00e9sent\ncompiledHtml = compiledHtml.replace(/\\[CONTACT\\s*:\\s*([^\\]]+)\\]/gi, (match, contents) => {\n const parts = contents.split('|').map(p => p.trim());\n const formattedParts = parts.map(part => {\n if (part.includes('@') && !part.includes(' ')) {\n return `<a href=\"mailto:${part}\">${part}</a>`;\n }\n if (part.startsWith('http://') || part.startsWith('https://')) {\n const cleanUrl = part.replace(/^https?:\\/\\/(www\\.)?/, '');\n return `<a href=\"${part}\" target=\"_blank\">${cleanUrl}</a>`;\n }\n return `<span>${part}</span>`;\n });\n return `<div class=\"resume-contact-bar\">${formattedParts.join(' \u2022 ')}</div>`;\n});\n\n// 5. LAYOUT 2 COLUMNS RESTRUCTURING\nlet finalHtml = compiledHtml;\nif (config.layoutMode === '2-column') {\n const parts = compiledHtml.split(/(?=<h[23]\\b)/i);\n const headerHtml = parts[0];\n let mainHtml = '';\n let sidebarHtml = '';\n \n for (let i = 1; i < parts.length; i++) {\n const part = parts[i];\n if (part.toLowerCase().startsWith('<h2')) {\n mainHtml += part;\n } else if (part.toLowerCase().startsWith('<h3')) {\n sidebarHtml += part;\n }\n }\n \n finalHtml = `\n <div class=\"resume-header\">\n ${headerHtml}\n </div>\n <div class=\"resume-columns ${config.sidebarPosition === 'left' ? 'sidebar-left' : ''}\">\n <div class=\"resume-main-col\">\n ${mainHtml}\n </div>\n <div class=\"resume-sidebar-col\" style=\"background-color: ${config.sidebarBg}; color: ${config.sidebarText};\">\n ${sidebarHtml}\n </div>\n </div>\n `;\n}\n\n// 6. GENERATE INLINE CSS VARIABLES\nconst inlineVariables = `\n:root {\n --resume-font-family: ${config.fontFamily};\n --resume-font-size: ${config.fontSize}px;\n --resume-line-height: ${config.lineHeight};\n --resume-heading-scale: ${config.headingScale};\n --resume-margin-x: ${config.marginX}px;\n --resume-margin-y: ${config.marginY}px;\n --resume-section-spacing: ${config.sectionSpacing}px;\n --resume-color-bg: ${config.colorBg || '#ffffff'};\n --resume-color-headings: ${config.colorHeadings};\n --resume-color-body: ${config.colorBody};\n --resume-color-links: ${config.colorLinks};\n --resume-color-accent: ${config.colorAccent};\n --resume-sidebar-bg: ${config.sidebarBg || '#2d3748'};\n --resume-sidebar-text: ${config.sidebarText || '#ffffff'};\n}`;\n\n// 7. ASSEMBLE STANDALONE DOCUMENT\nconst standaloneHtml = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${finalFileName}</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,600;0,700;1,400&family=Raleway:wght@300;400;500;600;700;800&family=Merriweather:ital,wght@0,300;0,400;0,700;1,300&family=JetBrains+Mono:wght@400;500;700&family=Lora:ital,wght@0,400;0,500;0,600;1,400&display=swap\" rel=\"stylesheet\">\n <style>\n ${inlineVariables}\n ${templatesCss}\n @media print {\n body {\n display: block !important;\n width: 100% !important;\n height: auto !important;\n background: #ffffff !important;\n }\n .a4-sheet {\n width: 100% !important;\n margin: 0 !important;\n box-shadow: none !important;\n }\n }\n body {\n background-color: var(--resume-color-bg, #ffffff);\n margin: 0;\n padding: 0;\n display: flex;\n justify-content: center;\n }\n .a4-sheet {\n box-shadow: none !important;\n border-radius: 0 !important;\n margin: 0 auto;\n }\n </style>\n</head>\n<body>\n <article class=\"a4-sheet\" id=\"resume-output\">\n ${finalHtml}\n </article>\n</body>\n</html>`;\n\nreturn [\n {\n json: {\n compiledBody: standaloneHtml,\n pdfFileName: finalFileName,\n printBackground: \"true\",\n marginTop: \"0in\",\n marginBottom: \"0in\",\n marginLeft: \"0in\",\n marginRight: \"0in\"\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
240,
-80
],
"id": "ba4574e0-c63b-4d8c-b043-d21b29954aaf",
"name": "G\u00e9n\u00e9ration du HTML & Style CSS"
},
{
"parameters": {
"method": "POST",
"url": "={{ $node[\"Notion : Initialiser l'upload\"].json.upload_url }}",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "notionApi",
"sendQuery": true,
"queryParameters": {
"parameters": []
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Notion-Version",
"value": "2022-06-28"
}
]
},
"sendBody": true,
"contentType": "multipart-form-data",
"bodyParameters": {
"parameters": [
{
"parameterType": "formBinaryData",
"name": "file",
"inputDataFieldName": "={{ $node[\"(Gotemberg) PDF\"].binary.data }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1360,
-80
],
"id": "425d0195-9b44-4887-ad6c-a86a6af34806",
"name": "Notion : Envoyer le binaire",
"credentials": {
"notionApi": {
"id": "KYrsxF0plfLkPpqW",
"name": "Notion LinkedIn Auto"
}
}
},
{
"parameters": {
"method": "POST",
"url": "https://api.notion.com/v1/file_uploads",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "notionApi",
"sendQuery": true,
"queryParameters": {
"parameters": []
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Notion-Version",
"value": " 2022-06-28"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"mode\": \"single_part\",\n \"filename\": \"{{ $('Get PageID').item.json.outputPdf }}\",\n \"content_type\": \"application/pdf\"\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1136,
-80
],
"id": "85ce1de6-67c4-4e7b-be47-b4b4c3a58616",
"name": "Notion : Initialiser l'upload",
"credentials": {
"notionApi": {
"id": "KYrsxF0plfLkPpqW",
"name": "Notion LinkedIn Auto"
}
}
},
{
"parameters": {
"jsCode": "// 1. R\u00e9cup\u00e9ration des donn\u00e9es brutes du Webhook\nconst body = $('Webhook').item.json.body?.data || {};\nconst properties = body.properties || {};\n\nconst pageId = body.id;\n\n// 2. Extraction du texte MD de CV\nconst richTextArray = properties.CV?.rich_text || [];\nconst mdText = richTextArray.map(block => block.plain_text || '').join('');\n\n// 3. Extraction du nom ($name est la ligne apr\u00e8s le '#' ou sur la ligne du '#')\nlet name = 'resume';\nconst lines = mdText.split(/\\r?\\n/);\nfor (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i].trim();\n if (trimmed.startsWith('# ')) {\n const headingText = trimmed.substring(2).trim();\n if (headingText) {\n name = headingText;\n } else if (i + 1 < lines.length) {\n name = lines[i + 1].trim();\n }\n break;\n }\n}\n\n// 4. Fonction locale slugify pour nettoyer les cha\u00eenes\nfunction slugify(text) {\n return text\n .toString()\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .trim()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9\\-]/g, '');\n}\n\nconst nameSlug = slugify(name);\nconst today = new Date().toISOString().split('T')[0];\n\n// 5. Calcul de l'incr\u00e9ment quotidien en inspectant les PDF existants\nlet maxIncrement = 0;\nconst pdfFiles = properties.PDF?.files || [];\nconst prefixPattern = new RegExp(`^${nameSlug}-resume-${today}-(\\d+)\\.pdf$`, 'i');\n\nfor (const fileObj of pdfFiles) {\n const fileName = fileObj.name || '';\n const match = fileName.match(prefixPattern);\n if (match) {\n const inc = parseInt(match[1], 10);\n if (inc > maxIncrement) {\n maxIncrement = inc;\n }\n }\n}\n\nconst nextIncrement = String(maxIncrement + 1).padStart(2, '0');\nconst finalFileName = `${nameSlug}-resume-${today}-${nextIncrement}`;\nconst outputPdf = `${finalFileName}.pdf`;\n\n// 6. Extraction de Company et JobTitle pour r\u00e9trocompatibilit\u00e9 n8n\nlet company = 'company';\nif (properties.Company && properties.Company.select) {\n company = properties.Company.select.name || 'company';\n}\nlet jobTitle = 'job';\nconst targetProperty = properties.Title || properties[\"Job Title\"];\nif (targetProperty && targetProperty.title && targetProperty.title.length > 0) {\n jobTitle = targetProperty.title.plain_text || 'job';\n}\n\n// 7. Retour des variables\nreturn [{ \n json: {\n pageId: pageId,\n company: company,\n jobTitle: jobTitle,\n outputPdf: outputPdf,\n increment: nextIncrement\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-432,
-80
],
"id": "3ad811d9-5ff0-4e35-97c3-231e5d5b3553",
"name": "Get PageID"
},
{
"parameters": {
"method": "PATCH",
"url": "=https://api.notion.com/v1/pages/{{ $node[\"Get PageID\"].json.pageId }}",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "notionApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Notion-Version",
"value": "2026-03-11"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"properties\": {\n \"PDF\": {\n \"type\": \"files\",\n \"files\": [\n {\n \"type\": \"file_upload\",\n \"file_upload\": { \n \"id\": \"{{ $('Notion : Initialiser l\\'upload').item.json.id }}\" \n },\n \"name\": \"{{ $json.filename }}\"\n }\n ]\n }\n }\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1584,
-80
],
"id": "aee20dc4-18f3-4b2b-a575-1e9066f32baa",
"name": "Attach Bin to page",
"credentials": {
"notionApi": {
"id": "KYrsxF0plfLkPpqW",
"name": "Notion LinkedIn Auto"
}
}
},
{
"parameters": {
"method": "POST",
"url": "http://gotenberg:3000/forms/chromium/convert/html",
"sendBody": true,
"contentType": "multipart-form-data",
"bodyParameters": {
"parameters": [
{
"parameterType": "formBinaryData",
"name": "index.html",
"inputDataFieldName": "indexHtml"
},
{
"name": "printBackground",
"value": "true"
},
{
"name": "marginTop",
"value": "0in"
},
{
"name": "marginBottom",
"value": "0in"
},
{
"name": "marginLeft",
"value": "0in"
},
{
"name": "marginRight",
"value": "0in"
},
{
"name": "preferCssPageSize",
"value": "true"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
912,
-80
],
"id": "d28e993d-2353-418d-904b-cd4b6d1ac7d2",
"name": "(Gotemberg) PDF"
},
{
"parameters": {
"operation": "toText",
"sourceProperty": "myRawContent",
"binaryPropertyName": "indexHtml",
"options": {
"addBOM": false,
"encoding": "utf8",
"fileName": "=index.html"
}
},
"type": "n8n-nodes-base.convertToFile",
"typeVersion": 1.1,
"position": [
688,
-80
],
"id": "d43058ca-042b-4786-85ec-7551ff479f79",
"name": "Convert to File"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "1fb1391e-c916-48ff-ae50-b4da474a5ea1",
"name": "myRawContent",
"value": "={{ $json.compiledBody }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
464,
-80
],
"id": "ba499321-0a26-4a49-b389-3540fa4eed82",
"name": "Template Design CV"
},
{
"parameters": {
"operation": "get",
"dataTableId": {
"__rl": true,
"value": "OtWoNuYUmoj7knoz",
"mode": "id"
},
"matchType": "allConditions",
"filters": {
"conditions": [
{
"keyName": "key",
"keyValue": "config"
}
]
}
},
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1,
"position": [
-208,
-80
],
"id": "a4c9b321-0a26-4a49-b389-3540fa4eed10",
"name": "Read Config from Table"
},
{
"parameters": {
"operation": "get",
"dataTableId": {
"__rl": true,
"value": "OtWoNuYUmoj7knoz",
"mode": "id"
},
"matchType": "allConditions",
"filters": {
"conditions": [
{
"keyName": "key",
"keyValue": "css"
}
]
}
},
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1,
"position": [
16,
-80
],
"id": "ba499321-0a26-4a49-b389-3540fa4eed20",
"name": "Read CSS from Table"
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Get PageID",
"type": "main",
"index": 0
}
]
]
},
"G\u00e9n\u00e9ration du HTML & Style CSS": {
"main": [
[
{
"node": "Template Design CV",
"type": "main",
"index": 0
}
]
]
},
"Notion : Envoyer le binaire": {
"main": [
[
{
"node": "Attach Bin to page",
"type": "main",
"index": 0
}
]
]
},
"Notion : Initialiser l'upload": {
"main": [
[
{
"node": "Notion : Envoyer le binaire",
"type": "main",
"index": 0
}
]
]
},
"Get PageID": {
"main": [
[
{
"node": "Read Config from Table",
"type": "main",
"index": 0
}
]
]
},
"(Gotemberg) PDF": {
"main": [
[
{
"node": "Notion : Initialiser l'upload",
"type": "main",
"index": 0
}
]
]
},
"Convert to File": {
"main": [
[
{
"node": "(Gotemberg) PDF",
"type": "main",
"index": 0
}
]
]
},
"Template Design CV": {
"main": [
[
{
"node": "Convert to File",
"type": "main",
"index": 0
}
]
]
},
"Read Config from Table": {
"main": [
[
{
"node": "Read CSS from Table",
"type": "main",
"index": 0
}
]
]
},
"Read CSS from Table": {
"main": [
[
{
"node": "G\u00e9n\u00e9ration du HTML & Style CSS",
"type": "main",
"index": 0
}
]
]
}
},
"authors": "\u00c9ole Wind",
"name": null,
"description": null,
"autosaved": false,
"workflowPublishHistory": [
{
"createdAt": "2026-06-03T11:51:05.164Z",
"id": 210,
"workflowId": "dSX8vdBy3PnPdiOU",
"versionId": "a9ab848d-0d02-43a2-8728-ca1997482ade",
"event": "activated",
"userId": "2792484d-cba3-4156-adba-fbc49134eb55"
},
{
"createdAt": "2026-06-03T11:51:05.476Z",
"id": 212,
"workflowId": "dSX8vdBy3PnPdiOU",
"versionId": "a9ab848d-0d02-43a2-8728-ca1997482ade",
"event": "activated",
"userId": "2792484d-cba3-4156-adba-fbc49134eb55"
},
{
"createdAt": "2026-06-03T11:51:05.094Z",
"id": 209,
"workflowId": "dSX8vdBy3PnPdiOU",
"versionId": "a9ab848d-0d02-43a2-8728-ca1997482ade",
"event": "deactivated",
"userId": "2792484d-cba3-4156-adba-fbc49134eb55"
},
{
"createdAt": "2026-06-03T11:51:05.453Z",
"id": 211,
"workflowId": "dSX8vdBy3PnPdiOU",
"versionId": "a9ab848d-0d02-43a2-8728-ca1997482ade",
"event": "deactivated",
"userId": "2792484d-cba3-4156-adba-fbc49134eb55"
}
]
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
httpHeaderAuthnotionApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Jobby - PDF - dyn. Uses httpRequest, dataTable. Webhook trigger; 11 nodes.
Source: https://github.com/gnueole/jobby-md2html/blob/main/n8n/jobby-pdf-dyn.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Response_Handler. Uses httpRequest, dataTable, n8n-nodes-evolution-api, redis. Webhook trigger; 32 nodes.
Receive instant push notifications on your phone and voice announcements on your Google Home every time someone orders from your intranet menu — with cumulative BAC tracking per person.
Jobby - PDF - static. Uses httpRequest, dataTable. Webhook trigger; 10 nodes.
This n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di
This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .