Last active
December 16, 2025 20:24
-
-
Save JohnONolan/a04222081e5066ae2b55cdc8f4d9d5f0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "Ghost Social → Twitter", | |
| "nodes": [ | |
| { | |
| "parameters": { | |
| "rule": { | |
| "interval": [ | |
| { | |
| "field": "minutes" | |
| } | |
| ] | |
| } | |
| }, | |
| "id": "0f4a4f43-26a3-4220-a59b-394f603fda04", | |
| "name": "Every 5 Minutes", | |
| "type": "n8n-nodes-base.scheduleTrigger", | |
| "typeVersion": 1.2, | |
| "position": [ | |
| -1616, | |
| 896 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "url": "https://YOUR_GHOST_URL.COM/.ghost/activitypub/outbox/index", | |
| "options": { | |
| "response": { | |
| "response": { | |
| "responseFormat": "json" | |
| } | |
| } | |
| } | |
| }, | |
| "id": "57b40c0b-78bc-4a80-ba45-3e903715ed31", | |
| "name": "Fetch Outbox", | |
| "type": "n8n-nodes-base.httpRequest", | |
| "typeVersion": 4.2, | |
| "position": [ | |
| -1392, | |
| 896 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "url": "={{ $json.first }}", | |
| "options": { | |
| "response": { | |
| "response": { | |
| "responseFormat": "json" | |
| } | |
| } | |
| } | |
| }, | |
| "id": "174fbb98-3a35-47eb-8f8e-e9e5dfd72700", | |
| "name": "Fetch First Page", | |
| "type": "n8n-nodes-base.httpRequest", | |
| "typeVersion": 4.2, | |
| "position": [ | |
| -1184, | |
| 896 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "jsCode": "// Get the ordered items from the outbox page\nconst items = $input.first().json.orderedItems || [];\n\n// Get the static data to track what we've already posted\nconst staticData = $getWorkflowStaticData('global');\nconst postedIds = staticData.postedIds || [];\n\n// Only process posts after this date\nconst cutoffDate = new Date('2025-12-15T00:00:00Z');\n\n// Filter to only Create activities for Note/Article objects\n// that we haven't already posted\nconst newPosts = [];\n\nfor (const item of items) {\n // Skip if not a Create activity\n if (item.type !== 'Create') continue;\n \n const object = item.object;\n if (!object) continue;\n \n // Skip replies - only syndicate top-level posts\n if (object.inReplyTo) continue;\n \n // Skip if we've already posted this\n const postId = object.id || item.id;\n if (postedIds.includes(postId)) continue;\n \n // Get the published date\n const published = object.published || item.published;\n \n // Skip if published before our cutoff date (December 15, 2025)\n if (!published || new Date(published) < cutoffDate) continue;\n \n // Extract the content\n let content = '';\n let url = object.url || object.id;\n \n // Handle different object types\n if (object.type === 'Note' || object.type === 'Article') {\n // Convert line breaks to newlines, then strip HTML tags and decode entities\n content = (object.content || object.name || '')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/p>\\s*<p>/gi, '\\n\\n')\n .replace(/<[^>]*>/g, '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ')\n .trim();\n \n // For Articles, prefer the name/title\n if (object.type === 'Article' && object.name) {\n content = object.name;\n }\n }\n \n if (!content) continue;\n \n // Extract image attachments (up to 4 for Twitter)\n const images = [];\n const attachments = object.attachment || object.image || [];\n const attachmentArray = Array.isArray(attachments) ? attachments : [attachments];\n \n for (const att of attachmentArray) {\n if (!att) continue;\n \n // Check if it's an image\n const mediaType = att.mediaType || att.type || '';\n const attType = att.type || '';\n \n if (mediaType.startsWith('image/') || attType === 'Image') {\n const imageUrl = att.url || att.href;\n if (imageUrl && images.length < 4) {\n images.push(imageUrl);\n }\n }\n }\n \n // Format for Twitter (280 char limit)\n const maxContentLength = 280;\n let tweetText = content;\n \n if (content.length > maxContentLength) {\n tweetText = content.substring(0, maxContentLength - 3) + '...';\n }\n \n newPosts.push({\n json: {\n postId: postId,\n tweetText: tweetText,\n originalContent: content,\n url: url,\n published: published,\n images: images,\n hasImages: images.length > 0,\n imageUrl: images[0] || null\n }\n });\n}\n\n// If no new posts, return empty to stop the workflow\nif (newPosts.length === 0) {\n return [];\n}\n\nreturn newPosts;" | |
| }, | |
| "id": "e16d5782-eaae-42e7-997f-45c831f0b724", | |
| "name": "Process New Activities", | |
| "type": "n8n-nodes-base.code", | |
| "typeVersion": 2, | |
| "position": [ | |
| -960, | |
| 896 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "conditions": { | |
| "options": { | |
| "caseSensitive": true, | |
| "leftValue": "", | |
| "typeValidation": "strict" | |
| }, | |
| "conditions": [ | |
| { | |
| "id": "has-images", | |
| "leftValue": "={{ $json.hasImages }}", | |
| "rightValue": true, | |
| "operator": { | |
| "type": "boolean", | |
| "operation": "equals" | |
| } | |
| } | |
| ], | |
| "combinator": "and" | |
| }, | |
| "options": {} | |
| }, | |
| "id": "61379fdb-a5aa-4517-9cee-4199d8c877df", | |
| "name": "Has Images?", | |
| "type": "n8n-nodes-base.if", | |
| "typeVersion": 2, | |
| "position": [ | |
| -736, | |
| 896 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "url": "={{ $json.imageUrl }}", | |
| "options": { | |
| "response": { | |
| "response": { | |
| "responseFormat": "file" | |
| } | |
| } | |
| } | |
| }, | |
| "id": "abbf4d38-1a87-4520-9e30-dffff8171cd8", | |
| "name": "Download Image", | |
| "type": "n8n-nodes-base.httpRequest", | |
| "typeVersion": 4.2, | |
| "position": [ | |
| -512, | |
| 736 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "method": "POST", | |
| "url": "https://upload.twitter.com/1.1/media/upload.json", | |
| "authentication": "predefinedCredentialType", | |
| "nodeCredentialType": "twitterOAuth1Api", | |
| "sendBody": true, | |
| "contentType": "multipart-form-data", | |
| "bodyParameters": { | |
| "parameters": [ | |
| { | |
| "parameterType": "formBinaryData", | |
| "name": "media", | |
| "inputDataFieldName": "data" | |
| } | |
| ] | |
| }, | |
| "options": {} | |
| }, | |
| "id": "bc3f3ccd-8c0a-4cde-9d5b-b777dd1f14d1", | |
| "name": "Upload Image to Twitter", | |
| "type": "n8n-nodes-base.httpRequest", | |
| "typeVersion": 4.2, | |
| "position": [ | |
| -304, | |
| 736 | |
| ], | |
| "credentials": { | |
| "twitterOAuth1Api": { | |
| "id": "YOUR_CREDENTIAL_ID", | |
| "name": "Twitter v1 API" | |
| } | |
| } | |
| }, | |
| { | |
| "parameters": { | |
| "text": "={{ $json.tweetText }}", | |
| "additionalFields": {} | |
| }, | |
| "id": "126b3462-9ff0-4e3b-81d0-20b8e41cec84", | |
| "name": "Post Tweet (No Media)", | |
| "type": "n8n-nodes-base.twitter", | |
| "typeVersion": 2, | |
| "position": [ | |
| -512, | |
| 1040 | |
| ], | |
| "credentials": { | |
| "twitterOAuth2Api": { | |
| "id": "YOUR_CREDENTIAL_ID", | |
| "name": "Twitter v2 API" | |
| } | |
| } | |
| }, | |
| { | |
| "parameters": { | |
| "jsCode": "// Mark this post as published so we don't repost it\nconst staticData = $getWorkflowStaticData('global');\nif (!staticData.postedIds) {\n staticData.postedIds = [];\n}\n\n// Get postId from whichever branch we came from\nlet postId;\ntry {\n postId = $('Process New Activities').item.json.postId;\n} catch (e) {\n postId = $input.first().json.postId;\n}\n\nstaticData.postedIds.push(postId);\n\n// Keep only the last 1000 IDs to prevent unbounded growth\nif (staticData.postedIds.length > 1000) {\n staticData.postedIds = staticData.postedIds.slice(-1000);\n}\n\nreturn $input.all();" | |
| }, | |
| "id": "2367774a-39a4-4590-8259-aea48cd2ebb5", | |
| "name": "Mark as Posted", | |
| "type": "n8n-nodes-base.code", | |
| "typeVersion": 2, | |
| "position": [ | |
| 144, | |
| 896 | |
| ] | |
| }, | |
| { | |
| "parameters": { | |
| "method": "POST", | |
| "url": "https://api.twitter.com/2/tweets", | |
| "authentication": "predefinedCredentialType", | |
| "nodeCredentialType": "twitterOAuth1Api", | |
| "sendBody": true, | |
| "bodyParameters": { | |
| "parameters": [ | |
| { | |
| "name": "text", | |
| "value": "={{ $('Process New Activities').item.json.tweetText }}" | |
| }, | |
| { | |
| "name": "media", | |
| "value": "={{ { media_ids: [$json.media_id_string] } }}" | |
| } | |
| ] | |
| }, | |
| "options": {} | |
| }, | |
| "type": "n8n-nodes-base.httpRequest", | |
| "typeVersion": 4.3, | |
| "position": [ | |
| -96, | |
| 736 | |
| ], | |
| "id": "3613ca84-cf32-4c97-8fa6-a844c572505a", | |
| "name": "Post Tweet", | |
| "credentials": { | |
| "twitterOAuth1Api": { | |
| "id": "TWITTER_CREDS", | |
| "name": "Twitter v1 API" | |
| } | |
| } | |
| } | |
| ], | |
| "pinData": {}, | |
| "connections": { | |
| "Every 5 Minutes": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Fetch Outbox", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Fetch Outbox": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Fetch First Page", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Fetch First Page": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Process New Activities", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Process New Activities": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Has Images?", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Has Images?": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Download Image", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ], | |
| [ | |
| { | |
| "node": "Post Tweet (No Media)", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Download Image": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Upload Image to Twitter", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Upload Image to Twitter": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Post Tweet", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Post Tweet (No Media)": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Mark as Posted", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Mark as Posted": { | |
| "main": [ | |
| [] | |
| ] | |
| }, | |
| "Post Tweet": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Mark as Posted", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| } | |
| }, | |
| "active": true, | |
| "settings": { | |
| "executionOrder": "v1", | |
| "callerPolicy": "workflowsFromSameOwner", | |
| "executionTimeout": -1, | |
| "availableInMCP": true | |
| }, | |
| "versionId": "62759a1b-0bd6-4c1e-9878-a1eb931ef908", | |
| "meta": { | |
| "templateCredsSetupCompleted": true, | |
| "instanceId": "" | |
| }, | |
| "id": "", | |
| "tags": [] | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment