{
  "name": "The Lantern Daily — Master Newsroom Production Pipeline",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 2 * * *"
            }
          ]
        }
      },
      "id": "w1-schedule-trigger",
      "name": "W1: Daily 2:00 AM Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [200, 300]
    },
    {
      "parameters": {
        "jsCode": "const FEEDS = [\n  // TIER 1: TECH (Leads the Daily)\n  { name: 'TechCrunch', tier: 'tech', url: 'https://techcrunch.com/feed/' },\n  { name: 'The Verge', tier: 'tech', url: 'https://www.theverge.com/rss/index.xml' },\n  { name: 'Wired', tier: 'tech', url: 'https://www.wired.com/feed/rss' },\n  { name: 'Ars Technica', tier: 'tech', url: 'https://feeds.arstechnica.com/arstechnica/index' },\n  { name: 'MIT Tech Review', tier: 'tech', url: 'https://www.technologyreview.com/topnews.rss' },\n  { name: 'The Information', tier: 'tech', url: 'https://www.theinformation.com/feed' },\n  { name: 'Hacker News', tier: 'tech', url: 'https://hnrss.org/frontpage' },\n  { name: 'CNBC Tech', tier: 'tech', url: 'https://www.cnbc.com/id/19854910/device/rss/rss.html' },\n  { name: 'Bloomberg Tech', tier: 'tech', url: 'https://feeds.bloomberg.com/technology/news.rss' },\n  // TIER 2: FINANCE & MARKETS\n  { name: 'Yahoo Finance', tier: 'finance', url: 'https://finance.yahoo.com/news/rssindex' },\n  { name: 'Fox Business', tier: 'finance', url: 'https://feeds.foxbusiness.com/foxbusiness/latest' },\n  { name: 'MarketWatch', tier: 'finance', url: 'https://feeds.marketwatch.com/marketwatch/topstories/' },\n  { name: 'CNBC Markets', tier: 'finance', url: 'https://www.cnbc.com/id/100003114/device/rss/rss.html' },\n  // TIER 3: WORLD\n  { name: 'BBC News', tier: 'world', url: 'http://feeds.bbci.co.uk/news/rss.xml' },\n  { name: 'The Guardian', tier: 'world', url: 'https://www.theguardian.com/world/rss' },\n  { name: 'CNN Top Stories', tier: 'world', url: 'http://rss.cnn.com/rss/cnn_topstories.rss' },\n  { name: 'Fox News', tier: 'world', url: 'https://moxie.foxnews.com/google-publisher/latest.xml' }\n];\n\nreturn FEEDS.map(feed => ({ json: feed }));"
      },
      "id": "w1-feed-definitions",
      "name": "W1: Source Tier Registry",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [420, 300]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $json.url }}",
        "options": {
          "timeout": 15000
        }
      },
      "id": "w1-fetch-rss",
      "name": "W1: HTTP Fetch RSS XML",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [640, 300]
    },
    {
      "parameters": {
        "jsCode": "const xmlData = $input.first().json.data || $input.first().json.body || '';\nconst sourceName = $('W1: Source Tier Registry').item.json.name;\nconst sourceTier = $('W1: Source Tier Registry').item.json.tier;\n\n// Fast regex extraction for XML/Atom items\nconst itemRegex = /<item[\\s\\S]*?<\\/item>|<entry[\\s\\S]*?<\\/entry>/gi;\nconst matches = xmlData.match(itemRegex) || [];\n\nconst items = [];\nfor (const block of matches.slice(0, 15)) {\n  const titleMatch = block.match(/<title(?:[^>]*)>(?:<!\\[CDATA\\[)?([\\s\\S]*?)(?:\\]\\]>)?<\\/title>/i);\n  const linkMatch = block.match(/<link(?:[^>]*)>(?:<!\\[CDATA\\[)?([\\s\\S]*?)(?:\\]\\]>)?<\\/link>|<link[^>]*href=[\"']([^\"']+)[\"']/i);\n  const pubDateMatch = block.match(/<(?:pubDate|published|updated)>(.*?)<\\/(?:pubDate|published|updated)>/i);\n\n  const rawTitle = titleMatch ? titleMatch[1].replace(/<[^>]+>/g, '').trim() : '';\n  const rawUrl = linkMatch ? (linkMatch[1] || linkMatch[2] || '').trim() : '';\n\n  if (rawTitle && rawUrl) {\n    items.push({\n      json: {\n        source_name: sourceName,\n        source_tier: sourceTier,\n        headline: rawTitle,\n        source_url: rawUrl,\n        published_at: pubDateMatch ? pubDateMatch[1] : new Date().toISOString()\n      }\n    });\n  }\n}\n\nreturn items;"
      },
      "id": "w1-parse-rss",
      "name": "W1: Normalize XML Items",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [860, 300]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.SUPABASE_URL }}/rest/v1/lantern_signals",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Prefer",
              "value": "resolution=ignore-duplicates"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n  source_name: $json.source_name,\n  source_tier: $json.source_tier,\n  headline: $json.headline,\n  source_url: $json.source_url,\n  published_at: $json.published_at\n}) }}"
      },
      "id": "w1-insert-signals",
      "name": "W1: Supabase Ingest Signals",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1080, 300]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "30 2 * * *"
            }
          ]
        }
      },
      "id": "w2-schedule-trigger",
      "name": "W2: Daily 2:30 AM Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [200, 600]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $env.SUPABASE_URL }}/rest/v1/lantern_signals?select=id,source_name,source_tier,headline,source_url,published_at&order=ingested_at.desc&limit=150",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            }
          ]
        }
      },
      "id": "w2-get-signals",
      "name": "W2: Query Signals Last 24H",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [420, 600]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "x-goog-api-key",
              "value": "={{ $env.GEMINI_API_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"contents\": [{\n    \"parts\": [{\n      \"text\": \"You are the Chief Intelligence Aggregator for The Lantern Daily.\\nCluster these news headlines by real-world event. ONLY return stories covered by 2 or more distinct media sources.\\n\\nHEADLINES:\\n\" + JSON.stringify($json) + \"\\n\\nReturn strict JSON format with an array of objects:\\n[{\\n  \\\"headline\\\": \\\"Normalized punchy headline\\\",\\n  \\\"section\\\": \\\"tech\\\" | \\\"finance\\\" | \\\"world\\\",\\n  \\\"outlet_count\\\": integer,\\n  \\\"outlets\\\": [\\\"TechCrunch\\\", \\\"CNBC\\\"],\\n  \\\"source_url\\\": \\\"primary authoritative article link\\\",\\n  \\\"summary\\\": \\\"2 clean factual sentences\\\"\\n}]\"\n    }]\n  }],\n  \"generationConfig\": {\n    \"responseMimeType\": \"application/json\",\n    \"temperature\": 0.2\n  }\n}"
      },
      "id": "w2-gemini-cluster",
      "name": "W2: Gemini Cross-Pub Cluster",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [640, 600]
    },
    {
      "parameters": {
        "jsCode": "const response = $input.first().json;\nlet rawText = '';\ntry {\n  rawText = response.candidates[0].content.parts[0].text;\n  const clusters = JSON.parse(rawText);\n  \n  // Filter 2+ outlets, sort by outlet_count desc, take top 5\n  const qualified = clusters\n    .filter(c => c.outlet_count >= 2)\n    .sort((a, b) => b.outlet_count - a.outlet_count)\n    .slice(0, 5);\n    \n  return qualified.map(item => ({ json: item }));\n} catch (e) {\n  throw new Error('Failed to parse Gemini clustering response: ' + e.message + ' | Raw: ' + rawText);\n}"
      },
      "id": "w2-parse-clusters",
      "name": "W2: Rank & Take Top 5",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [860, 600]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.SUPABASE_URL }}/rest/v1/posts",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Prefer",
              "value": "return=representation"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n  title: $json.headline,\n  slug: $json.headline.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') + '-' + Date.now().toString().slice(-4),\n  content: $json.summary,\n  excerpt: $json.summary,\n  section: $json.section,\n  outlet_count: $json.outlet_count,\n  outlets: $json.outlets,\n  source_url: $json.source_url,\n  status: 'draft',\n  content_type: 'issue'\n}) }}"
      },
      "id": "w2-save-draft-posts",
      "name": "W2: Supabase Save Draft Posts",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1080, 600]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 3 * * *"
            }
          ]
        }
      },
      "id": "w3-schedule-trigger",
      "name": "W3: Daily 3:00 AM Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [200, 900]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $env.SUPABASE_URL }}/rest/v1/posts?status=eq.draft&edition_date=eq.today&select=*",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            }
          ]
        }
      },
      "id": "w3-get-drafts",
      "name": "W3: Fetch Today's Drafts",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [420, 900]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "x-goog-api-key",
              "value": "={{ $env.GEMINI_API_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"contents\": [{\n    \"parts\": [{\n      \"text\": \"You are the senior editorial voice of The Lantern Daily.\\nTone: White-glove, calm elder who knows Islam and the modern economy. He speaks like a wise, trusted older brother. Never demanding, never lecturing, never issuing fatwas, never creating panic.\\n\\nProduce the Islamic Lens editorial for this story using the invisible 3-beat structure:\\n- Beat 1: Ground (what happened in plain terms)\\n- Beat 2: Reflect (what it touches in the reader's personal deen, wealth, or the Ummah)\\n- Beat 3: Soft Islamic anchor (Quran or Sahih Hadith, ONLY if genuinely relevant, with full attribution)\\n\\nSTORY:\\nTitle: \" + $json.title + \"\\nSummary: \" + $json.excerpt + \"\\nSection: \" + $json.section + \"\\n\\nReturn JSON:\\n{\\n  \\\"halal_stamp\\\": \\\"halal\\\" | \\\"nuanced\\\" | \\\"concern\\\",\\n  \\\"islamic_lens\\\": \\\"One or two flowing paragraphs hitting the 3 beats invisibly.\\\",\\n  \\\"pull_quote_text\\\": \\\"Arabic/English scripture quote or null if not earned\\\",\\n  \\\"pull_quote_source\\\": \\\"e.g., Sunan Ibn Majah 2341 · Sahih or null\\\",\\n  \\\"pull_quote_narrator\\\": \\\"e.g., Prophet Muhammad ﷺ or null\\\"\\n}\"\n    }]\n  }],\n  \"generationConfig\": {\n    \"responseMimeType\": \"application/json\",\n    \"temperature\": 0.3\n  }\n}"
      },
      "id": "w3-generate-lens",
      "name": "W3: AI Editor 3-Beat Lens",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [640, 900]
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "={{ $env.SUPABASE_URL }}/rest/v1/posts?id=eq.{{ $('W3: Fetch Today\\'s Drafts').item.json.id }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ \n  (() => {\n    const parsed = JSON.parse($json.candidates[0].content.parts[0].text);\n    return JSON.stringify({\n      halal_stance: parsed.halal_stamp,\n      editorial_note: parsed.islamic_lens,\n      pull_quote_text: parsed.pull_quote_text,\n      pull_quote_source: parsed.pull_quote_source,\n      pull_quote_narrator: parsed.pull_quote_narrator,\n      status: 'ready'\n    });\n  })()\n}}"
      },
      "id": "w3-update-post-ready",
      "name": "W3: Update Post Ready State",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [860, 900]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 6 * * *"
            }
          ]
        }
      },
      "id": "w5-schedule-trigger",
      "name": "W5: Daily 6:00 AM Publisher",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [200, 1200]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $env.SUPABASE_URL }}/rest/v1/posts?status=eq.ready&edition_date=eq.today&order=outlet_count.desc&limit=5",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            }
          ]
        }
      },
      "id": "w5-get-ready-posts",
      "name": "W5: Fetch Today's 5 Ready Posts",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [420, 1200]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.beehiiv.com/v2/publications/{{ $env.BEEHIIV_PUBLICATION_ID }}/posts",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.BEEHIIV_API_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ \n  (() => {\n    const posts = $json;\n    const dateStr = new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });\n    \n    let htmlBody = `<p style=\"color: #B8922A; font-weight: bold;\">بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ</p><p>The morning briefing for ${dateStr}. 5 stories moving the world, seen through the lens of clarity and sovereignty.</p><hr/>`;\n    \n    for (const post of posts) {\n      htmlBody += `<h3>${post.title}</h3>`;\n      htmlBody += `<p><em>Seen on: ${post.outlets ? post.outlets.join(' · ') : 'Major Outlets'}</em></p>`;\n      htmlBody += `<p>${post.excerpt}</p>`;\n      htmlBody += `<div style=\"border-left: 3px solid #B8922A; padding-left: 12px; margin: 16px 0;\"><p><strong>Islamic Lens:</strong></p><p>${post.editorial_note || ''}</p></div>`;\n      if (post.pull_quote_text) {\n        htmlBody += `<blockquote style=\"background: #0D0F14; border: 1px solid #2A2D35; border-left: 4px solid #B8922A; padding: 12px; font-style: italic;\">\"${post.pull_quote_text}\"<br/><span style=\"color: #B8922A; font-size: 0.85em;\">— ${post.pull_quote_narrator || ''} (${post.pull_quote_source || ''})</span></blockquote>`;\n      }\n      htmlBody += `<p><a href=\"${post.source_url}\">Read primary source →</a></p><hr/>`;\n    }\n    \n    return JSON.stringify({\n      title: `The Lantern Daily — ${dateStr}`,\n      subtitle: '5 stories moving the world through the Islamic lens',\n      content_html: htmlBody,\n      status: 'draft',\n      email_subject_line: `The Lantern Daily: ${posts[0]?.title || 'Morning Intelligence'}`\n    });\n  })()\n}}"
      },
      "id": "w5-publish-beehiiv",
      "name": "W5: Beehiiv Create Morning Edition",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [640, 1200]
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "={{ $env.SUPABASE_URL }}/rest/v1/posts?status=eq.ready&edition_date=eq.today",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.SUPABASE_SERVICE_ROLE_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\"status\": \"published\"}"
      },
      "id": "w5-mark-published",
      "name": "W5: Mark Posts Published",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [860, 1200]
    }
  ],
  "connections": {
    "W1: Daily 2:00 AM Trigger": {
      "main": [[{ "node": "W1: Source Tier Registry", "type": "main", "index": 0 }]]
    },
    "W1: Source Tier Registry": {
      "main": [[{ "node": "W1: HTTP Fetch RSS XML", "type": "main", "index": 0 }]]
    },
    "W1: HTTP Fetch RSS XML": {
      "main": [[{ "node": "W1: Normalize XML Items", "type": "main", "index": 0 }]]
    },
    "W1: Normalize XML Items": {
      "main": [[{ "node": "W1: Supabase Ingest Signals", "type": "main", "index": 0 }]]
    },
    "W2: Daily 2:30 AM Trigger": {
      "main": [[{ "node": "W2: Query Signals Last 24H", "type": "main", "index": 0 }]]
    },
    "W2: Query Signals Last 24H": {
      "main": [[{ "node": "W2: Gemini Cross-Pub Cluster", "type": "main", "index": 0 }]]
    },
    "W2: Gemini Cross-Pub Cluster": {
      "main": [[{ "node": "W2: Rank & Take Top 5", "type": "main", "index": 0 }]]
    },
    "W2: Rank & Take Top 5": {
      "main": [[{ "node": "W2: Supabase Save Draft Posts", "type": "main", "index": 0 }]]
    },
    "W3: Daily 3:00 AM Trigger": {
      "main": [[{ "node": "W3: Fetch Today's Drafts", "type": "main", "index": 0 }]]
    },
    "W3: Fetch Today's Drafts": {
      "main": [[{ "node": "W3: AI Editor 3-Beat Lens", "type": "main", "index": 0 }]]
    },
    "W3: AI Editor 3-Beat Lens": {
      "main": [[{ "node": "W3: Update Post Ready State", "type": "main", "index": 0 }]]
    },
    "W5: Daily 6:00 AM Publisher": {
      "main": [[{ "node": "W5: Fetch Today's 5 Ready Posts", "type": "main", "index": 0 }]]
    },
    "W5: Fetch Today's 5 Ready Posts": {
      "main": [[{ "node": "W5: Beehiiv Create Morning Edition", "type": "main", "index": 0 }]]
    },
    "W5: Beehiiv Create Morning Edition": {
      "main": [[{ "node": "W5: Mark Posts Published", "type": "main", "index": 0 }]]
    }
  }
}
