Approved drafts publish via a single n8n `publish-content` webhook; the panel POSTs the effective content (founder edits merged over generated body) and awaits a synchronous Respond-to-Webhook result (sp.semih.ai is Tailscale-only, so we avoid an n8n→panel callback). n8n routes by channel. - lib/n8n.ts: publishToN8n client (X-Content-Secret header, timeout, tolerant result parsing: ok|success + publishedUrl|url|postUrl|permalink) - publishDraft server action: approved|failed → publishing → published(+url) / failed(+error), audit-logged; effective content = bodyJson + founderEdits - DraftCard: "Yayınla" / "Yeniden yayınla" button + publishing state - docs/n8n: importable publish-content workflow (Webhook → Switch → LinkedIn / HTTP-blog → Respond) + runbook (contract, panel envs, LinkedIn OAuth setup, blog endpoint = sase.tr POST /blog/posts/internal Bearer) Needs panel-web envs N8N_PUBLISH_WEBHOOK_URL + N8N_WEBHOOK_SECRET. Publish is a graceful no-op (clear error) until those are set and the n8n workflow exists. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
161 lines
5.1 KiB
JSON
161 lines
5.1 KiB
JSON
{
|
|
"name": "publish-content",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"httpMethod": "POST",
|
|
"path": "publish-content",
|
|
"responseMode": "responseNode",
|
|
"authentication": "headerAuth",
|
|
"options": {}
|
|
},
|
|
"id": "webhook-in",
|
|
"name": "Webhook (publish-content)",
|
|
"type": "n8n-nodes-base.webhook",
|
|
"typeVersion": 2,
|
|
"position": [-200, 300],
|
|
"webhookId": "publish-content",
|
|
"credentials": {
|
|
"httpHeaderAuth": { "id": "REPLACE", "name": "X-Content-Secret" }
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"conditions": {
|
|
"options": { "caseSensitive": true, "typeValidation": "strict" },
|
|
"conditions": [
|
|
{
|
|
"id": "r-linkedin",
|
|
"leftValue": "={{ $json.body.channel }}",
|
|
"rightValue": "linkedin",
|
|
"operator": { "type": "string", "operation": "equals" }
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "linkedin"
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": { "caseSensitive": true, "typeValidation": "strict" },
|
|
"conditions": [
|
|
{
|
|
"id": "r-blog",
|
|
"leftValue": "={{ $json.body.channel }}",
|
|
"rightValue": "blog",
|
|
"operator": { "type": "string", "operation": "equals" }
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "blog"
|
|
}
|
|
]
|
|
},
|
|
"options": { "fallbackOutput": "extra" }
|
|
},
|
|
"id": "switch-channel",
|
|
"name": "Switch by channel",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3,
|
|
"position": [40, 300]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"postAs": "person",
|
|
"text": "={{ $('Webhook (publish-content)').item.json.body.content.body }}{{ $('Webhook (publish-content)').item.json.body.content.hashtags ? '\\n\\n' + $('Webhook (publish-content)').item.json.body.content.hashtags.join(' ') : '' }}",
|
|
"additionalFields": {}
|
|
},
|
|
"id": "linkedin-post",
|
|
"name": "LinkedIn — create post",
|
|
"type": "n8n-nodes-base.linkedIn",
|
|
"typeVersion": 1,
|
|
"position": [300, 180],
|
|
"credentials": {
|
|
"linkedInOAuth2Api": { "id": "REPLACE", "name": "LinkedIn OAuth2" }
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "={{ $env.SASE_BLOG_API }}/blog/posts/internal",
|
|
"authentication": "genericCredentialType",
|
|
"genericAuthType": "httpHeaderAuth",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ { ...$('Webhook (publish-content)').item.json.body.content, projectKey: 'sase' } }}",
|
|
"options": {}
|
|
},
|
|
"id": "blog-post",
|
|
"name": "Blog — POST sase.tr",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [300, 360],
|
|
"credentials": {
|
|
"httpHeaderAuth": { "id": "REPLACE", "name": "SASE_BLOG_TOKEN (Bearer)" }
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"respondWith": "json",
|
|
"responseBody": "={{ { ok: true, publishedUrl: ($json.permalink || $json.urn || $json.id || '') } }}",
|
|
"options": {}
|
|
},
|
|
"id": "respond-linkedin",
|
|
"name": "Respond — LinkedIn",
|
|
"type": "n8n-nodes-base.respondToWebhook",
|
|
"typeVersion": 1,
|
|
"position": [560, 180]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"respondWith": "json",
|
|
"responseBody": "={{ { ok: true, publishedUrl: ($json.url || $json.permalink || '') } }}",
|
|
"options": {}
|
|
},
|
|
"id": "respond-blog",
|
|
"name": "Respond — Blog",
|
|
"type": "n8n-nodes-base.respondToWebhook",
|
|
"typeVersion": 1,
|
|
"position": [560, 360]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"respondWith": "json",
|
|
"responseBody": "={{ { ok: false, error: 'unsupported channel: ' + $('Webhook (publish-content)').item.json.body.channel } }}",
|
|
"options": { "responseCode": 422 }
|
|
},
|
|
"id": "respond-fallback",
|
|
"name": "Respond — Unsupported",
|
|
"type": "n8n-nodes-base.respondToWebhook",
|
|
"typeVersion": 1,
|
|
"position": [300, 540]
|
|
}
|
|
],
|
|
"connections": {
|
|
"Webhook (publish-content)": {
|
|
"main": [[{ "node": "Switch by channel", "type": "main", "index": 0 }]]
|
|
},
|
|
"Switch by channel": {
|
|
"main": [
|
|
[{ "node": "LinkedIn — create post", "type": "main", "index": 0 }],
|
|
[{ "node": "Blog — POST sase.tr", "type": "main", "index": 0 }],
|
|
[{ "node": "Respond — Unsupported", "type": "main", "index": 0 }]
|
|
]
|
|
},
|
|
"LinkedIn — create post": {
|
|
"main": [[{ "node": "Respond — LinkedIn", "type": "main", "index": 0 }]]
|
|
},
|
|
"Blog — POST sase.tr": {
|
|
"main": [[{ "node": "Respond — Blog", "type": "main", "index": 0 }]]
|
|
}
|
|
},
|
|
"settings": { "executionOrder": "v1" },
|
|
"active": false
|
|
}
|