使用AI格式化将Markdown内容转换为Contentful富文本
中级
这是一个Engineering, AI领域的自动化工作流,包含 12 个节点。主要使用 Code, Merge, HttpRequest, Agent, LmChatOpenAi 等节点,结合人工智能技术实现智能自动化。 使用AI格式化将Markdown内容转换为Contentful富文本
前置要求
- •可能需要目标 API 的认证凭证
- •OpenAI API Key
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
"id": "uIREtTV8TRuF3lru",
"meta": {
"instanceId": "6bcff5ef8a06e8086902526a05c2a4c7bf5da8101f89e582301ed78094606f40"
},
"name": "使用 AI 格式化将 Markdown 内容转换为 Contentful 富文本",
"tags": [
{
"id": "NEDxtCyOlsLSVNBI",
"name": "Formatter",
"createdAt": "2025-05-17T10:41:27.012Z",
"updatedAt": "2025-05-17T10:41:27.012Z"
},
{
"id": "B6FHhLBUZRavCehy",
"name": "Content Creation",
"createdAt": "2025-05-17T10:41:40.276Z",
"updatedAt": "2025-05-17T10:41:40.276Z"
}
],
"nodes": [
{
"id": "f50b2f81-d9b4-4206-a18c-a02573afe8e7",
"name": "创建新格式化的 Contentful 条目",
"type": "n8n-nodes-base.httpRequest",
"position": [
1680,
240
],
"parameters": {
"url": "=https://api.contentful.com/spaces/{INSERT_YOUR_SPACE}/environments/master/entries",
"method": "POST",
"options": {},
"jsonBody": "={{ $json }}",
"sendBody": true,
"jsonHeaders": "{\n \"Authorization\": \"Bearer {INSERT TOKEN HERE}\",\n \"Content-Type\": \"application/vnd.contentful.management.v1+json\",\n \"X-Contentful-Version\": \"2\", \n \"X-Contentful-Content-Type\": \"knowledgeBaseArticle\"\n}",
"sendHeaders": true,
"specifyBody": "json",
"specifyHeaders": "json"
},
"typeVersion": 4.2
},
{
"id": "d2d8efbc-3914-4c9b-98fe-566c1843f58d",
"name": "当被其他工作流执行时",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"position": [
-380,
20
],
"parameters": {
"inputSource": "jsonExample",
"jsonExample": "{\n \"title\": \"...\",\n \"slug\": \"...\",\n \"category\": {\n \"id\": \"...\"\n },\n \"description\": \"...\",\n \"keywords\": [\"keyword1\", \"keyword2\"],\n \"content\": \"...\",\n \"metaTitle\": \"...\",\n \"metaDescription\": \"...\",\n \"readingTime\": \"...\",\n \"difficulty\": \"...\"\n}"
},
"typeVersion": 1.1
},
{
"id": "57960c96-94ce-43d1-83b8-d6417b79373a",
"name": "合并1",
"type": "n8n-nodes-base.merge",
"position": [
1020,
240
],
"parameters": {},
"typeVersion": 3
},
{
"id": "f304bcec-2150-4767-95a9-38c1e98e2c52",
"name": "格式化1",
"type": "n8n-nodes-base.code",
"position": [
1360,
240
],
"parameters": {
"jsCode": "// Get all items passed into this node as an array\nconst items = $input.all();\n\n\n// If you always have at least two items:\nconst firstItem = items[0].json;\nconst secondItem = items[1].json;\n\n// Overwrite the first item’s “content” with the second item’s “content”\nfirstItem.content = secondItem.content;\n\nreturn [\n {\n json: {\n fields: {\n title: { \"en-US\": firstItem.title },\n slug: { \"en-US\": firstItem.slug },\n category: {\n \"en-US\": {\n sys: {\n type: \"Link\",\n linkType: \"Entry\",\n id: firstItem.category.id\n }\n }\n },\n description: { \"en-US\": firstItem.description },\n keywords: { \"en-US\": firstItem.keywords },\n content: {\n \"en-US\": {\n nodeType: \"document\",\n data: {},\n content: firstItem.content\n }\n },\n metaTitle: { \"en-US\": firstItem.metaTitle },\n metaDescription: { \"en-US\": firstItem.metaDescription },\n readingTime: { \"en-US\": firstItem.readingTime },\n difficulty: { \"en-US\": firstItem.difficulty }\n }\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "85edaba2-c42b-4ca8-af84-080255dd93d3",
"name": "Markdown 转 Contentful 格式",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
340,
380
],
"parameters": {
"text": "=Here is the markdown content to convert:\n\n{{ $json.contentChunk }}",
"options": {
"systemMessage": "=You are a npm package which takes markdown and converts it to valid Contentful Rich Text\n\nHere are some examples of input to output:\n\nAdditional Rules to Avoid Validation Errors:\n\nEnsure every single node, especially heading and paragraph types, includes the \"data\": {} property explicitly. Do NOT omit \"data\" even if it is empty.\n\nValidate that each text, inline, or block element consistently adheres to this structure:\n\n{\n \"nodeType\": \"<type>\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"<childType>\",\n \"value\": \"<textValue>\",\n \"marks\": [],\n \"data\": {}\n }\n ]\n}\nFor elements like text nodes, always explicitly include \"data\": {} alongside \"marks\": [] even if they are empty arrays or objects.\n\nDo not output nodes without the complete structure specified above, as this can cause validation failures in Contentful.\n\nConfirm explicitly that \"content\": [] is included, even if empty, for nodes that require it (e.g., embedded-asset-block, hr, etc.).\n\nNever output incomplete nodes missing \"data\" or \"content\".\n\nExample of Correct Minimal Node Structure:\n\n{\n \"nodeType\": \"paragraph\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"text\",\n \"value\": \"Your text here.\",\n \"marks\": [],\n \"data\": {}\n }\n ]\n}\n\nAlways strictly follow this guidance to ensure successful content publishing and avoid repeated validation errors.\n\n1. Paragraphs\n\nMarkdown:\n\nThis is a paragraph of text.\n\nContentful Rich Text JSON:\n\n{\n \"nodeType\": \"document\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"paragraph\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"text\",\n \"value\": \"This is a paragraph of text.\",\n \"marks\": [],\n \"data\": {}\n }\n ]\n }\n ]\n}\n2. Headings\n\nMarkdown:\n\n# Heading Level 1\n## Heading Level 2\n### Heading Level 3\n\nContentful Rich Text JSON:\n\n{\n \"nodeType\": \"document\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"heading-1\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"text\",\n \"value\": \"Heading Level 1\",\n \"marks\": [],\n \"data\": {}\n }\n ]\n },\n {\n \"nodeType\": \"heading-2\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"text\",\n \"value\": \"Heading Level 2\",\n \"marks\": [],\n \"data\": {}\n }\n ]\n },\n {\n \"nodeType\": \"heading-3\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"text\",\n \"value\": \"Heading Level 3\",\n \"marks\": [],\n \"data\": {}\n }\n ]\n }\n ]\n}\n\n3. Bold and Italic Text\n\nMarkdown:\n\n**Bold Text**\n*Italic Text*\n\nContentful Rich Text JSON:\n\n{\n \"nodeType\": \"document\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"paragraph\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"text\",\n \"value\": \"Bold Text\",\n \"marks\": [\n {\n \"type\": \"bold\"\n }\n ],\n \"data\": {}\n }\n ]\n },\n {\n \"nodeType\": \"paragraph\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"text\",\n \"value\": \"Italic Text\",\n \"marks\": [\n {\n \"type\": \"italic\"\n }\n ],\n \"data\": {}\n }\n ]\n }\n ]\n}\n4. Lists\n\nMarkdown:\n\n- Unordered Item 1\n- Unordered Item 2\n\n1. Ordered Item 1\n2. Ordered Item 2\n\nContentful Rich Text JSON:\n\n{\n \"nodeType\": \"document\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"unordered-list\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"list-item\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"paragraph\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"text\",\n \"value\": \"Unordered Item 1\",\n \"marks\": [],\n \"data\": {}\n }\n ]\n }\n ]\n },\n {\n \"nodeType\": \"list-item\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"paragraph\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"text\",\n \"value\": \"Unordered Item 2\",\n \"marks\": [],\n \"data\": {}\n }\n ]\n }\n ]\n }\n ]\n },\n {\n \"nodeType\": \"ordered-list\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"list-item\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"paragraph\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"text\",\n \"value\": \"Ordered Item 1\",\n \"marks\": [],\n \"data\": {}\n }\n ]\n }\n ]\n },\n {\n \"nodeType\": \"list-item\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"paragraph\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"text\",\n \"value\": \"Ordered Item 2\",\n \"marks\": [],\n \"data\": {}\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n}\n5. Links\n\nMarkdown:\n\n[Contentful](https://www.contentful.com)\n\nContentful Rich Text JSON:\n\n{\n \"nodeType\": \"document\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"paragraph\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"hyperlink\",\n \"data\": {\n \"uri\": \"https://www.contentful.com\"\n },\n \"content\": [\n {\n \"nodeType\": \"text\",\n \"value\": \"Contentful\",\n \"marks\": [],\n \"data\": {}\n }\n ]\n }\n ]\n }\n ]\n}\n6. Images\n\nMarkdown:\n\n\n\nContentful Rich Text JSON:\n\n{\n \"nodeType\": \"document\",\n \"data\": {},\n \"content\": [\n {\n \"nodeType\": \"embedded-asset-block\",\n \"data\": {\n \"target\": {\n \"sys\": {\n \"id\": \"asset-id\",\n \"type\": \"Link\",\n \"linkType\": \"Asset\"\n }\n }\n },\n \"content\": []\n }\n ]\n}\n\nRules:\n- The output must be valid Rich Text based on the specifications\n- Do not make up any new types, only use the information that is available\n- Do not output anything else besides the Contetnful RIch Text JSON in the format specified above\n- Do not include ```json before the output. The output needs to be parseable JSON\n- You must include a node to account for every single work in the input. DO NOT exclude any data from the input\n\nHere are all the possible types:\n\nimport { BLOCKS } from './blocks';\nimport { INLINES } from './inlines';\nimport { Block, Inline, ListItemBlock, Text } from './types';\n\n// eslint-disable-next-line @typescript-eslint/ban-types\ntype EmptyNodeData = {};\n// BLOCKS\n\n// Heading\nexport interface Heading1 extends Block {\n nodeType: BLOCKS.HEADING_1;\n data: EmptyNodeData;\n content: Array<Inline | Text>;\n}\n\nexport interface Heading2 extends Block {\n nodeType: BLOCKS.HEADING_2;\n data: EmptyNodeData;\n content: Array<Inline | Text>;\n}\n\nexport interface Heading3 extends Block {\n nodeType: BLOCKS.HEADING_3;\n data: EmptyNodeData;\n content: Array<Inline | Text>;\n}\n\nexport interface Heading4 extends Block {\n nodeType: BLOCKS.HEADING_4;\n data: EmptyNodeData;\n content: Array<Inline | Text>;\n}\n\nexport interface Heading5 extends Block {\n nodeType: BLOCKS.HEADING_5;\n data: EmptyNodeData;\n content: Array<Inline | Text>;\n}\n\nexport interface Heading6 extends Block {\n nodeType: BLOCKS.HEADING_6;\n data: EmptyNodeData;\n content: Array<Inline | Text>;\n}\n\n// Paragraph\nexport interface Paragraph extends Block {\n nodeType: BLOCKS.PARAGRAPH;\n data: EmptyNodeData;\n content: Array<Inline | Text>;\n}\n\n// Quote\nexport interface Quote extends Block {\n nodeType: BLOCKS.QUOTE;\n data: EmptyNodeData;\n content: Paragraph[];\n}\n// Horizontal rule\nexport interface Hr extends Block {\n nodeType: BLOCKS.HR;\n /**\n *\n * @maxItems 0\n */\n data: EmptyNodeData;\n content: Array<Inline | Text>;\n}\n\n// OL\nexport interface OrderedList extends Block {\n nodeType: BLOCKS.OL_LIST;\n data: EmptyNodeData;\n content: ListItem[];\n}\n// UL\nexport interface UnorderedList extends Block {\n nodeType: BLOCKS.UL_LIST;\n data: EmptyNodeData;\n content: ListItem[];\n}\n\nexport interface ListItem extends Block {\n nodeType: BLOCKS.LIST_ITEM;\n data: EmptyNodeData;\n content: ListItemBlock[];\n}\n\n// taken from graphql schema-generator/contentful-types/link.ts\nexport interface Link<T extends string = string> {\n sys: {\n type: 'Link';\n linkType: T;\n id: string;\n };\n}\n\nexport interface ResourceLink {\n sys: {\n type: 'ResourceLink';\n linkType: 'Contentful:Entry';\n urn: string;\n };\n}\n\nexport interface EntryLinkBlock extends Block {\n nodeType: BLOCKS.EMBEDDED_ENTRY;\n data: {\n target: Link<'Entry'>;\n };\n /**\n *\n * @maxItems 0\n */\n content: Array<Inline | Text>;\n}\n\nexport interface AssetLinkBlock extends Block {\n nodeType: BLOCKS.EMBEDDED_ASSET;\n data: {\n target: Link<'Asset'>;\n };\n /**\n *\n * @maxItems 0\n */\n content: Array<Inline | Text>;\n}\n\nexport interface ResourceLinkBlock extends Block {\n nodeType: BLOCKS.EMBEDDED_RESOURCE;\n data: {\n target: ResourceLink;\n };\n /**\n *\n * @maxItems 0\n */\n content: Array<Inline | Text>;\n}\n\n// INLINE\n\nexport interface EntryLinkInline extends Inline {\n nodeType: INLINES.EMBEDDED_ENTRY;\n data: {\n target: Link<'Entry'>;\n };\n /**\n *\n * @maxItems 0\n */\n content: Text[];\n}\n\nexport interface ResourceLinkInline extends Inline {\n nodeType: INLINES.EMBEDDED_RESOURCE;\n data: {\n target: ResourceLink;\n };\n /**\n *\n * @maxItems 0\n */\n content: Text[];\n}\n\nexport interface Hyperlink extends Inline {\n nodeType: INLINES.HYPERLINK;\n data: {\n uri: string;\n };\n content: Text[];\n}\n\nexport interface AssetHyperlink extends Inline {\n nodeType: INLINES.ASSET_HYPERLINK;\n data: {\n target: Link<'Asset'>;\n };\n content: Text[];\n}\n\nexport interface EntryHyperlink extends Inline {\n nodeType: INLINES.ENTRY_HYPERLINK;\n data: {\n target: Link<'Entry'>;\n };\n content: Text[];\n}\n\nexport interface ResourceHyperlink extends Inline {\n nodeType: INLINES.RESOURCE_HYPERLINK;\n data: {\n target: ResourceLink;\n };\n content: Text[];\n}\n\nexport interface TableCell extends Block {\n nodeType: BLOCKS.TABLE_HEADER_CELL | BLOCKS.TABLE_CELL;\n data: {\n colspan?: number;\n rowspan?: number;\n };\n\n /**\n * @minItems 1\n */\n content: Paragraph[];\n}\n\nexport interface TableHeaderCell extends TableCell {\n nodeType: BLOCKS.TABLE_HEADER_CELL;\n}\n\n// An abstract table row can have both table cell types\n\nexport interface TableRow extends Block {\n nodeType: BLOCKS.TABLE_ROW;\n data: EmptyNodeData;\n\n /**\n * @minItems 1\n */\n content: TableCell[];\n}\n\nexport interface Table extends Block {\n nodeType: BLOCKS.TABLE;\n data: EmptyNodeData;\n\n /**\n * @minItems 1\n */\n content: TableRow[];\n}"
},
"promptType": "define"
},
"typeVersion": 1.8
},
{
"id": "3b409954-1506-46da-8ea9-ed2c938b84fc",
"name": "按标题分割",
"type": "n8n-nodes-base.code",
"position": [
0,
220
],
"parameters": {
"jsCode": "// Split the markdown into sections using ## as the logical chunk point\nconst splitByHeading = (markdown, headingLevel = 3) => {\n const headingRegex = new RegExp(`(?=^${'#'.repeat(headingLevel)}\\\\s)`, 'gm');\n return markdown.split(headingRegex).filter(chunk => chunk.trim());\n};\n\n// Get the first input item's content\nconst input = items[0].json;\nconst chunks = splitByHeading(input.content);\n\nreturn chunks.map((chunk, index) => {\n return {\n json: {\n index,\n slug: input.slug?.trim(),\n title: input.title?.trim(),\n contentChunk: chunk.trim(),\n }\n };\n});\n"
},
"typeVersion": 2
},
{
"id": "d9bd0b06-730b-4e63-a3f4-73678b09aaa7",
"name": "合并富文本对象",
"type": "n8n-nodes-base.code",
"position": [
720,
380
],
"parameters": {
"jsCode": "const combinedContent = [];\n\nfor (const item of items) {\n let raw = item.json;\n\n // Handle if AI agent returned a stringified JSON inside a property like `output`\n if (typeof raw === 'object' && typeof raw.output === 'string') {\n try {\n raw = JSON.parse(raw.output);\n } catch (e) {\n throw new Error(`Failed to parse JSON from agent output:\\n${raw.output}`);\n }\n }\n\n // Handle if AI agent returned plain stringified JSON (not wrapped in a key)\n if (typeof raw === 'string') {\n try {\n raw = JSON.parse(raw);\n } catch (e) {\n throw new Error(`Failed to parse raw JSON string:\\n${raw}`);\n }\n }\n\n // Add content from parsed result\n if (raw?.content?.length) {\n combinedContent.push(...raw.content);\n }\n}\n\nreturn [\n {\n json: {\n nodeType: 'document',\n data: {},\n content: combinedContent\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "08f5c7a8-3200-47bf-a4c9-5333956ddf49",
"name": "OpenAI 聊天模型2",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
300,
620
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1",
"cachedResultName": "gpt-4.1"
},
"options": {}
},
"credentials": {
"openAiApi": {
"id": "KLN8ZfDzv8mW6pyu",
"name": "OpenAi account"
}
},
"typeVersion": 1.2
},
{
"id": "e1bbe1ba-3be3-44c9-af34-e27c56246545",
"name": "便签",
"type": "n8n-nodes-base.stickyNote",
"position": [
200,
80
],
"parameters": {
"width": 880,
"height": 760,
"content": "# 转换为 Contentful 专有格式"
},
"typeVersion": 1
},
{
"id": "36266f04-12cf-4a19-8d60-45974e69ab6b",
"name": "便签1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1180,
-340
],
"parameters": {
"color": 3,
"width": 980,
"height": 860,
"content": "# 发布到 Contentful API"
},
"typeVersion": 1
},
{
"id": "f0ecec88-c4a7-472f-b71b-d78f57635472",
"name": "便签2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-800,
-520
],
"parameters": {
"color": 5,
"width": 900,
"height": 620,
"content": "# Varritech 免费 n8n 工作流"
},
"typeVersion": 1
},
{
"id": "e4bf6af7-cd5a-4f54-9512-5e91d86f6bf6",
"name": "便签3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1180,
620
],
"parameters": {
"color": 4,
"width": 980,
"height": 540,
"content": "# 需要关于 Contentful 的额外帮助吗?"
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"callerPolicy": "workflowsFromSameOwner",
"executionOrder": "v1",
"executionTimeout": 600
},
"versionId": "c049c9ae-5037-4f0e-b499-65b5315529b3",
"connections": {
"Merge1": {
"main": [
[
{
"node": "Format1",
"type": "main",
"index": 0
}
]
]
},
"Format1": {
"main": [
[
{
"node": "Create newly formatted Contentful Entry",
"type": "main",
"index": 0
}
]
]
},
"Split by Headings": {
"main": [
[
{
"node": "Markdown to Contentful format",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Chat Model2": {
"ai_languageModel": [
[
{
"node": "Markdown to Contentful format",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Combine Rich Text Objects": {
"main": [
[
{
"node": "Merge1",
"type": "main",
"index": 1
}
]
]
},
"Markdown to Contentful format": {
"main": [
[
{
"node": "Combine Rich Text Objects",
"type": "main",
"index": 0
}
]
]
},
"When Executed by Another Workflow": {
"main": [
[
{
"node": "Split by Headings",
"type": "main",
"index": 0
},
{
"node": "Merge1",
"type": "main",
"index": 0
}
]
]
}
}
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
中级 - 工程, 人工智能
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
基于 Github 的自动单元测试生成器
为 GitHub PR 自动生成 Jest 测试,支持双 AI 审查
Code
Merge
Github
+7
24 节点Cristiano Varriale
工程
使用GPT和Perplexity AI为Contentful CMS生成知识库文章
使用GPT和Perplexity AI为Contentful CMS生成知识库文章
If
Set
Code
+10
23 节点Cristiano Varriale
客户支持
VDS工作流公开版
使用AI、GitHub和Vercel从文本提示构建和部署MVP
If
Code
Wait
+15
54 节点Varritech
开发运维
AI智能助手与Airtable对话及数据分析
AI智能助手与Airtable对话及数据分析
If
Set
Merge
+12
41 节点Mark Shcherbakov
工程
使用OpenAI和RAGAS方法评估AI代理响应正确性
使用OpenAI和RAGAS方法评估AI代理响应正确性
Set
Code
Merge
+12
27 节点Jimleuk
工程
评估指标:答案相似度
评估指标:答案相似度
Set
Code
Merge
+10
21 节点Jimleuk
工程