8
n8n 한국어amn8n.com

Firecrawl과 Google Sheets를 사용하여 웹사이트 맵과 시각적 트리 그래프 생성

중급

이것은Market Research, Multimodal AI분야의자동화 워크플로우로, 8개의 노드를 포함합니다.주로 If, Code, GoogleDrive, GoogleSheets, RespondToWebhook 등의 노드를 사용하며. Firecrawl과 Google Sheets를 사용하여 웹사이트 맵과 시각적 트리 구조도 생성

사전 요구사항
  • Google Drive API 인증 정보
  • Google Sheets API 인증 정보
  • HTTP Webhook 엔드포인트(n8n이 자동으로 생성)
워크플로우 미리보기
노드 연결 관계를 시각적으로 표시하며, 확대/축소 및 이동을 지원합니다
워크플로우 내보내기
다음 JSON 구성을 복사하여 n8n에 가져오면 이 워크플로우를 사용할 수 있습니다
{
  "meta": {
    "instanceId": "393ca9e36a1f81b0f643c72792946a5fe5e49eb4864181ba4032e5a408278263",
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "f6f18549-b9e2-4ea5-b0ad-9a4a4df3bff1",
      "name": "채팅 메시지 수신 시",
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "position": [
        0,
        0
      ],
      "webhookId": "9a4aeebc-9dd5-4248-8349-ebaf7e9bd7ce",
      "parameters": {
        "mode": "webhook",
        "public": true,
        "options": {
          "responseMode": "responseNode"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "3ab02f4d-4593-4d32-8007-f657e7706f84",
      "name": "Firecrawl OK",
      "type": "n8n-nodes-base.if",
      "position": [
        480,
        0
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "d1e1025f-704e-4392-bf2b-5be624a9c3a2",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.success }}",
              "rightValue": "true"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "1f44edba-d802-4e48-a193-9aa073971724",
      "name": "템플릿 복사",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        768,
        0
      ],
      "parameters": {
        "name": "={{ $('When chat message received').item.json.chatInput }} - n8n - Arborescence",
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "12lV4HwgudgzPPGXKNesIEExbFg09Tuu9gyC_jSS1HjI"
        },
        "options": {},
        "operation": "copy"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "3TalAPza9NdMx3yx",
          "name": "Google Drive account"
        }
      },
      "executeOnce": true,
      "typeVersion": 3
    },
    {
      "id": "9e188a8a-4faa-488d-ba3e-10e25fb94c05",
      "name": "데이터 매핑",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1408,
        0
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "Niv 0",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Niv 0",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Niv 1",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Niv 1",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Niv 2",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Niv 2",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Niv 3",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Niv 3",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Niv 4",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Niv 4",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Niv 5",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Niv 5",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "error",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "error",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "message",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "message",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "FR"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Copy template').item.json.id }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "wBRLUCktxqXE6DVJ",
          "name": "Google Sheets account"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "90c7df17-d3ad-434b-b6ec-23c7e64888de",
      "name": "URL을 테이블로 정렬",
      "type": "n8n-nodes-base.code",
      "position": [
        1120,
        0
      ],
      "parameters": {
        "jsCode": "/**\n * Fonction pour traiter les URLs collectées par Firecrawl et générer une arborescence de site\n * en traitant séparément les différents domaines et sous-domaines\n * \n * @param {Object} inputData - Les données brutes de l'appel Firecrawl\n * @returns {Array} - Tableau d'objets avec les colonnes pour Google Sheets\n */\nfunction createSiteHierarchy(inputData) {\n  // Vérifier que les données d'entrée sont valides\n  if (!inputData || !inputData.success || !Array.isArray(inputData.links) || inputData.links.length === 0) {\n    throw new Error(\"Données d'entrée invalides ou vides\");\n  }\n\n  // Normaliser toutes les URLs (convertir http en https)\n  const urls = inputData.links.map(url => {\n    if (url.startsWith('http://')) {\n      return 'https://' + url.substring(7);\n    }\n    return url;\n  });\n\n  // Extraire les différents domaines/sous-domaines présents dans les URLs\n  const domainPattern = /^https?:\\/\\/([^\\/]+)/;\n  const domains = {};\n  \n  // Regrouper les URLs par domaine/sous-domaine\n  for (const url of urls) {\n    const match = url.match(domainPattern);\n    if (!match) continue;\n    \n    const fullDomain = match[1]; // ex: www.zest.fr, wiki.zest.fr\n    \n    // Extraire le sous-domaine et le domaine de base\n    const domainParts = fullDomain.split('.');\n    const isSubdomain = domainParts.length > 2;\n    \n    // Déterminer le domaine principal\n    let mainDomain;\n    if (isSubdomain) {\n      // Pour les sous-domaines comme wiki.zest.fr\n      mainDomain = domainParts.slice(domainParts.length - 2).join('.');\n    } else {\n      // Pour les domaines principaux comme zest.fr\n      mainDomain = fullDomain;\n    }\n    \n    // Enregistrer cette URL dans son groupe de domaine\n    if (!domains[fullDomain]) {\n      domains[fullDomain] = {\n        mainDomain: mainDomain,\n        fullDomain: fullDomain,\n        baseUrl: `https://${fullDomain}`,\n        urls: []\n      };\n    }\n    \n    domains[fullDomain].urls.push(url);\n  }\n  \n  // Traiter chaque domaine/sous-domaine séparément\n  const results = [];\n  \n  // Fonction pour formater le texte d'affichage d'une URL\n  function formatDisplayText(segment) {\n    if (!segment) return \"HOME PAGE\";\n    // Décodage des caractères URL (comme %20, %C3%A9, etc.)\n    try {\n      const decoded = decodeURIComponent(segment);\n      return decoded.toUpperCase().replace(/-/g, ' ');\n    } catch (e) {\n      // En cas d'erreur de décodage, utiliser le segment tel quel\n      return segment.toUpperCase().replace(/-/g, ' ');\n    }\n  }\n  \n  // Fonction pour extraire le chemin relatif d'une URL\n  function getPathFromUrl(url, baseUrl) {\n    // Supprimer le domaine\n    let path = url.replace(baseUrl, '');\n    \n    // Supprimer les slashes au début et à la fin\n    if (path.startsWith('/')) path = path.substring(1);\n    if (path.endsWith('/')) path = path.substring(0, path.length - 1);\n    \n    return path;\n  }\n  \n  // Fonction pour créer l'arborescence d'un domaine spécifique\n  function processUrlsForDomain(domainInfo) {\n    // Créer une structure arborescente pour ce domaine\n    const tree = {};\n    \n    // Ajouter la page d'accueil (niveau 0)\n    tree[domainInfo.baseUrl] = {\n      url: domainInfo.baseUrl,\n      level: 0,\n      segments: [],\n      displayText: domainInfo.fullDomain.toUpperCase(),\n      children: {}\n    };\n    \n    // Trier les URLs par longueur de chemin (du plus court au plus long)\n    domainInfo.urls.sort((a, b) => {\n      const pathA = getPathFromUrl(a, domainInfo.baseUrl);\n      const pathB = getPathFromUrl(b, domainInfo.baseUrl);\n      \n      const segmentsA = pathA ? pathA.split('/') : [];\n      const segmentsB = pathB ? pathB.split('/') : [];\n      \n      // D'abord comparer le nombre de segments\n      if (segmentsA.length !== segmentsB.length) {\n        return segmentsA.length - segmentsB.length;\n      }\n      \n      // Si même nombre de segments, comparer alphabétiquement\n      return pathA.localeCompare(pathB);\n    });\n    \n    // Construire l'arborescence\n    for (const url of domainInfo.urls) {\n      // Ignorer l'URL racine déjà ajoutée\n      if (url === domainInfo.baseUrl || url === domainInfo.baseUrl + '/') continue;\n      \n      const path = getPathFromUrl(url, domainInfo.baseUrl);\n      const segments = path ? path.split('/') : [];\n      \n      // Déterminer le niveau (limité à 5)\n      const level = Math.min(segments.length, 5);\n      \n      if (level === 0) continue; // Ignorer les duplications de l'URL racine\n      \n      // Construire le chemin complet segment par segment\n      let currentNode = tree[domainInfo.baseUrl];\n      let parentPath = domainInfo.baseUrl;\n      \n      for (let i = 0; i < level; i++) {\n        const segment = segments[i];\n        const currentPath = parentPath + '/' + segment;\n        \n        // Créer le nœud s'il n'existe pas\n        if (!currentNode.children[segment]) {\n          currentNode.children[segment] = {\n            url: currentPath,\n            level: i + 1,\n            segments: segments.slice(0, i + 1),\n            displayText: formatDisplayText(segment),\n            children: {}\n          };\n        }\n        \n        // Avancer au nœud enfant\n        currentNode = currentNode.children[segment];\n        parentPath = currentPath;\n      }\n    }\n    \n    // Convertir l'arborescence en lignes\n    const domainRows = [];\n    \n    // Fonction récursive pour parcourir l'arborescence\n    function traverseTree(node) {\n      // Créer une nouvelle ligne\n      const row = {\n        \"Niv 0\": \"\",\n        \"Niv 1\": \"\",\n        \"Niv 2\": \"\",\n        \"Niv 3\": \"\",\n        \"Niv 4\": \"\",\n        \"Niv 5\": \"\",\n        \"URL\": node.url // Ajout de la colonne URL avec l'URL en texte brut\n      };\n      \n      // Définir la valeur au niveau approprié\n      if (node.level <= 5) {\n        row[`Niv ${node.level}`] = `=HYPERLINK(\"${node.url}\";\"${node.displayText}\")`;\n      }\n      \n      // Ajouter la ligne au résultat\n      domainRows.push(row);\n      \n      // Traiter les enfants dans l'ordre alphabétique\n      const children = Object.values(node.children);\n      children.sort((a, b) => a.displayText.localeCompare(b.displayText));\n      \n      for (const child of children) {\n        traverseTree(child);\n      }\n    }\n    \n    // Commencer le parcours avec le nœud racine\n    traverseTree(tree[domainInfo.baseUrl]);\n    \n    return domainRows;\n  }\n  \n  // Trier les domaines: d'abord le domaine principal (sans sous-domaine), puis les sous-domaines\n  const sortedDomains = Object.values(domains).sort((a, b) => {\n    // Si un domaine est exactement le domaine principal, il vient en premier\n    const aParts = a.fullDomain.split('.');\n    const bParts = b.fullDomain.split('.');\n    \n    // Cas spécial pour www: le traiter comme domaine principal\n    const aIsWWW = aParts.length > 2 && aParts[0] === 'www';\n    const bIsWWW = bParts.length > 2 && bParts[0] === 'www';\n    \n    if (aIsWWW && !bIsWWW) return -1;\n    if (!aIsWWW && bIsWWW) return 1;\n    \n    // Ensuite comparer par nombre de parties\n    if (aParts.length !== bParts.length) {\n      return aParts.length - bParts.length;\n    }\n    \n    // Enfin, comparer alphabétiquement\n    return a.fullDomain.localeCompare(b.fullDomain);\n  });\n  \n  // Traiter chaque domaine et ajouter les résultats\n  for (const domainInfo of sortedDomains) {\n    const domainRows = processUrlsForDomain(domainInfo);\n    results.push(...domainRows);\n  }\n  \n  return results;\n}\n\n/**\n * Fonction principale pour traiter l'entrée de n8n\n */\nfunction processInput() {\n  try {\n    // Récupérer les données de la node \"Map a website and get urls\" en utilisant la méthode $()\n    // Cette méthode a été confirmée fonctionnelle par nos tests\n    const firecrawlData = $('Map a website and get urls').item.json;\n    \n    // Vérifier la structure des données\n    if (!firecrawlData || !firecrawlData.success || !Array.isArray(firecrawlData.links)) {\n      throw new Error(\"Données d'entrée non valides ou structure incorrecte\");\n    }\n    \n    // Traiter les URLs pour créer l'arborescence\n    const siteHierarchy = createSiteHierarchy(firecrawlData);\n    \n    // Créer un nouvel item pour chaque ligne de l'arborescence\n    // C'est le format attendu par Google Sheets dans n8n\n    return siteHierarchy.map(row => {\n      return {\n        json: row\n      };\n    });\n    \n  } catch (error) {\n    console.error(\"Erreur lors du traitement:\", error.message);\n    // Retourner un message d'erreur formaté pour n8n\n    return [{\n      json: {\n        error: true,\n        message: error.message,\n        details: error.stack\n      }\n    }];\n  }\n}\n\n// Exécuter le traitement\nreturn processInput();"
      },
      "typeVersion": 2
    },
    {
      "id": "85d435ab-89fb-4f30-a5ff-66239dd02bfc",
      "name": "최종 응답",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1648,
        0
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={\n  \"text\": \"Cliquez [ici](https://docs.google.com/spreadsheets/d/{{ $('Copy template').item.json.id }}) afin d'accéder à votre arborescence\"\n}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "9d187a39-57fd-43f3-9426-6ba0f13b4a6b",
      "name": "Bad URL",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        672,
        208
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={\n  \"text\": \"L'url {{ $('Chat input').item.json.chatInput }} n'est pas une url correcte ou elle n'est pas prise en compte par ce service\"\n}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "b58250bb-3f3e-4a29-a8c1-215f23503a79",
      "name": "웹사이트 매핑 및 URL 가져오기",
      "type": "@mendable/n8n-nodes-firecrawl.firecrawl",
      "position": [
        272,
        0
      ],
      "parameters": {
        "url": "={{ $json.chatInput }}",
        "operation": "map",
        "sitemapOnly": true,
        "ignoreSitemap": false,
        "requestOptions": {}
      },
      "credentials": {
        "firecrawlApi": {
          "id": "E34WDB80ik5VHjiI",
          "name": "Firecrawl account"
        }
      },
      "typeVersion": 1
    }
  ],
  "pinData": {},
  "connections": {
    "9e188a8a-4faa-488d-ba3e-10e25fb94c05": {
      "main": [
        [
          {
            "node": "85d435ab-89fb-4f30-a5ff-66239dd02bfc",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3ab02f4d-4593-4d32-8007-f657e7706f84": {
      "main": [
        [
          {
            "node": "1f44edba-d802-4e48-a193-9aa073971724",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "9d187a39-57fd-43f3-9426-6ba0f13b4a6b",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1f44edba-d802-4e48-a193-9aa073971724": {
      "main": [
        [
          {
            "node": "90c7df17-d3ad-434b-b6ec-23c7e64888de",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "90c7df17-d3ad-434b-b6ec-23c7e64888de": {
      "main": [
        [
          {
            "node": "9e188a8a-4faa-488d-ba3e-10e25fb94c05",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "b58250bb-3f3e-4a29-a8c1-215f23503a79": {
      "main": [
        [
          {
            "node": "3ab02f4d-4593-4d32-8007-f657e7706f84",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "f6f18549-b9e2-4ea5-b0ad-9a4a4df3bff1": {
      "main": [
        [
          {
            "node": "b58250bb-3f3e-4a29-a8c1-215f23503a79",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
자주 묻는 질문

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

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

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

중급 - 시장 조사, 멀티모달 AI

유료인가요?

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

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

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

외부 링크
n8n.io에서 보기

이 워크플로우 공유

카테고리

카테고리: 34