8
n8n 한국어amn8n.com

Google Docs에 대한 키워드 기반 연구 지원형 SEO 콘텐츠 요약 생성

중급

이것은Miscellaneous, AI RAG, Multimodal AI분야의자동화 워크플로우로, 10개의 노드를 포함합니다.주로 Code, GoogleDocs, FormTrigger, HttpRequest, Agent 등의 노드를 사용하며. 사용OpenAI와Firecrawl로 연구 지원 SEO 콘텐츠 요약 보고서를 Google Docs에 생성

사전 요구사항
  • 대상 API의 인증 정보가 필요할 수 있음
  • OpenAI API Key
워크플로우 미리보기
노드 연결 관계를 시각적으로 표시하며, 확대/축소 및 이동을 지원합니다
워크플로우 내보내기
다음 JSON 구성을 복사하여 n8n에 가져오면 이 워크플로우를 사용할 수 있습니다
{
  "id": "qyhYS0xy7fCuyqLn",
  "meta": {
    "instanceId": "ec3694b688f9802e6ff1f61802ca6bc63a40ecfa76d32f2c71cfdf2978426fce",
    "templateCredsSetupCompleted": true
  },
  "name": "Create keyword-to-Google Docs research-backed SEO content brief",
  "tags": [],
  "nodes": [
    {
      "id": "d0cbb365-b3df-430e-89a6-338bc316315e",
      "name": "폼 제출 시",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        0,
        0
      ],
      "webhookId": "e561604e-f1f9-445d-b734-d268da983895",
      "parameters": {
        "options": {
          "appendAttribution": false,
          "respondWithOptions": {
            "values": {
              "redirectUrl": "={{google_drive_folder_url}}",
              "respondWith": "redirect"
            }
          }
        },
        "formTitle": "VertoDigital Keyword to SEO Content Brief",
        "formFields": {
          "values": [
            {
              "html": "<div style=\"text-align:left !important; font-family:Arial, Helvetica, sans-serif; font-size:16px;\"> <p style=\"text-align:left !important; margin:0 0 24px 0; line-height:1.9;\"> Enter the primary keyword or topic for the content brief. This should represent the main focus of the SEO-optimized content you want to create. Once completed, you'll be redirected to a Google Drive folder containing your generated SEO content brief. </p> \n<p style=\"text-align:left !important; margin:0 0 24px 0; line-height:1.9;\">Here is how the workflow works:</p>\n<ol style=\"text-align:left !important; margin:0; padding-left:1.5rem; line-height:1.9;\"> <li style=\"text-align:left !important; margin:0 0 18px 0;\">Firecrawl searches the web for your keyword and scrapes the top 5 relevant pages (main content only).</li> <li style=\"text-align:left !important; margin:0 0 18px 0;\">GPT AI Agent (with Think) analyzes the sources and generates the content brief in Markdown.</li> <li style=\"text-align:left !important; margin:0 0 18px 0;\">The Markdown brief is converted and formatted for Google Docs.</li> <li style=\"text-align:left !important; margin:0 0 18px 0;\">A Google Doc titled “SEO Brief for &lt;Keyword&gt;” is created and populated.</li> <li style=\"text-align:left !important; margin:0 0 18px 0;\">You are redirected to the Google Drive folder containing your SEO content brief.</li> </ol> </div>",
              "fieldType": "html",
              "elementName": "Description"
            },
            {
              "fieldLabel": "Keyword/Topic",
              "placeholder": "Write your keyword/topic here",
              "requiredField": true
            }
          ]
        },
        "responseMode": "lastNode"
      },
      "notesInFlow": false,
      "typeVersion": 2.2
    },
    {
      "id": "c3ac02d5-67c7-4f24-8ae1-0af6ba9f4d79",
      "name": "FireCrawl 검색 및 스크래핑",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        256,
        0
      ],
      "parameters": {
        "url": "https://api.firecrawl.dev/v2/search",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"query\": \"{{ $('On form submission').item.json['Keyword/Topic'] }}\",\n  \"sources\": [\n    \"web\"\n  ],\n  \"limit\": 5,\n  \"location\": \"United States\",\n  \"scrapeOptions\": {\n    \"onlyMainContent\": true,\n    \"maxAge\": 172800000,\n    \"parsers\": [\n      \"pdf\"\n    ],\n    \"formats\": [\n      \"markdown\",\n      \"links\"\n    ]\n  }\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer {{API_KEY}}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "8d7a216c-017e-49fa-a3dc-980aab03d32c",
      "name": "Markdown을 JSON로 변환",
      "type": "n8n-nodes-base.code",
      "position": [
        816,
        0
      ],
      "parameters": {
        "jsCode": "// Markdown → Google Docs batchUpdate (safe ranges, compact spacing, proper headings/lists/inline)\n// Input: items[0].json.output (Markdown)\n// Output: { json: { requests: [...] } }\n\nconst rawInput = items[0].json.output ?? \"\";\n\n/* =========================\n   Helpers & Normalization\n   ========================= */\nfunction hexToRgb(hex) {\n  if (!hex || !hex.startsWith('#')) return null;\n  const r = parseInt(hex.slice(1, 3), 16) / 255;\n  const g = parseInt(hex.slice(3, 5), 16) / 255;\n  const b = parseInt(hex.slice(5, 7), 16) / 255;\n  return { red: r, green: g, blue: b };\n}\n\nfunction headingStyleForLevel(level) {\n  const map = { 1: 'HEADING_1', 2: 'HEADING_2', 3: 'HEADING_3', 4: 'HEADING_4', 5: 'HEADING_5', 6: 'HEADING_6' };\n  return map[level] || 'HEADING_1';\n}\n\n// Collapse 3+ blank lines → 2; trim trailing spaces\nfunction normalizeMarkdown(md) {\n  return md\n    .replace(/\\r\\n/g, '\\n')\n    .replace(/[ \\t]+\\n/g, '\\n')\n    .replace(/\\n{3,}/g, '\\n\\n')\n    .replace(/[ \\t]+$/gm, '');\n}\n\n// Parse inline markdown for one line\n// Returns { text, spans[] } where spans: { offsetStart, offsetEnd, bold?, italic?, link? }\nfunction parseInline(line) {\n  let text = line;\n  const spans = [];\n\n  // Handle your previous LINK markers if present (safety)\n  // \\u0001LINKSTART\\u0001Label\\u0001LINKURL:https://...\\u0001LINKEND\\u0001\n  if (text.includes('\\u0001LINKSTART\\u0001')) {\n    let acc = '';\n    for (let i = 0; i < text.length; ) {\n      if (text.startsWith('\\u0001LINKSTART\\u0001', i)) {\n        i += '\\u0001LINKSTART\\u0001'.length;\n        const urlTag = '\\u0001LINKURL:';\n        const endTag = '\\u0001LINKEND\\u0001';\n        const urlIdx = text.indexOf(urlTag, i);\n        const endIdx = text.indexOf(endTag, i);\n        const label = text.slice(i, urlIdx);\n        const url = text.slice(urlIdx + urlTag.length, endIdx);\n        const start = acc.length;\n        acc += label;\n        spans.push({ offsetStart: start, offsetEnd: start + label.length, link: url });\n        i = endIdx + endTag.length;\n      } else {\n        acc += text[i++];\n      }\n    }\n    text = acc;\n  }\n\n  // Markdown links: [label](url)\n  text = text.replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^\\s)]+)\\)/g, (m, label, url) => {\n    const start = m.indexOf(label);\n    return `\\u0002L${label}\\u0002U${url}\\u0002E`;\n  });\n\n  // Bold+Italic ***text*** or ___text___\n  text = text.replace(/(\\*{3}|_{3})([\\s\\S]*?)\\1/g, (m, _w, inner) => `\\u0002BIS${inner}\\u0002BIE`);\n  // Bold **text** or __text__\n  text = text.replace(/(\\*{2}|__)([\\s\\S]*?)\\1/g, (m, _w, inner) => `\\u0002BS${inner}\\u0002BE`);\n  // Italic *text* or _text_\n  text = text.replace(/(\\*|_)([\\s\\S]*?)\\1/g, (m, _w, inner) => `\\u0002IS${inner}\\u0002IE`);\n\n  // Second pass: build clean text + spans\n  let clean = '';\n  for (let i = 0; i < text.length; ) {\n    if (text.startsWith('\\u0002L', i)) {\n      i += 2; // \\u0002 + 'L'\n      const uIdx = text.indexOf('\\u0002U', i);\n      const eIdx = text.indexOf('\\u0002E', i);\n      const label = text.slice(i, uIdx);\n      const url = text.slice(uIdx + 2, eIdx);\n      const start = clean.length;\n      clean += label;\n      spans.push({ offsetStart: start, offsetEnd: start + label.length, link: url });\n      i = eIdx + 2;\n      continue;\n    }\n    if (text.startsWith('\\u0002BIS', i)) {\n      i += 4;\n      const eIdx = text.indexOf('\\u0002BIE', i);\n      const inner = text.slice(i, eIdx);\n      const start = clean.length;\n      clean += inner;\n      spans.push({ offsetStart: start, offsetEnd: start + inner.length, bold: true, italic: true });\n      i = eIdx + 4;\n      continue;\n    }\n    if (text.startsWith('\\u0002BS', i)) {\n      i += 3;\n      const eIdx = text.indexOf('\\u0002BE', i);\n      const inner = text.slice(i, eIdx);\n      const start = clean.length;\n      clean += inner;\n      spans.push({ offsetStart: start, offsetEnd: start + inner.length, bold: true });\n      i = eIdx + 3;\n      continue;\n    }\n    if (text.startsWith('\\u0002IS', i)) {\n      i += 3;\n      const eIdx = text.indexOf('\\u0002IE', i);\n      const inner = text.slice(i, eIdx);\n      const start = clean.length;\n      clean += inner;\n      spans.push({ offsetStart: start, offsetEnd: start + inner.length, italic: true });\n      i = eIdx + 3;\n      continue;\n    }\n    clean += text[i++];\n  }\n  return { text: clean, spans };\n}\n\n/* =========================\n   Block-level parser\n   ========================= */\nfunction parseMarkdownToModel(md) {\n  const lines = normalizeMarkdown(md).split('\\n');\n\n  let docText = '';\n  const blocks = [];       // { start, end, kind: 'heading'|'paragraph'|'blockquote'|'hr'|'numbered'|'bulleted', level? }\n  const inlineSpans = [];  // { start, end, bold?, italic?, link? }\n  const listGroups = [];   // contiguous list ranges → { kind, start, end }\n  let cursor = 0;\n\n  function addListLine(kind, start, end) {\n    const last = listGroups[listGroups.length - 1];\n    if (last && last.kind === kind && last.end === start) last.end = end;\n    else listGroups.push({ kind, start, end });\n  }\n\n  for (let i = 0; i < lines.length; i++) {\n    const raw = lines[i];\n\n    // Horizontal rule\n    if (/^\\s*(?:-{3,}|_{3,}|\\*{3,})\\s*$/.test(raw)) {\n      const hr = '────────────────────────\\n';\n      const start = cursor;\n      docText += hr;\n      const end = cursor + hr.length;\n      blocks.push({ start, end, kind: 'hr' });\n      cursor = end;\n      continue;\n    }\n\n    // Headings\n    const h = raw.match(/^(\\s{0,3})(#{1,6})\\s+(.*)$/);\n    if (h) {\n      const level = h[2].length;\n      const { text, spans } = parseInline(h[3]);\n      const para = text + '\\n';\n      const start = cursor;\n      docText += para;\n      const end = cursor + para.length;\n      blocks.push({ start, end, kind: 'heading', level });\n      spans.forEach(sp => inlineSpans.push({ start: start + sp.offsetStart, end: start + sp.offsetEnd, bold: sp.bold, italic: sp.italic, link: sp.link }));\n      cursor = end;\n      continue;\n    }\n\n    // Blockquote\n    const bq = raw.match(/^\\s*>\\s?(.*)$/);\n    if (bq) {\n      const { text, spans } = parseInline(bq[1]);\n      const para = text + '\\n';\n      const start = cursor;\n      docText += para;\n      const end = cursor + para.length;\n      blocks.push({ start, end, kind: 'blockquote' });\n      spans.forEach(sp => inlineSpans.push({ start: start + sp.offsetStart, end: start + sp.offsetEnd, bold: sp.bold, italic: sp.italic, link: sp.link }));\n      cursor = end;\n      continue;\n    }\n\n    // Numbered list\n    const num = raw.match(/^\\s*\\d+\\.\\s+(.*)$/);\n    if (num) {\n      const { text, spans } = parseInline(num[1]);\n      const para = text + '\\n';\n      const start = cursor;\n      docText += para;\n      const end = cursor + para.length;\n      blocks.push({ start, end, kind: 'numbered' });\n      addListLine('numbered', start, end);\n      spans.forEach(sp => inlineSpans.push({ start: start + sp.offsetStart, end: start + sp.offsetEnd, bold: sp.bold, italic: sp.italic, link: sp.link }));\n      cursor = end;\n      continue;\n    }\n\n    // Bulleted list\n    const bul = raw.match(/^\\s*([-*•])\\s+(.*)$/);\n    if (bul) {\n      const { text, spans } = parseInline(bul[2]);\n      const para = text + '\\n';\n      const start = cursor;\n      docText += para;\n      const end = cursor + para.length;\n      blocks.push({ start, end, kind: 'bulleted' });\n      addListLine('bulleted', start, end);\n      spans.forEach(sp => inlineSpans.push({ start: start + sp.offsetStart, end: start + sp.offsetEnd, bold: sp.bold, italic: sp.italic, link: sp.link }));\n      cursor = end;\n      continue;\n    }\n\n    // Paragraph\n    {\n      const { text, spans } = parseInline(raw);\n      const para = text + '\\n';\n      const start = cursor;\n      docText += para;\n      const end = cursor + para.length;\n      blocks.push({ start, end, kind: 'paragraph' });\n      spans.forEach(sp => inlineSpans.push({ start: start + sp.offsetStart, end: start + sp.offsetEnd, bold: sp.bold, italic: sp.italic, link: sp.link }));\n      cursor = end;\n    }\n  }\n\n  return { docText, blocks, inlineSpans, listGroups };\n}\n\n/* =========================\n   Build requests (safe ranges)\n   ========================= */\nconst { docText, blocks, inlineSpans, listGroups } = parseMarkdownToModel(rawInput);\n\n// Insert at 1 to skip the first section break at 0\nconst baseOffset = 1;\nconst docLen = docText.length;\nconst bodyEndExclusive = baseOffset + docLen;\n\n// Safe range builder: clamp end < bodyEndExclusive (end is exclusive per API)\nfunction absRangeSafe(relStart, relEnd) {\n  const start = Math.max(baseOffset + relStart, baseOffset);\n  const maxEnd = Math.max(Math.min(baseOffset + relEnd, bodyEndExclusive - 1), start); // clamp exclusive\n  if (maxEnd <= start) return null; // skip zero/invalid\n  return { startIndex: start, endIndex: maxEnd };\n}\n\nconst requests = [];\n\n// 1) Insert full text\nrequests.push({ insertText: { location: { index: baseOffset }, text: docText } });\n\n// 2) Default text styling across the inserted range (clamped end)\nconst rAll = absRangeSafe(0, docLen);\nif (rAll) {\n  requests.push({\n    updateTextStyle: {\n      range: rAll,\n      textStyle: {\n        weightedFontFamily: { fontFamily: 'Sofia Sans' },\n        fontSize: { magnitude: 12.5, unit: 'PT' },\n        foregroundColor: { color: { rgbColor: hexToRgb('#0d1216') } }\n      },\n      fields: 'weightedFontFamily,fontSize,foregroundColor'\n    }\n  });\n}\n\n// 3) Headings & blockquotes (tight spacing to avoid big gaps)\nfor (const b of blocks) {\n  if (b.kind === 'heading') {\n    const rng = absRangeSafe(b.start, b.end);\n    if (!rng) continue;\n    requests.push({\n      updateParagraphStyle: {\n        range: rng,\n        paragraphStyle: {\n          namedStyleType: headingStyleForLevel(b.level),\n          alignment: 'START',\n          spaceAbove: { magnitude: 0, unit: 'PT' },\n          spaceBelow: { magnitude: 0, unit: 'PT' },\n          indentStart: { magnitude: 0, unit: 'PT' },\n          indentFirstLine: { magnitude: 0, unit: 'PT' }\n        },\n        fields: 'namedStyleType,alignment,spaceAbove,spaceBelow,indentStart,indentFirstLine'\n      }\n    });\n  } else if (b.kind === 'blockquote') {\n    const rng = absRangeSafe(b.start, b.end);\n    if (!rng) continue;\n    requests.push({\n      updateParagraphStyle: {\n        range: rng,\n        paragraphStyle: {\n          namedStyleType: 'NORMAL_TEXT',\n          alignment: 'START',\n          spaceAbove: { magnitude: 0, unit: 'PT' },\n          spaceBelow: { magnitude: 0, unit: 'PT' },\n          indentStart: { magnitude: 18, unit: 'PT' },\n          indentFirstLine: { magnitude: 18, unit: 'PT' }\n        },\n        fields: 'namedStyleType,alignment,spaceAbove,spaceBelow,indentStart,indentFirstLine'\n      }\n    });\n    requests.push({ updateTextStyle: { range: rng, textStyle: { italic: true }, fields: 'italic' } });\n  }\n}\n\n// 4) Lists (convert contiguous ranges to bullets; clamp ranges)\nfor (const g of listGroups) {\n  const rng = absRangeSafe(g.start, g.end);\n  if (!rng) continue;\n  requests.push({\n    createParagraphBullets: {\n      range: rng,\n      bulletPreset: g.kind === 'numbered' ? 'NUMBERED_DECIMAL_ALPHA_ROMAN' : 'BULLET_DISC_CIRCLE_SQUARE'\n    }\n  });\n}\n\n// 5) Inline styles (bold/italic/links)\nfor (const sp of inlineSpans) {\n  const rng = absRangeSafe(sp.start, sp.end);\n  if (!rng) continue;\n  const fields = [];\n  const textStyle = {};\n  if (sp.bold) { textStyle.bold = true; fields.push('bold'); }\n  if (sp.italic) { textStyle.italic = true; fields.push('italic'); }\n  if (sp.link) { textStyle.link = { url: sp.link }; fields.push('link'); }\n  if (!fields.length) continue;\n  requests.push({ updateTextStyle: { range: rng, textStyle, fields: fields.join(',') } });\n}\n\nreturn { json: { requests } };\n"
      },
      "typeVersion": 2
    },
    {
      "id": "320d3015-2fa6-4d54-b729-802025444f7e",
      "name": "문서 생성",
      "type": "n8n-nodes-base.googleDocs",
      "position": [
        1040,
        0
      ],
      "parameters": {
        "title": "=SEO Brief for {{ $('On form submission').item.json['Keyword/Topic'] }}",
        "driveId": "sharedWithMe",
        "folderId": "={{google_drive_folder_id}}"
      },
      "credentials": {
        "googleDocsOAuth2Api": {
          "id": "M5Z3DWLrDsl5ycru",
          "name": "Google Docs account"
        }
      },
      "executeOnce": true,
      "typeVersion": 2
    },
    {
      "id": "d4c538c3-d527-4c96-99f0-e06b236aee10",
      "name": "문서 업데이트",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        1264,
        0
      ],
      "parameters": {
        "url": "=https://docs.googleapis.com/v1/documents/{{ $('Create a document').item.json.id }}:batchUpdate",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify($('Markdown to JSON').item.json) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleDocsOAuth2Api"
      },
      "credentials": {
        "googleDocsOAuth2Api": {
          "id": "M5Z3DWLrDsl5ycru",
          "name": "Google Docs account"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "e46c8bdd-b125-4791-9b20-f09d0dea472d",
      "name": "AI 에이전트",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        496,
        0
      ],
      "parameters": {
        "text": "=Here is the topic for the blog post:\n\n<topic>\n{{ $('On form submission').item.json['Keyword/Topic'] }}\n</topic>\n\nBelow are the top-ranking pages and their content for the target keyword. For each source, you'll find the content and its corresponding URL:\n\n\n<source_content_1>\n{{ $json.data.web[0].markdown }}\n<source_content_1>\n\n<source_url_1>\n{{ $json.data.web[0].url }}\n<source_url_1>\n\n<source_content_2>\n{{ $json.data.web[1].markdown }}\n<source_content_2>\n\n<source_url_2>\n{{ $json.data.web[1].url }}\n<source_url_2>\n\n<source_content_3>\n{{ $json.data.web[2].markdown }}\n<source_content_3>\n\n<source_url_3>\n{{ $json.data.web[2].url }}\n<source_url_3>\n\n<source_content_4>\n{{ $json.data.web[3].markdown }}\n<source_content_4>\n\n<source_url_4>\n{{ $json.data.web[3].url }}\n<source_url_4>\n\n<source_content_5>\n{{ $json.data.web[4].markdown }}\n<source_content_5>\n\n<source_url_5>\n{{ $json.data.web[4].url }}\n<source_url_5>\n\nYour task is to analyze these sources and create a unique, comprehensive content brief that incorporates best practices and insights from top-ranking content. Focus on providing valuable insights and a fresh perspective on the given topic while incorporating SEO best practices.\n\nBefore creating the content brief, use think tool and make a strategy and analysis. In your analysis, consider the following:\n\n1. For each source:\n   - List 3-5 key points\n   - Note any unique angles or perspectives\n   - Identify potential content gaps\n\n2. Compare and contrast the sources:\n   - Identify common themes across all sources (list at least 3)\n   - Note any conflicting information or viewpoints (if any)\n   - Highlight unique elements in each source\n\n3. Brainstorm 5 unique angles or perspectives not covered in the existing sources. Number each idea as you list it.\n\n4. Determine 3-5 content gaps and areas for improvement or expansion across all sources\n\n5. Outline 5 key SEO strategies to be implemented\n\n6. Suggest 3 visual or interactive content ideas that could enhance the article\n\n7. List 10 questions that the content should aim to answer. Number each question as you list it.\n\n8. Propose an overall approach to make the content competitive with top-ranking pages, including 5 specific strategies and tactics\n\nAfter your analysis, develop the content brief using the following structure:\n\nContent Brief for {{ $('On form submission').item.json['Keyword/Topic'] }}\n(H1) [Optimized Title] (5-10 words)\n\nTarget Keywords: [List primary keywords]\nSecondary Keywords: [List secondary keywords]\n\nIntroduction Guidelines: [2-3 sentences outlining the introduction]\n\nContent Structure:\n[H2] [Section Title]\n- [H3] [Subsection Title]\n  [Content guidelines and key points to cover]\n- [H3] [Subsection Title]\n  [Content guidelines and key points to cover]\n\n[H2] [Section Title]\n- [H3] [Subsection Title]\n  [Content guidelines and key points to cover]\n- [H3] [Subsection Title]\n  [Content guidelines and key points to cover]\n\n[Continue with additional H2 and H3 sections as needed]\n\nSEO Recommendations:\n- Semantic keywords: [List relevant semantic keywords]\n- Word count: [Recommended word count range]\n- Technical requirements: [Specific technical SEO recommendations]\n\nTop-Ranking References: [List the provided URLs]\n\nAdditional Content Guidelines:\n- [List any additional guidelines or requirements]\n\nSource Overview:\n[Provide a 4-5 sentence overview summarizing key insights from all sources]\n\n\nThe final format should be only a markdown with the content brief.\n\nRequirements:\n1. Include all major aspects of the topic in your content structure.\n2. Suggest at least one to three visual representation of a key concept related to the topic.\n3. If relevant, compare the main topic to a related concept or model.\n4. Total word count should be based on best SEO practices and the content lenght - n words.\n5. Incorporate semantic keywords naturally throughout the content brief.\n6. Identify opportunities for featured snippets, internal linking, and FAQs where relevant.\n7. Include the provided source URLs in the \"Top-ranking references\" section of the SEO Recommendations.\n8. Provide a concise 5-10 sentence overview of all sources at the end of the brief.\n9. 9. Everything should be in markdown format. Never write ```markdown in the beginning and ```\\n at the end, just start with the content.\n\nRemember to create a unique and comprehensive brief that goes beyond summarizing the existing content. Your goal is to provide a strategy for creating content that can compete with and surpass the top-ranking pages. The content structure provided is an example; adapt it as necessary for the specific topic given.",
        "options": {
          "systemMessage": "You are an expert content strategist and SEO specialist tasked with creating a comprehensive content brief for a blog post. Your goal is to analyze top-ranking content on a given topic and create a detailed outline that will help produce high-quality, SEO-optimized content that can compete with and surpass the top-ranking pages."
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "ba5808b2-ce45-4c2e-8cd9-87dcd11f4772",
      "name": "사고",
      "type": "@n8n/n8n-nodes-langchain.toolThink",
      "position": [
        656,
        208
      ],
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "ee7a9941-823b-47e2-8196-51e1f19307a8",
      "name": "OpenAI 채팅 모델",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        496,
        208
      ],
      "parameters": {
        "model": "gpt-4.1-mini",
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "id": "SVy0dG502VH2c1j9",
          "name": "OpenAi account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "96050769-6142-44bb-9529-3079bd68d20e",
      "name": "스티커 노트",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        272,
        -832
      ],
      "parameters": {
        "width": 944,
        "height": 736,
        "content": "## 🟨 Overview — Create SEO content brief from keyword to Google Docs\n\n## Who’s it for\nContent/SEO teams who want a fast, consistent, research-driven brief for a copywriters from a single keyword—without manual review and analysis of the SERP (Google results).\n\n## How it works / What it does\n- **Form Trigger** collects the keyword/topic and redirects to Google Drive Folder after the final node.  \n- **FireCrawl Search & Scrape** pulls the top 5 pages for the chosen keyword.  \n- **AI Agent** (with **Think** + **OpenAI Chat Model**) analyzes sources and generates an **original Markdown** brief.  \n- **Markdown to JSON** converts the Markdown into Google Docs `batchUpdate` requests (H1/H2/H3, lists, links, spacing). Then this is used in **Update a document** for updating the empty doc.\n- **Create a document** + **Update a document** write a Google Doc titled **“SEO Brief for <keyword>”** and update the Google Doc in your target Drive folder.\n\n## How to set up\n1) Add credentials: **Firecrawl** (Authorization header), **OpenAI** (Chat), **Google Docs OAuth2**.  \n2) Replace placeholders: `{{API_KEY}}`, `{{google_drive_folder_id}}`, `{{google_drive_folder_url}}`.  \n3) Publish and open the Form URL to test.\n\n## Requirements\nFirecrawl API key • OpenAI API key • Google account with access to the target Drive folder.\n\n## Resources\nGoogle OAuth2 Credentials Setup - https://docs.n8n.io/integrations/builtin/credentials/google/oauth-generic/\nFirecrawl API key - https://take.ms/lGcUp\nOpenAI API key - https://docs.n8n.io/integrations/builtin/credentials/openai/\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "0fcc0b14-172e-4b1f-ba32-bccbd66fd9ed",
      "name": "스티커 노트1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -592,
        -64
      ],
      "parameters": {
        "width": 528,
        "height": 272,
        "content": "## 🗒️ Setup checklist (do these before first run)\n\n- **On form submission → Respond with redirect**: set `={{google_drive_folder_url}}`.  \n- **Create a document → folderId**: set `={{google_drive_folder_id}}`.  \n- **FireCrawl headers**: `Authorization: Bearer {{API_KEY}}`.  \n- **OpenAI Chat Model**: attach your OpenAI credential.  \n- **Google Docs OAuth2**: attach to both Docs nodes - Create a document + Update a document. \n- Test with a keyword (e.g., `GEO strategy`) and verify the created Doc & redirect."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "c1662ea4-210d-47f6-bf2f-fea975cacef1",
  "connections": {
    "ba5808b2-ce45-4c2e-8cd9-87dcd11f4772": {
      "ai_tool": [
        [
          {
            "node": "e46c8bdd-b125-4791-9b20-f09d0dea472d",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "e46c8bdd-b125-4791-9b20-f09d0dea472d": {
      "main": [
        [
          {
            "node": "8d7a216c-017e-49fa-a3dc-980aab03d32c",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "8d7a216c-017e-49fa-a3dc-980aab03d32c": {
      "main": [
        [
          {
            "node": "320d3015-2fa6-4d54-b729-802025444f7e",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "320d3015-2fa6-4d54-b729-802025444f7e": {
      "main": [
        [
          {
            "node": "d4c538c3-d527-4c96-99f0-e06b236aee10",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ee7a9941-823b-47e2-8196-51e1f19307a8": {
      "ai_languageModel": [
        [
          {
            "node": "e46c8bdd-b125-4791-9b20-f09d0dea472d",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "d0cbb365-b3df-430e-89a6-338bc316315e": {
      "main": [
        [
          {
            "node": "c3ac02d5-67c7-4f24-8ae1-0af6ba9f4d79",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "c3ac02d5-67c7-4f24-8ae1-0af6ba9f4d79": {
      "main": [
        [
          {
            "node": "e46c8bdd-b125-4791-9b20-f09d0dea472d",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
자주 묻는 질문

이 워크플로우를 어떻게 사용하나요?

위의 JSON 구성 코드를 복사하여 n8n 인스턴스에서 새 워크플로우를 생성하고 "JSON에서 가져오기"를 선택한 후, 구성을 붙여넣고 필요에 따라 인증 설정을 수정하세요.

이 워크플로우는 어떤 시나리오에 적합한가요?

중급 - 기타, AI RAG, 멀티모달 AI

유료인가요?

이 워크플로우는 완전히 무료이며 직접 가져와 사용할 수 있습니다. 다만, 워크플로우에서 사용하는 타사 서비스(예: OpenAI API)는 사용자 직접 비용을 지불해야 할 수 있습니다.

워크플로우 정보
난이도
중급
노드 수10
카테고리3
노드 유형8
난이도 설명

일정 경험을 가진 사용자를 위한 6-15개 노드의 중간 복잡도 워크플로우

저자
Simeon Penev

Simeon Penev

@monkatagpt

AI-Powered Automation & Analytics I specialize in building AI workflows that streamline processes and reduce manual work. My main workflows are in the marketing field, but I can automate all kind of processes and integrate AI in them.

외부 링크
n8n.io에서 보기

이 워크플로우 공유

카테고리

카테고리: 34