{
  "id": "rc4n2nMUBWhg7IIv",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Github Code to LinkedIn Publisher",
  "tags": [],
  "nodes": [
    {
      "id": "0667bada-510c-4bac-a863-8ef1b29f06ee",
      "name": "Main Info",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2592,
        128
      ],
      "parameters": {
        "width": 686,
        "height": 423,
        "content": "## \ud83d\ude80 Github Code to LinkedIn Publisher\n\n### How it works\nAutomatically turns your code commits into engaging LinkedIn posts! \n1. **Detects** new/modified code in Github.\n2. **Analyzes** the code with AI to write a catchy post & extract key snippets.\n3. **Generates** a beautiful \"Mac-window\" style image of the code.\n4. **Uploads** the image back to Github & **Posts** everything to LinkedIn.\n\n### Setup steps\n- [ ] **Credentials**: Configure Github, LinkedIn, OpenRouter, and HCTI API.\n- [ ] **Github Nodes**: Update `owner` and `repository` fields in the Trigger and Download nodes.\n- [ ] **LinkedIn Node**: Update the `person` URN.\n- [ ] **HCTI**: Ensure you have an account for image generation."
      },
      "typeVersion": 1
    },
    {
      "id": "a3290627-b926-4b7c-b853-1c9384738f54",
      "name": "Section 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1840,
        176
      ],
      "parameters": {
        "color": 7,
        "width": 896,
        "height": 372,
        "content": "### 1. Monitor & Fetch\nWatches for `push` events, extracts modified files, and downloads the raw code content."
      },
      "typeVersion": 1
    },
    {
      "id": "d1d2dcd2-b6df-4680-bf7d-7327ee944ceb",
      "name": "Section 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1744,
        672
      ],
      "parameters": {
        "color": 7,
        "width": 532,
        "height": 468,
        "content": "### 2. AI Content Creation\nUses an LLM to analyze the code, write a LinkedIn post, and select the best snippet."
      },
      "typeVersion": 1
    },
    {
      "id": "eff6680c-f3cc-4559-b15d-c8b9a1415b3a",
      "name": "Section 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1200,
        672
      ],
      "parameters": {
        "color": 7,
        "width": 652,
        "height": 340,
        "content": "### 3. Image Generation\nCreates HTML/CSS for a \"pretty\" code window and converts it to an image via API."
      },
      "typeVersion": 1
    },
    {
      "id": "9bf75f05-3346-40b6-9e50-13c450d64905",
      "name": "Section 4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -928,
        96
      ],
      "parameters": {
        "color": 7,
        "width": 844,
        "height": 492,
        "content": "### 4. Store & Publish\nUploads the generated image to your repo for hosting, then combines text + image for the final LinkedIn post."
      },
      "typeVersion": 1
    },
    {
      "id": "53ff6b5d-cda9-4bf5-b28a-a8983e8b8d5e",
      "name": "Github Trigger1",
      "type": "n8n-nodes-base.githubTrigger",
      "position": [
        -1808,
        352
      ],
      "parameters": {
        "owner": {
          "__rl": true,
          "mode": "name",
          "value": "your-github-username"
        },
        "events": [
          "push"
        ],
        "options": {},
        "repository": {
          "__rl": true,
          "mode": "name",
          "value": "your-source-repo-name"
        }
      },
      "credentials": {
        "githubApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ae31ca41-9c74-48ef-bb43-9ff5627c1c0f",
      "name": "GitHub File Download",
      "type": "n8n-nodes-base.github",
      "position": [
        -1392,
        352
      ],
      "parameters": {
        "owner": {
          "__rl": true,
          "mode": "name",
          "value": "your-github-username"
        },
        "filePath": "={{ $json.filePath }}",
        "resource": "file",
        "operation": "get",
        "repository": {
          "__rl": true,
          "mode": "name",
          "value": "your-source-repo-name"
        },
        "additionalParameters": {}
      },
      "credentials": {
        "githubApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "c19ca9b7-3d0a-4f8a-be54-bff3b188c12a",
      "name": "Post to LinkedIn",
      "type": "n8n-nodes-base.linkedIn",
      "position": [
        -304,
        416
      ],
      "parameters": {
        "text": "={{ $('LinkedIn Content Creator').item.json.output.post_title }}\n\n{{ $('LinkedIn Content Creator').item.json.output.post_content }}\n{{ $('LinkedIn Content Creator').item.json.output.hashtags }}\n\nLink to Github: {{ $json.content._links.html }}\n",
        "person": "your-linkedin-urn",
        "additionalFields": {
          "visibility": "PUBLIC"
        },
        "shareMediaCategory": "IMAGE"
      },
      "credentials": {
        "linkedInOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "3eb75e0a-0c99-4cf1-b6a5-105a5fb705d4",
      "name": "LinkedIn Content Creator",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -1616,
        784
      ],
      "parameters": {
        "text": "=Data\n{{ $json.data }}",
        "options": {
          "systemMessage": "You are a LinkedIn content strategist and copywriter specialising in professional, engaging technical content focused on infrastructure as code with bicep for azure workloads.\n\nAnalyze the provided code file and create:\n1. A LinkedIn post with compelling narrative (without including the actual code)\n2. Extract the most important/interesting code snippet (10-20 lines max) that showcases the key concept\n\nFor the LinkedIn post:\n- Open with a compelling hook that grabs attention\n- Explain what the code does and why it's valuable\n- Use professional yet conversational tone\n- Include a clear call-to-action\n- Stay within 400 characters\n- DO NOT include the actual code in the post content\n\nFor the code snippet:\n- Select the most interesting or important part\n- Keep it concise (10-20 lines)\n- Ensure it's self-contained and understandable\n- Include any necessary context as comments\n\nIMPORTANT: Return hashtags as a single string with space-separated hashtags, NOT as an array.\n\nFormat your response as JSON with the following structure:\n{\n  \"post_title\": \"string\",\n  \"post_content\": \"string (narrative without code and without hashtags)\",\n  \"code_snippet\": \"string (the actual code to display)\",\n  \"code_language\": \"string (javascript, typescript, python, etc.)\",\n  \"hashtags\": \"string\",\n  \"character_count\": integer\n}"
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 2
    },
    {
      "id": "377af262-7c5f-45ad-81a3-ea8af25f3862",
      "name": "Create Code HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        -1152,
        752
      ],
      "parameters": {
        "jsCode": "// Get the code snippet and language from previous node\nconst codeSnippet = $json.code_snippet || '';\nconst language = $json.code_language || 'javascript';\n\n// Calculate dimensions based on content\nconst lines = codeSnippet.split('\\n');\nconst lineCount = lines.length;\nconst maxLineLength = Math.max(...lines.map(line => line.length));\n\n// Adjust sizing calculations\nconst charWidth = 8.4; // approximate width per character in monospace\nconst lineHeight = 24; // pixels per line\nconst containerPadding = 20; // padding inside code container\nconst bodyPadding = 10; // minimal body padding\nconst headerHeight = 40; // header with dots\n\n// Calculate dimensions and FORCE INTEGERS (Math.ceil) to prevent API errors\nconst calculatedWidth = Math.max(400, maxLineLength * charWidth + (containerPadding * 2));\nconst contentWidth = Math.ceil(Math.min(calculatedWidth, 1200)); \nconst contentHeight = Math.ceil((lineCount * lineHeight) + headerHeight + (containerPadding * 2));\n\n// Create HTML with minimal padding, sized to content\nconst html = `\n<!DOCTYPE html>\n<html>\n<head>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <link href=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css\" rel=\"stylesheet\" />\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=Fira+Code:wght@400;500&display=swap\" rel=\"stylesheet\">\n  <style>\n    * {\n      margin: 0;\n      padding: 0;\n      box-sizing: border-box;\n    }\n    html, body {\n      width: 100%;\n      height: 100%;\n      margin: 0;\n      padding: 0;\n    }\n    body {\n      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n      font-family: 'Fira Code', 'Consolas', monospace;\n      padding: ${bodyPadding}px;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n    }\n    .code-container {\n      background: #2d2d2d;\n      border-radius: 10px;\n      padding: ${containerPadding}px;\n      box-shadow: 0 10px 30px rgba(0,0,0,0.4);\n      width: auto;\n      max-width: 100%;\n    }\n    .header {\n      display: flex;\n      gap: 8px;\n      margin-bottom: 15px;\n    }\n    .dot {\n      width: 12px;\n      height: 12px;\n      border-radius: 50%;\n    }\n    .dot-red { background: #ff5f56; }\n    .dot-yellow { background: #ffbd2e; }\n    .dot-green { background: #27c93f; }\n    pre {\n      margin: 0;\n      padding: 0;\n      overflow-x: visible;\n      white-space: pre;\n      max-width: 100%;\n    }\n    code {\n      font-size: 12px;\n      line-height: 1.5;\n      display: block;\n      font-family: 'Fira Code', 'Consolas', monospace;\n    }\n    /* Override Prism's default margins */\n    pre[class*=\"language-\"] {\n      margin: 0;\n      padding: 0;\n      background: transparent;\n    }\n  </style>\n</head>\n<body>\n  <div class=\"code-container\">\n    <div class=\"header\">\n      <div class=\"dot dot-red\"></div>\n      <div class=\"dot dot-yellow\"></div>\n      <div class=\"dot dot-green\"></div>\n    </div>\n    <pre><code class=\"language-${language}\">${codeSnippet.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</code></pre>\n  </div>\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js\"></script>\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-${language}.min.js\"></script>\n</body>\n</html>\n`;\n\nreturn [{\n  json: {\n    ...items[0].json,\n    html: html,\n    estimatedWidth: contentWidth + (bodyPadding * 2),\n    estimatedHeight: contentHeight + (bodyPadding * 2)\n  }\n}];"
      },
      "typeVersion": 1
    },
    {
      "id": "d1d0195c-ec4e-4990-bce9-0b103ceef218",
      "name": "Generate Code Image",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -960,
        752
      ],
      "parameters": {
        "url": "https://hcti.io/v1/image",
        "method": "POST",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendBody": true,
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "html",
              "value": "={{ $json.html }}"
            },
            {
              "name": "google_fonts",
              "value": "Fira Code"
            },
            {
              "name": "viewport_width",
              "value": "={{ Math.min($json.estimatedWidth || 800, 1920) }}"
            },
            {
              "name": "viewport_height",
              "value": "={{ Math.min($json.estimatedHeight || 400, 1080) }}"
            },
            {
              "name": "device_scale",
              "value": "={{ 2 }}"
            }
          ]
        },
        "genericAuthType": "httpBasicAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpBasicAuth": {
          "name": "<your credential>"
        },
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "b900240c-9695-4c2a-8766-a0703c530721",
      "name": "GET the genarted Image",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -752,
        752
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.2,
      "alwaysOutputData": true
    },
    {
      "id": "88ce4578-58f3-4cbc-8ae4-fed95b73a75d",
      "name": "Upload to Image Repo",
      "type": "n8n-nodes-base.github",
      "position": [
        -864,
        192
      ],
      "parameters": {
        "owner": {
          "__rl": true,
          "mode": "name",
          "value": "your-github-username"
        },
        "filePath": "={{ $('GitHub File Download').item.json.filePath }}",
        "resource": "file",
        "repository": {
          "__rl": true,
          "mode": "name",
          "value": "your-image-storage-repo"
        },
        "fileContent": "={{ $json.data }}",
        "commitMessage": "=Upload generated image for {{ $('GitHub File Download').item.json.filePath }}"
      },
      "credentials": {
        "githubApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "b99f1752-f6cd-4149-88c7-ba02975aeaf4",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        -496,
        416
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineAll"
      },
      "typeVersion": 3.2
    },
    {
      "id": "d9397243-bad3-4699-9056-a22e41009a4d",
      "name": "Extract Modified Files",
      "type": "n8n-nodes-base.code",
      "notes": "Needed to extract the path of different types of changes in Github, be it modified, added, etc.",
      "position": [
        -1584,
        352
      ],
      "parameters": {
        "jsCode": "// Get the head_commit object from the webhook body\nconst headCommit = $json.body.head_commit;\n\n// Get the arrays of added and modified files. Default to empty arrays if they don't exist.\nconst addedFiles = headCommit.added || [];\nconst modifiedFiles = headCommit.modified || [];\n\n// Combine both arrays into a single list of file paths\nconst allChangedFiles = [...addedFiles, ...modifiedFiles];\n\n// If there are no changed files, return an empty result to stop the workflow gracefully.\nif (allChangedFiles.length === 0) {\n  return [];\n}\n\n// Create a new list of n8n items. Each item will contain one file path.\nconst items = allChangedFiles.map(filePath => {\n  return {\n    json: {\n      // We store the path in a new field called 'filePath'\n      filePath: filePath\n    }\n  };\n});\n\n// Return the new list of items\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "b00dc7bd-c999-4617-b752-1c78a93fabd7",
      "name": "Extract from File",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -1120,
        352
      ],
      "parameters": {
        "options": {},
        "operation": "text"
      },
      "typeVersion": 1
    },
    {
      "id": "426f6797-088e-42f2-9f5d-ebcf2b853974",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        -1472,
        1008
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n\t\"type\": \"object\",\n\t\"properties\": {\n\t\t\"post_title\": {\n\t\t\t\"type\": \"string\"\n\t\t},\n      \"post_content\": {\n\t\t\t\"type\": \"string\"\n\t\t},\n      \"code_snippet\": {\n\t\t\t\"type\": \"string\"\n\t\t},\n      \"code_language\": {\n\t\t\t\"type\": \"string\"\n\t\t},\n      \"hashtags\": {\n\t\t\t\"type\": \"string\"\n\t\t},\n      \"character_count\": {\n\t\t\t\"type\": \"integer\"\n\t\t}\n\t}\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "b1c72cac-7e02-4a24-8dc0-99ea96a00c44",
      "name": "Edit Fields",
      "type": "n8n-nodes-base.set",
      "position": [
        -1328,
        752
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "13b0cb01-c54d-43bb-90df-a48f12d76fb3",
              "name": "post_title",
              "type": "string",
              "value": "={{ $json.output.post_title }}"
            },
            {
              "id": "f3b23908-6876-46b7-be5f-d8a41c209202",
              "name": "post_content",
              "type": "string",
              "value": "={{ $json.output.post_content }}"
            },
            {
              "id": "a915c874-e76a-408e-ad92-d8863397a5e0",
              "name": "code_snippet",
              "type": "string",
              "value": "={{ $json.output.code_snippet }}"
            },
            {
              "id": "571704c3-425c-4aa2-a44a-1147ac56ad1c",
              "name": "code_language",
              "type": "string",
              "value": "={{ $json.output.code_language }}"
            },
            {
              "id": "6599c4be-cbaf-4155-b7b9-4930b1386e48",
              "name": "hashtags",
              "type": "string",
              "value": "={{ $json.output.hashtags }}"
            },
            {
              "id": "0e9bde61-489f-4c20-b68a-0193cd8a388e",
              "name": "character_count",
              "type": "number",
              "value": "={{ $json.output.character_count }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "7d068914-c1cc-4553-945d-0327ea5e66df",
      "name": "OpenRouter Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        -1616,
        1008
      ],
      "parameters": {
        "model": "google/gemini-2.5-flash",
        "options": {
          "responseFormat": "json_object"
        }
      },
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "5afb4709-1651-4ba5-9cb7-e9da7437afe4",
      "name": "Extract from File1",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -1120,
        192
      ],
      "parameters": {
        "options": {},
        "operation": "binaryToPropery"
      },
      "typeVersion": 1.1
    }
  ],
  "active": false,
  "settings": {
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "7d6f38a7-c1b0-496a-8c19-2c533b631ddf",
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "Post to LinkedIn",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Create Code HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Github Trigger1": {
      "main": [
        [
          {
            "node": "Extract Modified Files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Code HTML": {
      "main": [
        [
          {
            "node": "Generate Code Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from File": {
      "main": [
        [
          {
            "node": "LinkedIn Content Creator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from File1": {
      "main": [
        [
          {
            "node": "Upload to Image Repo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Code Image": {
      "main": [
        [
          {
            "node": "GET the genarted Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GitHub File Download": {
      "main": [
        [
          {
            "node": "Extract from File",
            "type": "main",
            "index": 0
          },
          {
            "node": "Extract from File1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload to Image Repo": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "LinkedIn Content Creator",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Extract Modified Files": {
      "main": [
        [
          {
            "node": "GitHub File Download",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GET the genarted Image": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "LinkedIn Content Creator": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "LinkedIn Content Creator",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    }
  }
}