# Content publish — n8n `publish-content` workflow (Phase 8c) The panel generates + reviews drafts; **n8n publishes them**. The panel POSTs an approved draft to a single n8n webhook and waits for a synchronous response. n8n routes by `channel` (LinkedIn = native node, blog = HTTP to sase.tr API). Synchronous by design: `sp.semih.ai` is Tailscale-only, so we avoid an n8n→panel callback. n8n returns the result via a **Respond to Webhook** node. ## Contract **Panel → n8n** (`POST` to `N8N_PUBLISH_WEBHOOK_URL`) Headers: `Content-Type: application/json`, `X-Content-Secret: ` ```jsonc { "draftId": "clx…", "channel": "linkedin", // linkedin | blog | x | instagram "projectKey": "sase", "topicTitle": "Şase Numarası (VIN) Nedir?", "content": { // effective body (founder edits merged over generated) // linkedin: { body, hashtags[], cta? } // blog: { title, slug, meta_description, body_markdown, tags[], cta? } // x: { tweets[], hashtags[] } // instagram:{ caption, hashtags[], image_prompt? } } } ``` **n8n → panel** (Respond to Webhook, synchronous) ```jsonc { "ok": true, "publishedUrl": "https://www.linkedin.com/feed/update/urn:li:share:123" } // or { "ok": false, "error": "linkedin 401: token expired" } ``` The panel accepts `ok|success` true, and reads the URL from any of `publishedUrl|url|postUrl|permalink`. A 2xx with no JSON is treated as success without a URL. ## Panel env (set on panel-web in Coolify) | key | value | |-----|-------| | `N8N_PUBLISH_WEBHOOK_URL` | `https://n8n.semih.ai/webhook/publish-content` (prod) — use `/webhook-test/publish-content` while building | | `N8N_WEBHOOK_SECRET` | a long random string; also set as the n8n Header Auth credential value | | `N8N_PUBLISH_TIMEOUT_MS` | optional, default `30000` | After setting these, redeploy panel-web (push to main auto-deploys web). ## Build the workflow in n8n Import `publish-content.workflow.json` (Workflows → Import from File) **or** build these 5 nodes: 1. **Webhook** — `POST`, path `publish-content`, **Respond** = "Using Respond to Webhook node". Authentication = **Header Auth** → credential checking header `X-Content-Secret` equals `N8N_WEBHOOK_SECRET`. 2. **Switch** (on `={{ $json.body.channel }}`): route `linkedin` and `blog` (add `x` / `instagram` later). Add a fallback output → error response. 3. **LinkedIn** node (`linkedin` branch) — see OAuth setup below. Post text: `={{ $json.body.content.body }}{{ $json.body.content.hashtags ? '\n\n' + $json.body.content.hashtags.join(' ') : '' }}` 4. **HTTP Request** (`blog` branch) — `POST {SASE_BLOG_API}/blog/posts/internal`, Header Auth **`Authorization: Bearer `** (mirrors sase.tr's existing `changelog/internal` automation pattern), JSON body = `={{ $json.body.content }}` plus `{ "projectKey": "sase" }`. 5. **Respond to Webhook** (one per branch, or a shared Set→Respond) — return `{ "ok": true, "publishedUrl": "" }`. On the fallback / error path return `{ "ok": false, "error": "" }`. Map `publishedUrl` from each node's response: - LinkedIn node returns the share/ugcPost id → build `https://www.linkedin.com/feed/update/`. - Blog HTTP returns `{ url }` from the sase.tr API (see below). Activate the workflow (toggle top-right) to use the `/webhook/` (prod) path. ## LinkedIn OAuth (n8n credential) 1. **LinkedIn Developer** (https://www.linkedin.com/developers/) → Create app, associate it with the **company page** you post from. 2. Products: request **"Share on LinkedIn"** and **"Advertising API"** / **"Community Management API"** as needed for organization posting. Member posting uses `w_member_social`; company-page posting uses `w_organization_social` (needs page admin + may need app review). 3. Auth tab → add redirect URL: `https://n8n.semih.ai/rest/oauth2-credential/callback`. 4. In n8n → Credentials → **LinkedIn OAuth2 API** → paste Client ID/Secret, set scopes (`w_member_social` and/or `w_organization_social r_organization_social`), connect, authorize. 5. In the LinkedIn node pick "Post" and, for a company page, set `Post As = Organization` + the organization URN. > Note: organization posting often requires LinkedIn app review. Start with > member posting (`w_member_social`) to validate end-to-end, then upgrade. ## Blog (sase.tr API) — see the sase.tr repo The `blog` branch POSTs to the new sase.tr blog API (`POST /blog/posts/internal`, header `Authorization: Bearer ` — mirrors sase.tr's existing `changelog/internal` automation auth). The Drizzle model/endpoint live in the sase.tr codebase (`apps/api/src/blog`, modeled on the `changelog` module). The API returns `{ url: "https://sase.tr/blog/" }`, which n8n echoes back as `publishedUrl`. ## Testing end-to-end 1. Set the panel envs to the **test** webhook URL, click "Listen for test event" in the n8n Webhook node. 2. In the panel: approve a draft → **Yayınla**. Watch n8n execute; the panel draft flips to `published` (with URL) or `failed` (with the error). 3. Switch the env to the prod `/webhook/` URL and **activate** the workflow.