{
  "id": "88oaK8VygviD77nR",
  "name": "Generate social hub (link-in-bio) page with FireCrawl AI and Apify",
  "tags": [],
  "nodes": [
    {
      "id": "b8a7f53a-e611-4689-92b1-2951f4cf7b94",
      "name": "On form submission",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        560,
        -208
      ],
      "parameters": {
        "path": "website-link-aggregator",
        "options": {},
        "formTitle": "Website Link Aggregator",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Website URL",
              "placeholder": "Example: https://n8n.io",
              "requiredField": true
            }
          ]
        },
        "responseMode": "responseNode",
        "formDescription": "Insert your website URL and wait for the result"
      },
      "typeVersion": 2.1
    },
    {
      "id": "0993afeb-cc7a-4476-a677-96862ae71f05",
      "name": "Start Apify Scraper",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        784,
        -208
      ],
      "parameters": {
        "url": "=https://api.apify.com/v2/acts/WYyiMAvNXhfc2Rthx/runs?token=YOUR_APIFY_TOKEN",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ Posts: [$json['Website URL']] }) }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "3583d1f2-e17c-48b4-94fe-00368ef6e1ea",
      "name": "Wait for the Apify Scraper Process",
      "type": "n8n-nodes-base.wait",
      "position": [
        1008,
        -208
      ],
      "parameters": {
        "amount": 30
      },
      "typeVersion": 1.1
    },
    {
      "id": "fd7a5122-0de2-4e83-b9d7-6f089c0201cc",
      "name": "Get Apify Results",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1424,
        -288
      ],
      "parameters": {
        "url": "={{ \"https://api.apify.com/v2/datasets/\" + $('Start Apify Scraper').item.json.data.defaultDatasetId + \"/items?token=YOUR_APIFY_TOKEN\" }}",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "id": "9adcd448-b69c-4c86-b1b1-cecfe5c4e538",
      "name": "Scrape website description",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1424,
        -128
      ],
      "parameters": {
        "url": "https://api.firecrawl.dev/v1/scrape",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ url: ($('On form submission').item.json['Website URL'].startsWith('http') ? $('On form submission').item.json['Website URL'] : 'https://' + $('On form submission').item.json['Website URL']), formats: ['extract'], extract: { prompt: 'Extract a short 1-2 sentence description about what this business does. Keep it under 150 characters.' } }) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "b2193cfc-1b72-4966-9436-7736bbabc251",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2528,
        -208
      ],
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/html; charset=utf-8"
              }
            ]
          }
        },
        "respondWith": "text",
        "responseBody": "={{ $json.html }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "2beb1c73-9831-42eb-ae9d-83ed829d4a8b",
      "name": "Merge result from Apify and Firecrawl",
      "type": "n8n-nodes-base.merge",
      "position": [
        1680,
        -208
      ],
      "parameters": {},
      "typeVersion": 3
    },
    {
      "id": "89827724-2a4b-4968-b0f5-5dd5ac32e6e9",
      "name": "Create html format",
      "type": "n8n-nodes-base.code",
      "position": [
        2256,
        -208
      ],
      "parameters": {
        "jsCode": "const d = $input.first().json;\n\nconst socialIcons = {\n  facebook: 'https://cdn.simpleicons.org/facebook/1877F2',\n  instagram: 'https://cdn.simpleicons.org/instagram/E1306C',\n  twitter: 'https://cdn.simpleicons.org/x/000000',\n  linkedin: 'https://www.svgrepo.com/show/157006/linkedin.svg',\n  youtube: 'https://cdn.simpleicons.org/youtube/FF0000',\n  tiktok: 'https://cdn.simpleicons.org/tiktok/000000',\n  whatsapp: 'https://cdn.simpleicons.org/whatsapp/25D366'\n};\n\nlet linksHtml = '';\nconst socials = ['facebook', 'instagram', 'twitter', 'linkedin', 'youtube', 'tiktok', 'whatsapp'];\n\nfor (let i = 0; i < socials.length; i++) {\n  const platform = socials[i];\n  \n  if (d[platform]) {\n    const displayName = platform.charAt(0).toUpperCase() + platform.slice(1);\n    \n    linksHtml += `\n    <a href=\"${d[platform]}\" class=\"link\" target=\"_blank\" rel=\"noopener\">\n      <img src=\"${socialIcons[platform]}\" class=\"icon\" alt=\"${displayName}\">\n      ${displayName}\n    </a>`;\n  }\n}\n\nlet websiteUrl = d.originalUrl;\nif (!websiteUrl.startsWith('http')) {\n  websiteUrl = 'https://' + websiteUrl;\n}\n\nlinksHtml += `\n<a href=\"${websiteUrl}\" class=\"link\" target=\"_blank\" rel=\"noopener\">\n  <img src=\"https://cdn.simpleicons.org/googlechrome/4285F4\" class=\"icon\" alt=\"Website\">\n  Visit Website\n</a>`;\n\nconst html = `<!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>${d.domain}</title>\n  <style>\n    * {\n      margin: 0;\n      padding: 0;\n      box-sizing: border-box;\n    }\n    \n    body {\n      font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n      min-height: 100vh;\n      padding: 40px 20px;\n    }\n    \n    .wrapper {\n      max-width: 600px;\n      margin: 0 auto;\n    }\n    \n    .header {\n      text-align: center;\n      margin-bottom: 35px;\n    }\n    \n    .profile-pic {\n      width: 96px;\n      height: 96px;\n      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n      border: 4px solid white;\n      border-radius: 50%;\n      margin: 0 auto 20px;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      color: white;\n      font-size: 36px;\n      font-weight: 700;\n      box-shadow: 0 8px 16px rgba(0,0,0,0.1);\n    }\n    \n    .domain-name {\n      font-size: 24px;\n      font-weight: 700;\n      color: white;\n      margin: 0 0 8px 0;\n      text-shadow: 0 2px 4px rgba(0,0,0,0.1);\n    }\n    \n    .bio {\n      font-size: 15px;\n      line-height: 1.5;\n      color: rgba(255,255,255,0.95);\n      max-width: 520px;\n      margin: 0 auto;\n    }\n    \n    .links-container {\n      display: flex;\n      flex-direction: column;\n      gap: 12px;\n    }\n    \n    .link {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      gap: 12px;\n      padding: 18px;\n      background: white;\n      border-radius: 12px;\n      text-decoration: none;\n      color: #222;\n      font-size: 16px;\n      font-weight: 600;\n      box-shadow: 0 2px 8px rgba(0,0,0,0.08);\n      transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n    }\n    \n    .link:hover {\n      transform: translateY(-2px);\n      box-shadow: 0 4px 16px rgba(0,0,0,0.12);\n    }\n    \n    .link:active {\n      transform: translateY(0);\n    }\n    \n    .icon {\n      width: 24px;\n      height: 24px;\n    }\n    \n    @media (max-width: 500px) {\n      body {\n        padding: 30px 16px;\n      }\n      \n      .profile-pic {\n        width: 80px;\n        height: 80px;\n        font-size: 30px;\n      }\n      \n      .domain-name {\n        font-size: 20px;\n      }\n    }\n  </style>\n</head>\n<body>\n  <div class=\"wrapper\">\n    <div class=\"header\">\n      <div class=\"profile-pic\">${d.initials}</div>\n      <h1 class=\"domain-name\">${d.domain}</h1>\n      <p class=\"bio\">${d.description}</p>\n    </div>\n    \n    <div class=\"links-container\">\n      ${linksHtml}\n    </div>\n  </div>\n</body>\n</html>`;\n\nreturn { html };\n"
      },
      "typeVersion": 2
    },
    {
      "id": "58442908-f5cc-496c-9372-34dc76620889",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        496,
        -336
      ],
      "parameters": {
        "color": 7,
        "width": 704,
        "height": 336,
        "content": "## 1. Start scraping process from the user's input"
      },
      "typeVersion": 1
    },
    {
      "id": "cbdc29e4-d7c3-402a-bf1b-6c54391558dd",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1328,
        -368
      ],
      "parameters": {
        "color": 7,
        "width": 768,
        "height": 432,
        "content": "## 2. Get the scrape results and start Firecrawl to summarize"
      },
      "typeVersion": 1
    },
    {
      "id": "4df92277-ad39-4901-94ef-5eb1db614a2f",
      "name": "Process the description and all social media",
      "type": "n8n-nodes-base.code",
      "position": [
        1888,
        -208
      ],
      "parameters": {
        "jsCode": "const allInputs = $input.all();\nconst originalUrl = $('On form submission').item.json['Website URL'];\n\nconst apifyItems = allInputs.filter(item => item.json['Contact Type']);\nconst firecrawlItems = allInputs.filter(item => item.json.data && item.json.data.extract);\n\nlet description = 'Connect with us through our channels';\nif (firecrawlItems.length > 0) {\n  const extract = firecrawlItems[0].json.data.extract;\n  if (extract.businessDescription) {\n    description = extract.businessDescription;\n  } else if (typeof extract === 'string') {\n    description = extract;\n  } else if (extract.description || extract.summary) {\n    description = extract.description || extract.summary;\n  }\n  if (description.length > 150) {\n    description = description.substring(0, 147) + '...';\n  }\n}\n\nlet emails = [];\nlet phones = [];\nlet socials = [];\n\napifyItems.forEach(input => {\n  const type = input.json['Contact Type'];\n  const value = input.json['Contact Value'];\n  if (!type || !value) return;\n  \n  const t = type.toLowerCase().trim();\n  if (t.includes('email')) emails.push(value);\n  else if (t.includes('phone')) phones.push(value);\n  else if (t.includes('social')) socials.push(value);\n});\n\nemails = [...new Set(emails)].filter(e => e && !e.toLowerCase().includes('noreply')).slice(0, 3);\nphones = [...new Set(phones)].filter(p => p && p.length > 5).slice(0, 3);\n\nconst socialMap = {};\nsocials.forEach(url => {\n  if (!url || typeof url !== 'string') return;\n  \n  const u = url.toLowerCase();\n  let platform = null;\n  \n  if (u.includes('facebook.com') || u.includes('fb.com')) platform = 'Facebook';\n  else if (u.includes('instagram.com')) platform = 'Instagram';\n  else if (u.includes('twitter.com') || u.includes('x.com')) platform = 'Twitter';\n  else if (u.includes('linkedin.com')) platform = 'LinkedIn';\n  else if (u.includes('youtube.com') || u.includes('youtu.be')) platform = 'YouTube';\n  else if (u.includes('tiktok.com')) platform = 'TikTok';\n  else if (u.includes('whatsapp.com') || u.includes('wa.me')) platform = 'WhatsApp';\n  \n  if (platform && !socialMap[platform]) socialMap[platform] = url;\n});\n\nlet domain = 'Website';\ntry {\n  const url = new URL(originalUrl.startsWith('http') ? originalUrl : 'https://' + originalUrl);\n  domain = url.hostname.replace('www.', '');\n} catch(e) {\n  domain = originalUrl;\n}\n\nconst initials = domain.split('.')[0].substring(0, 2).toUpperCase();\n\nreturn {\n  domain,\n  initials,\n  originalUrl,\n  description,\n  emails,\n  phones,\n  facebook: socialMap['Facebook'] || '',\n  instagram: socialMap['Instagram'] || '',\n  twitter: socialMap['Twitter'] || '',\n  linkedin: socialMap['LinkedIn'] || '',\n  youtube: socialMap['YouTube'] || '',\n  tiktok: socialMap['TikTok'] || '',\n  whatsapp: socialMap['WhatsApp'] || ''\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "3cb0f863-94e5-4677-ab72-ae8a8ffd9918",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2192,
        -368
      ],
      "parameters": {
        "color": 7,
        "width": 544,
        "height": 432,
        "content": "## 3. Create HTML and send back to the webhook (Form)"
      },
      "typeVersion": 1
    },
    {
      "id": "4ef13fb1-fb33-42a4-852b-186929728f1a",
      "name": "View HTML for redesign or debug",
      "type": "n8n-nodes-base.html",
      "position": [
        2432,
        272
      ],
      "parameters": {
        "html": "{{ $json.html }}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "d9cba68e-7cdd-4086-bdaa-c759e85e7102",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2208,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 528,
        "height": 304,
        "content": "## Use this node to debug or show the HTML if want to edit the style"
      },
      "typeVersion": 1
    },
    {
      "id": "bd6b274e-9ae3-4e68-9e73-7f7ae9095eb7",
      "name": "Sticky Note12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -112,
        -512
      ],
      "parameters": {
        "width": 512,
        "height": 704,
        "content": "## Generate social hub (link-in-bio) page with FireCrawl AI and Apify\n\n### How it works\n1. User submits a website URL through the form.\n2. Apify scrapes contact info \u00e2\u20ac\u201d emails, phones, and social media links.\n3. FireCrawl AI generates a short business description via LLM extraction.\n4. Results are merged and social platforms are identified (Facebook, Instagram, X, LinkedIn, YouTube, TikTok, WhatsApp).\n5. A styled link-in-bio HTML page is generated and returned as the form response.\n\n### Setup steps\n1. Get an [Apify API key](https://apify.com) and a [FireCrawl API key](https://www.firecrawl.dev).\n2. Replace `YOUR_APIFY_TOKEN` in both the **Start Apify Scraper** and **Get Apify Results** nodes.\n3. Replace `YOUR_TOKEN_HERE` in the **Scrape website description** node's Authorization header with your FireCrawl key (format: `Bearer fc-xxx`).\n4. If Apify errors on first run, visit the [Contact Details Scraper Actor](https://console.apify.com/actors/WYyiMAvNXhfc2Rthx/input) and run it once manually to enable it.\n\n### Customize\n1. **Change HTML styling**: Edit the CSS in the \\\"Create html format\\\" node to customize colors, fonts, layout, or add animations. The current design uses a purple gradient background with white cards.\n2. **Debug HTML output**: Use the \\\"View HTML for redesign or debug\\\" node at the bottom to preview the generated HTML without submitting through the webhook.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "d973bf35-c9df-4c3b-9bd4-56c13f413fde",
      "name": "Sticky Note13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2832,
        -464
      ],
      "parameters": {
        "width": 528,
        "height": 652,
        "content": "## Final result\n![](https://i.ibb.co/rK9WtphY/Screenshot-2025-12-29-155821.png)"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "732c712a-9c30-437d-b303-28a6d2288777",
  "connections": {
    "Get Apify Results": {
      "main": [
        [
          {
            "node": "Merge result from Apify and Firecrawl",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create html format": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "On form submission": {
      "main": [
        [
          {
            "node": "Start Apify Scraper",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start Apify Scraper": {
      "main": [
        [
          {
            "node": "Wait for the Apify Scraper Process",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape website description": {
      "main": [
        [
          {
            "node": "Merge result from Apify and Firecrawl",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Wait for the Apify Scraper Process": {
      "main": [
        [
          {
            "node": "Get Apify Results",
            "type": "main",
            "index": 0
          },
          {
            "node": "Scrape website description",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge result from Apify and Firecrawl": {
      "main": [
        [
          {
            "node": "Process the description and all social media",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process the description and all social media": {
      "main": [
        [
          {
            "node": "Create html format",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}