{
  "nodes": [
    {
      "id": "2cc333b3-d32b-4223-bac0-9c17ade76528",
      "name": "Done",
      "type": "n8n-nodes-base.noOp",
      "position": [
        1616,
        208
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "52626e91-8c78-4e20-9397-2068f67c5559",
      "name": "Read Input URLs (Google Sheets)",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1168,
        400
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1quBpAwPIcUzjPERE2icKM-bhKnoe7cJvz1-UK81aKVE/edit#gid=0",
          "cachedResultName": "Input"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.sheet_id }}"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "776e7963-9169-4462-a9b7-f289b2196d71",
      "name": "Loop URLs (Split in Batches)",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1392,
        400
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "6628ae2f-d52a-4050-a734-a75707400c27",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        0
      ],
      "parameters": {
        "width": 576,
        "height": 720,
        "content": "## Weekly SEO Watchlist Audit to Google Sheets (Gemini + Decodo)\n\nSign up for Decodo [HERE](https://visit.decodo.com/discount) for Discount\n\nAutomatically fetches page content, generates a compact SEO audit (score, issues, fixes), and writes both a per-URL summary and a normalized \u201cAll Issues\u201d table to Google Sheets\u2014great for weekly monitoring and prioritization.\n\n## Who\u2019s it for?\nContent/SEO teams that want lightweight, scheduled audits of key pages with actionable next steps and spreadsheet reporting.\n\n## How it works\n1. Weekly trigger loads the Google Sheet of URLs.\n2. Split in Batches processes each URL.\n3. Decodo fetches page content (markdown + status).\n4. Gemini produces a strict JSON audit via the AI Chain + Output Parser.\n5. Code nodes flatten data for two tabs.\n6. Google Sheets nodes append Summary and All Issues rows.\n7. Split in Batches continues to the next URL.\n\n## How to set up\n- Add credentials for Google Sheets, [Decodo](https://visit.decodo.com/discount), and Gemini.\n- Set `sheet_id` and Sheet GIDs in the Set node.\n- Ensure input sheet has a `URL` column.\n- Configure your Google Sheets tabs with proper headers matching each field being appended (e.g., URL, Decodo Score, Priority, etc.).\n- Adjust schedule as needed.\n- Activate the workflow.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "7036150a-964b-4315-a41d-5f557b18b196",
      "name": "AI Model: Google Gemini",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        1856,
        624
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "8a1f65f0-fcd4-472c-adae-96ce769ba991",
      "name": "Output Parser: Enforce SEO JSON",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        1984,
        624
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"output\": [\n    {\n      \"url\": \"string\",\n      \"decodo_score\": 0,\n      \"top_issues\": \"string\",\n      \"priority\": \"High|Medium|Low\",\n      \"recommended_actions\": [\"string\"],\n      \"suggested_meta\": \"string\",\n      \"suggested_title\": \"string\",\n      \"status\": \"ok|no_content|error\",\n      \"raw_decodo\": {},\n      \"last_checked\": \"string\",\n      \"all_issues\": [\n        {\n          \"id\": \"string\",\n          \"title\": \"string\",\n          \"category\": \"On-Page|Technical|Content|Performance|Accessibility|Indexing|Internal Linking|Other\",\n          \"severity\": \"High|Medium|Low\",\n          \"evidence\": \"string\",\n          \"recommendation\": \"string\",\n          \"impact\": \"High|Medium|Low\",\n          \"effort\": \"Low|Medium|High\",\n          \"score_delta\": -1,\n          \"examples\": [\"string\"]\n        }\n      ]\n    }\n  ]\n}\n"
      },
      "typeVersion": 1.3
    },
    {
      "id": "84f5205f-945e-459c-b45c-4717ba9e54f0",
      "name": "Trigger: Weekly",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        720,
        400
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "899ea14f-7813-4689-8438-1d5ae259323f",
      "name": "Decodo: Fetch Page Content",
      "type": "@decodo/n8n-nodes-decodo.decodo",
      "onError": "continueRegularOutput",
      "position": [
        1616,
        400
      ],
      "parameters": {
        "url": "={{ $json.URL_list }}"
      },
      "retryOnFail": true,
      "typeVersion": 1
    },
    {
      "id": "6cf87396-1e7e-4df3-a9f0-7b660bdde5fc",
      "name": "Generate SEO Audit",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        1840,
        400
      ],
      "parameters": {
        "text": "=System:\nYou are an SEO Watchlist Agent.\nReturn only a valid JSON object and nothing else.\nOutput MUST be exactly this shape (no extra keys, no markdown):\n{\"output\":[{...}]}\nDo not return plain objects, arrays, strings, or explanations.\nAll numbers must be numeric (not strings).\nIf a field has no data, use null (except raw_decodo must be {}).\n\nUser:\nAnalyze the following webpage content (markdown) and produce an SEO audit JSON object.\n\nINPUT\n- url_hint: {{ $('Loop URLs (Split in Batches)').item.json.URL_list }}\n- status_code: {{ $json.data.results[0].status_code }}\n- fetched_at: {{ $json.data.results[0].updated_at }}\n- content:\n{{ $json.data.results[0].content }}\n\nREQUIREMENTS\nReturn only this JSON object (with the \"output\" wrapper and a single-item array):\n\n{\n  \"output\": [\n    {\n      \"url\": \"string\",\n      \"decodo_score\": 0,\n      \"top_issues\": \"semicolon-separated list of 3\u20135 key issues\",\n      \"priority\": \"High|Medium|Low\",\n      \"recommended_actions\": [\"up to 10 short, actionable fixes\"],\n      \"suggested_meta\": \"120\u2013155 chars\",\n      \"suggested_title\": \"\u226470 chars\",\n      \"status\": \"ok|no_content|error\",\n      \"raw_decodo\": {},\n      \"last_checked\": \"UTC ISO 8601 with Z\",\n      \"all_issues\": [\n        {\n          \"id\": \"kebab-case identifier\",\n          \"title\": \"short issue name\",\n          \"category\": \"On-Page|Technical|Content|Performance|Accessibility|Indexing|Internal Linking|Other\",\n          \"severity\": \"High|Medium|Low\",\n          \"evidence\": \"\u2264240 chars concrete proof from the page text\",\n          \"recommendation\": \"how to fix\",\n          \"impact\": \"High|Medium|Low\",\n          \"effort\": \"Low|Medium|High\",\n          \"score_delta\": -1,\n          \"examples\": [\"example selector/url/snippet\"]\n        }\n      ]\n    }\n  ]\n}\n\nSCORING & RULES\n- \"url\": use url_hint if present; otherwise null.\n- \"status\": \"no_content\" if content is empty/null/whitespace; else \"ok\".\n- \"last_checked\": current UTC timestamp with Z.\n- \"decodo_score\": 0\u2013100 based on on-page basics + content quality.\n- Order \"all_issues\" High \u2192 Medium \u2192 Low; max 10 items.\n- Evidence must come only from provided content (\u2264240 chars).\n- If content empty: status=\"no_content\", priority=\"Low\", decodo_score=0, top_issues=\"No content detected\", recommended_actions=[], all_issues=[].\n\nSTRICT OUTPUT RULES\n- Return exactly one JSON object with a top-level \"output\" key whose value is an array with exactly one object.\n- Do not emit any keys other than \"output\".\n- Do not include code fences, prose, or additional arrays/objects.\n",
        "batching": {},
        "messages": {
          "messageValues": [
            {
              "message": "=You are an SEO Watchlist Agent.\nReturn only a valid JSON object that matches the specified schema.\nDo not include explanations, markdown, or arrays.\nAll numbers must be numeric, not strings."
            },
            {
              "type": "AIMessagePromptTemplate",
              "message": "={\n  \"output\": [\n    {\n      \"url\": \"string\",\n      \"decodo_score\": 0,\n      \"top_issues\": \"semicolon-separated list of 3\u20135 key issues\",\n      \"priority\": \"High|Medium|Low\",\n      \"recommended_actions\": [\"up to 10 short, actionable fixes\"],\n      \"suggested_meta\": \"120\u2013155 chars\",\n      \"suggested_title\": \"\u226470 chars\",\n      \"status\": \"ok|no_content|error\",\n      \"raw_decodo\": {},\n      \"last_checked\": \"UTC ISO 8601 with Z\",\n      \"all_issues\": [\n        {\n          \"id\": \"kebab-case identifier\",\n          \"title\": \"short issue name\",\n          \"category\": \"On-Page|Technical|Content|Performance|Accessibility|Indexing|Internal Linking|Other\",\n          \"severity\": \"High|Medium|Low\",\n          \"evidence\": \"\u2264240 chars concrete proof from the page text\",\n          \"recommendation\": \"how to fix\",\n          \"impact\": \"High|Medium|Low\",\n          \"effort\": \"Low|Medium|High\",\n          \"score_delta\": -1,\n          \"examples\": [\"example selector/url/snippet\"]\n        }\n      ]\n    }\n  ]\n}"
            }
          ]
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.7
    },
    {
      "id": "7e73282d-d648-4823-8379-2b9936e521e9",
      "name": "Transform: Summary Row",
      "type": "n8n-nodes-base.code",
      "position": [
        2192,
        304
      ],
      "parameters": {
        "jsCode": "const data = $json.output?.[0] ?? {};\nreturn [{\n  url: data.url,\n  decodo_score: data.decodo_score,\n  top_issues: data.top_issues,\n  priority: data.priority,\n  suggested_title: data.suggested_title,\n  suggested_meta: data.suggested_meta,\n  status: data.status,\n  last_checked: data.last_checked,\n  // optional: ubah jadi JSON string supaya tetap 1 kolom\n  recommended_actions: (data.recommended_actions || []).join('\\n'),\n  all_issues: JSON.stringify(data.all_issues || [])\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "04f7eac0-a0a0-4a5a-9335-ab104b771fea",
      "name": "Google Sheets: Append Summary",
      "type": "n8n-nodes-base.googleSheets",
      "onError": "continueRegularOutput",
      "position": [
        2416,
        304
      ],
      "parameters": {
        "columns": {
          "value": {
            "URL": "={{ $json.url }}",
            "Status": "={{ $json.status }}",
            "Priority": "={{ $json.priority }}",
            "Top Issues": "={{ $json.top_issues }}",
            "Decodo Score": "={{ $json.decodo_score }}",
            "Last Checked": "={{ $json.last_checked }}",
            "Suggested Meta": "={{ $json.suggested_meta }}",
            "Suggested Title": "={{ $json.suggested_title }}",
            "Recommended Actions": "={{ $json.recommended_actions }}"
          },
          "schema": [
            {
              "id": "URL",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "URL",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Decodo Score",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Decodo Score",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Top Issues",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Top Issues",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Priority",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Priority",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Suggested Title",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Suggested Title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Suggested Meta",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Suggested Meta",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Last Checked",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Last Checked",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Recommended Actions",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Recommended Actions",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 2024550449,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1quBpAwPIcUzjPERE2icKM-bhKnoe7cJvz1-UK81aKVE/edit#gid=2024550449",
          "cachedResultName": "Output"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set: Configure Workflow').item.json.sheet_id }}"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "7b6d68e7-4a04-415f-9676-f2a336106b33",
      "name": "Transform: Explode Issues",
      "type": "n8n-nodes-base.code",
      "position": [
        2192,
        496
      ],
      "parameters": {
        "jsCode": "const data = $json.output?.[0];\nif (!data || !Array.isArray(data.all_issues)) return [];\nreturn data.all_issues.map(issue => ({\n  url: data.url,\n  id: issue.id,\n  title: issue.title,\n  category: issue.category,\n  severity: issue.severity,\n  evidence: issue.evidence,\n  recommendation: issue.recommendation,\n  impact: issue.impact,\n  effort: issue.effort,\n  score_delta: issue.score_delta,\n  examples: (issue.examples || []).join(\"; \")\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "75d8de44-5812-4164-b3f7-f9c87eaa4cc7",
      "name": "Google Sheets: Append Issues",
      "type": "n8n-nodes-base.googleSheets",
      "onError": "continueRegularOutput",
      "position": [
        2416,
        496
      ],
      "parameters": {
        "columns": {
          "value": {
            "URL": "={{ $json.url }}",
            "Title": "={{ $json.title }}",
            "Effort": "={{ $json.effort }}",
            "Impact": "={{ $json.impact }}",
            "Category": "={{ $json.category }}",
            "Evidence": "={{ $json.evidence }}",
            "Examples": "={{ $json.examples }}",
            "Issue ID": "={{ $json.id }}",
            "Severity": "={{ $json.severity }}",
            "Score Delta": "={{ $json.score_delta }}",
            "Recommendation": "={{ $json.recommendation }}"
          },
          "schema": [
            {
              "id": "URL",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "URL",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Issue ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Issue ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Title",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Category",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Category",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Severity",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Severity",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Evidence",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Evidence",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Recommendation",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Recommendation",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Impact",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Impact",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Effort",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Effort",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Score Delta",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Score Delta",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Examples",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Examples",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1837304635,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1quBpAwPIcUzjPERE2icKM-bhKnoe7cJvz1-UK81aKVE/edit#gid=1837304635",
          "cachedResultName": "All Issues"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set: Configure Workflow').item.json.sheet_id }}"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "96ff7143-a01e-4197-b5f8-d896fee165c1",
      "name": "Join Summary + Issues",
      "type": "n8n-nodes-base.merge",
      "position": [
        2640,
        496
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "c706b3ad-70a0-49de-964b-71dbe46ae8db",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        672,
        288
      ],
      "parameters": {
        "color": 4,
        "width": 192,
        "height": 272,
        "content": "### Adjust interval to your reporting cadence (e.g., every Monday 09:00 UTC)."
      },
      "typeVersion": 1
    },
    {
      "id": "ec1bd973-3ef6-4b85-9a88-afac819da300",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        896,
        288
      ],
      "parameters": {
        "color": 4,
        "width": 192,
        "height": 272,
        "content": "### Enter sheet_id for Input, Output, and All Issues tabs."
      },
      "typeVersion": 1
    },
    {
      "id": "595917c9-2c82-44ec-92ff-a887eeda369d",
      "name": "Set: Configure Workflow",
      "type": "n8n-nodes-base.set",
      "position": [
        944,
        400
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "16ed2967-7c19-48ad-86c1-10367f650253",
              "name": "sheet_id",
              "type": "string",
              "value": "change me!"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "092e361c-d545-48e6-9f9a-01f72691654e",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1120,
        288
      ],
      "parameters": {
        "color": 4,
        "width": 192,
        "height": 288,
        "content": "### Make sure your Input tab contains a column named URL.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "be6e63cc-7d51-4c0e-a9ec-6bd590548d59",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2368,
        176
      ],
      "parameters": {
        "color": 4,
        "width": 192,
        "height": 480,
        "content": "### Ensure all columns match the expected field names in this node."
      },
      "typeVersion": 1
    },
    {
      "id": "ffdb95bd-f5a7-44fd-8cb6-2bd87454af03",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1312,
        -448
      ],
      "parameters": {
        "width": 992,
        "height": 576,
        "content": "@[youtube](Dcy5fOfB68Q)\n"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Trigger: Weekly": {
      "main": [
        [
          {
            "node": "Set: Configure Workflow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate SEO Audit": {
      "main": [
        [
          {
            "node": "Transform: Summary Row",
            "type": "main",
            "index": 0
          },
          {
            "node": "Transform: Explode Issues",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Join Summary + Issues": {
      "main": [
        [
          {
            "node": "Loop URLs (Split in Batches)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transform: Summary Row": {
      "main": [
        [
          {
            "node": "Google Sheets: Append Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Model: Google Gemini": {
      "ai_languageModel": [
        [
          {
            "node": "Generate SEO Audit",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Set: Configure Workflow": {
      "main": [
        [
          {
            "node": "Read Input URLs (Google Sheets)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transform: Explode Issues": {
      "main": [
        [
          {
            "node": "Google Sheets: Append Issues",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Decodo: Fetch Page Content": {
      "main": [
        [
          {
            "node": "Generate SEO Audit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets: Append Issues": {
      "main": [
        [
          {
            "node": "Join Summary + Issues",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Loop URLs (Split in Batches)": {
      "main": [
        [
          {
            "node": "Done",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Decodo: Fetch Page Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets: Append Summary": {
      "main": [
        [
          {
            "node": "Join Summary + Issues",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Output Parser: Enforce SEO JSON": {
      "ai_outputParser": [
        [
          {
            "node": "Generate SEO Audit",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Read Input URLs (Google Sheets)": {
      "main": [
        [
          {
            "node": "Loop URLs (Split in Batches)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}