📋 Overview
What it does: Automatically identifies underperforming keywords with high impressions, analyzes their pages, and generates AI-powered optimization recommendations for title tags, meta descriptions, and content.
Time saved: 90% reduction (from 8 hours to 45 minutes per week)
ROI potential: 20-35% CTR improvement = hundreds of additional clicks per week
Ideal for: SEO managers, Digital marketers, Content strategists, Marketing agencies, In-house SEO teams
✨ Key Features
🤖 AI-Powered Analysis
- Smart Filtering — Automatically identifies keywords with ≥100 impressions, <2% CTR, positions 6-20
- Page Analysis — Extracts current title, meta description, and content using Jina AI
- Claude Optimization — Generates URL-specific recommendations for each underperforming keyword
- Batch Processing — Analyzes multiple keywords in a single automated run
- Notion Integration — Saves all recommendations in organized database for team review
🎯 Output Quality
- Keyword-Specific — Each recommendation is tailored to the exact keyword and URL combination
- Actionable — Clear title, meta, and content changes ready to implement
- Data-Driven — Includes CTR improvement estimates and hypothesis for each change
- Organized — All results tracked in Notion with status tracking
- Weekly Reports — Slack notifications with priority rankings and potential impact
💡 How It Helps Digital Marketers & SEOs
For SEO Managers
Challenge: Manually reviewing hundreds of keywords takes days
Solution: Automated weekly analysis surfaces quick-win opportunities
Impact: Focus team on highest-ROI optimizations
For Content Teams
Challenge: Writing compelling meta descriptions at scale is time-consuming
Solution: AI generates optimized copy for each keyword-URL pair
Impact: Consistent, high-quality metadata across entire site
For Marketing Agencies
Challenge: Clients need ongoing SEO improvements with limited budgets
Solution: Deliver weekly optimization reports automatically
Impact: Demonstrate continuous value, improve client retention
For E-commerce Sites
Challenge: Product pages underperform despite high search visibility
Solution: Identify exactly which pages need better titles/descriptions
Impact: Convert more impressions into clicks and sales
🔧 How It Works
Step 1: Automated Weekly Trigger
Every Monday at 8 AM, the workflow automatically:
- Connects to Google Search Console
- Pulls last 30 days of search performance data
- Filters for underperforming keywords (≥100 impressions, <2% CTR, positions 6-20)
Step 2: Page Content Analysis
For each filtered keyword-URL pair:
- Fetches page content using Jina AI Reader
- Extracts current SEO elements:
- Title tag (first 60 characters)
- Meta description (first 160 characters)
- Content preview (first 500 characters)
Step 3: AI Optimization
Claude 3.5 Haiku analyzes each keyword-URL combination and generates:
- Optimized Title — 50-60 chars, keyword-optimized, compelling
- Optimized Meta Description — 150-160 chars, includes CTA
- Content Recommendations — Specific changes to improve relevance
- Hypothesis — Why these changes will improve CTR
- CTR Improvement Estimate — Expected percentage lift (e.g., “25-35%”)
Step 4: Results Organization
All recommendations are:
- Saved to Notion — Organized database with all fields
- Aggregated — Summary statistics calculated
- Prioritized — Sorted by potential impact (additional clicks)
Step 5: Team Notification
Slack summary includes:
- Total keywords analyzed
- High/medium/low priority breakdown
- Total potential additional clicks
- Average position & CTR
- Top 3 opportunities
📊 Technical Architecture
Workflow Nodes (11 total)
⏰ Schedule Trigger → 🔍 Google Search Console → 🎯 Filter Keywords ↓ 💬 Slack Summary ← 📋 Generate Summary ← 📊 Aggregate ← 📝 Notion ↑ 🔍 Parse Response ↑ 🧠 Claude Optimization ↑ 🔄 Loop Over Items ↑ 📄 Parse Page Data ↑ 🌐 Jina AI Fetch
AI Models Used
Model | Purpose | Provider |
|---|---|---|
Claude 3.5 Haiku | SEO optimization recommendations | Anthropic |
Jina AI Reader | Web page content extraction | Jina AI |
Integrations
- Google Search Console — Search performance data
- Jina AI — Page content extraction
- Notion — Results database & tracking
- Slack — Weekly summary notifications
- n8n — Workflow orchestration
Data Processing
- Filter Criteria:
- Impressions: ≥100
- CTR: <2%
- Position: 6-20 (first two pages)
- Batch Size: 1 item per Claude API call
- Rate Limiting: Managed by n8n loop node
🎯 Use Cases
SEO Agency - Client Reporting
Scenario: Monthly SEO retainer for 10 clients
- Run workflow for each client domain weekly
- Generate 40+ optimization opportunities/month per client
- Deliver data-driven recommendations with CTR projections
- Value: Justify retainer fees with concrete deliverables
E-commerce - Product Page Optimization
Scenario: 500-product online store
- Identify underperforming product pages
- Optimize titles/descriptions for better CTR
- Prioritize by potential revenue impact
- Impact: 20% CTR increase = 200+ additional daily visitors
SaaS Company - Content Marketing
Scenario: 200+ blog posts competing for keywords
- Surface blog posts stuck on page 2
- Optimize for target keywords
- Improve click-through from search
- Result: 30% more organic traffic from existing content
Marketing Team - Quick Wins
Scenario: Limited dev resources, need fast results
- Identify no-code optimization opportunities
- Implement title/meta changes same day
- Track improvement week-over-week
- Benefit: High-impact changes with zero dev time
🛠️ Setup Requirements
Prerequisites
- n8n Instance (self-hosted or n8n Cloud)
- Google Search Console with verified property
- Anthropic API Key (Claude access)
- Jina AI Account (free tier works)
- Notion Account with API access
- Slack Workspace (for notifications)
Credentials Needed
- Google Search Console OAuth2
- Anthropic API Key
- Jina AI API Key
- Notion Integration Token
- Slack OAuth2 (Bot Token)
Notion Database Schema
Create a database with these properties:
- URL (Title/URL type)
- Keyword (Rich Text)
- Optimized Title (Rich Text)
- Optimized Meta (Rich Text)
- Content Recommendations (Rich Text)
- Hypothesis (Rich Text)
- CTR Improvement Estimate (Rich Text)
- Status (Select: “pending_review”, “implemented”, “testing”)
Installation Time
- Import workflow: 2 minutes
- Configure credentials: 20 minutes
- Set up Notion database: 10 minutes
- Configure Slack channel: 5 minutes
- Test execution: 10 minutes
- Total setup: ~45 minutes
📈 Performance Metrics
Efficiency Gains
- 90% time reduction (8 hours → 45 minutes per week)
- 100% consistent analysis (no human error)
- Weekly automation (set and forget)
Business Impact
- 20-35% CTR improvement per optimized keyword
- 100-500 additional clicks/month (typical)
- $0-$5,000+ revenue impact (depends on conversion rates)
Quality Metrics
- URL-specific recommendations (not generic advice)
- Data-driven prioritization (by potential impact)
- Trackable improvements (before/after CTR)
🐛 Troubleshooting
No Keywords Found in Filter
Problem: Filter node returns empty results
Solutions:
- ✅ Check Google Search Console has data for your domain
- ✅ Verify date range includes last 30 days
- ✅ Lower filter thresholds (50 impressions instead of 100)
- ✅ Expand position range (1-30 instead of 6-20)
- ✅ Confirm GSC API credentials are working
Jina AI Fetch Errors
Problem: “Failed to fetch URL content”
Solutions:
- ✅ Verify Jina AI API key is valid
- ✅ Check URL is publicly accessible (not behind login)
- ✅ Test URL manually at r.jina.ai/{your-url}
- ✅ Add delay between fetches (rate limiting)
- ✅ Switch to alternative: Puppeteer or HTTP Request node
Claude Response Parsing Failed
Problem: “Parsing failed” in Notion results
Solutions:
- ✅ Check Claude prompt asks for “ONLY valid JSON”
- ✅ Verify JSON schema matches parsing code
- ✅ Test prompt manually in Claude.ai console
- ✅ Add error handling in Parse Claude Response node
- ✅ Increase max_tokens if response gets cut off
Notion Save Errors
Problem: “Failed to create database page”
Solutions:
- ✅ Verify Notion database ID is correct
- ✅ Check all property names match exactly (case-sensitive)
- ✅ Confirm Notion integration has write access
- ✅ Test with manual Notion node execution
- ✅ Check for special characters in data breaking JSON
Slack Notification Not Sending
Problem: Summary doesn’t appear in Slack
Solutions:
- ✅ Verify Slack OAuth2 credentials
- ✅ Check bot has permission to post in channel
- ✅ Confirm channel ID is correct
- ✅ Test with manual Slack node execution
- ✅ Check message formatting (invalid JSON)
🚀 Quick Start Guide
Step 1: Import Workflow (2 min)
- Download workflow JSON file
- Open n8n instance
- Click “Import from File”
- Upload the JSON
- Workflow appears in your canvas
Step 2: Connect Google Search Console (5 min)
- Go to “Query search analytics” node
- Click “Create New Credential”
- Select “Google Search Console OAuth2”
- Follow Google OAuth flow
- Select your verified property
- Test connection
Step 3: Configure Jina AI (2 min)
- Sign up at jina.ai (free)
- Get API key from dashboard
- Go to “Read URL content” node
- Add Jina AI credential
- Paste API key
- Test with sample URL
Step 4: Set Up Anthropic (3 min)
- Get API key from console.anthropic.com
- Go to “SEO optimization expert” node
- Add Anthropic credential
- Paste API key (
sk-ant - Verify model is Claude 3.5 Haiku
Step 5: Create Notion Database (10 min)
- In Notion, create new database
- Add properties (see schema above)
- Get database ID from URL
- Create Notion integration
- Share database with integration
- Go to “Create Notion Records” node
- Add Notion credential
- Select your database
- Map fields to properties
Step 6: Configure Slack (5 min)
- Create Slack app in your workspace
- Add Bot Token Scopes:
chat:write - Install app to workspace
- Copy Bot User OAuth Token
- Go to “Send Slack Notification” node
- Add Slack credential
- Select notification channel
- Test message send
Step 7: Test Execution (10 min)
- Disable schedule trigger (to avoid auto-runs)
- Click “Execute Workflow” manually
- Watch each node execute
- Check for errors
- Verify Notion records created
- Confirm Slack notification sent
- Review data quality
- Enable schedule trigger when ready
Step 8: Monitor First Week (ongoing)
- Let workflow run on Monday
- Check Slack for summary
- Review Notion database
- Implement 2-3 recommendations
- Track CTR changes in GSC
- Adjust filters if needed
Full JSON Workflow:
{
"name": "🔍 Automated Underperforming Keywords Recovery",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 8 * * 1"
}
]
}
},
"id": "node_1",
"name": "⏰ Weekly Schedule (Monday 8 AM)",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
112,
96
]
},
{
"parameters": {
"resource": "databasePage",
"databaseId": {
"__rl": true,
"value": "{{ NOTION_DATABASE_ID }}",
"mode": "list",
"cachedResultName": "Your Database Name",
"cachedResultUrl": "{{ NOTION_DATABASE_URL }}"
},
"title": "Updated Changes",
"propertiesUi": {
"propertyValues": [
{
"key": "URL|title",
"title": "={{ $json.url }}"
},
{
"key": "Keyword|rich_text",
"textContent": "={{ $json.keyword }}"
},
{
"key": "Optimized Title|rich_text",
"textContent": "={{ $json.optimized_title }}"
},
{
"key": "Optimized Meta|rich_text",
"textContent": "={{ $json.optimized_meta }}"
},
{
"key": "Content Recommendations|rich_text",
"textContent": "={{ $json.content_recommendations }}"
},
{
"key": "Hypothesis|rich_text",
"textContent": "={{ $json.hypothesis }}"
},
{
"key": "Status|rich_text",
"textContent": "={{ $json.status }}"
}
]
},
"options": {}
},
"id": "node_2",
"name": "📝 Create Notion Records",
"type": "n8n-nodes-base.notion",
"typeVersion": 2.2,
"position": [
2128,
-16
],
"credentials": {
"notionApi": {
"id": "{{ NOTION_CREDENTIAL_ID }}",
"name": "Notion account"
}
}
},
{
"parameters": {
"aggregate": "aggregateAllItemData",
"options": {}
},
"id": "node_3",
"name": "📊 Aggregate Batch Results",
"type": "n8n-nodes-base.aggregate",
"typeVersion": 1,
"position": [
2368,
160
]
},
{
"parameters": {
"jsCode": "// Generate summary statistics for Slack notification\nconst allResults = items.map(item => item.json);\n\nconst summary = {\n total_keywords_analyzed: allResults.length,\n high_priority: allResults.filter(k => k.gsc_data && k.gsc_data.additional_clicks_potential > 50).length,\n medium_priority: allResults.filter(k => k.gsc_data && k.gsc_data.additional_clicks_potential > 20 && k.gsc_data.additional_clicks_potential <= 50).length,\n low_priority: allResults.filter(k => k.gsc_data && k.gsc_data.additional_clicks_potential <= 20).length,\n total_potential_clicks: allResults.reduce((sum, k) => (\n sum + (k.gsc_data && k.gsc_data.additional_clicks_potential ? k.gsc_data.additional_clicks_potential : 0)\n ), 0),\n avg_position: (\n allResults.reduce((sum, k) => (\n sum + (k.gsc_data && k.gsc_data.position ? parseFloat(k.gsc_data.position) : 0)\n ), 0) / allResults.filter(k => k.gsc_data && k.gsc_data.position !== undefined).length\n ).toFixed(1),\n avg_ctr: (\n allResults.reduce((sum, k) => (\n sum + (k.gsc_data && k.gsc_data.ctr ? parseFloat(k.gsc_data.ctr) : 0)\n ), 0) / allResults.filter(k => k.gsc_data && k.gsc_data.ctr !== undefined).length\n ).toFixed(2),\n top_3_keywords: allResults\n .filter(k => k.gsc_data && k.gsc_data.additional_clicks_potential !== undefined)\n .sort((a, b) => b.gsc_data.additional_clicks_potential - a.gsc_data.additional_clicks_potential)\n .slice(0, 3)\n .map(k => ({\n keyword: k.keyword,\n potential_clicks: k.gsc_data.additional_clicks_potential,\n current_position: k.gsc_data.position,\n current_ctr: k.gsc_data.ctr\n }))\n};\n\nreturn [{ json: summary }];"
},
"id": "node_4",
"name": "📋 Generate Weekly Summary",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2592,
160
]
},
{
"parameters": {
"authentication": "oAuth2",
"select": "channel",
"channelId": {
"__rl": true,
"value": "{{ SLACK_CHANNEL_ID }}",
"mode": "list",
"cachedResultName": "your-channel-name"
},
"text": "=Weekly Underperforming Keywords Report:\n\n{{ $json.total_keywords_analyzed }},\n\n{{ $json.high_priority }}, \n\n{{ $json.medium_priority }}\n\n{{ $json.total_potential_clicks }}",
"otherOptions": {}
},
"id": "node_5",
"name": "💬 Send Slack Notification",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.1,
"position": [
2800,
160
],
"credentials": {
"slackOAuth2Api": {
"id": "{{ SLACK_CREDENTIAL_ID }}",
"name": "Slack account"
}
}
},
{
"parameters": {
"operation": "getPageInsights",
"siteUrl": "{{ YOUR_SITE_URL }}",
"dimensions": [
"page",
"date",
"query"
]
},
"type": "n8n-nodes-google-search-console.googleSearchConsole",
"typeVersion": 1,
"position": [
320,
96
],
"id": "node_6",
"name": "Query search analytics",
"credentials": {
"googleSearchConsoleOAuth2Api": {
"id": "{{ GSC_CREDENTIAL_ID }}",
"name": "Google Search Console account"
}
}
},
{
"parameters": {
"url": "={{ $json.page }}",
"options": {},
"requestOptions": {}
},
"type": "n8n-nodes-base.jinaAi",
"typeVersion": 1,
"position": [
880,
-16
],
"id": "node_7",
"name": "Read URL content",
"credentials": {
"jinaAiApi": {
"id": "{{ JINA_CREDENTIAL_ID }}",
"name": "Jina AI account"
}
}
},
{
"parameters": {
"jsCode": "// Filter GSC data for keywords with ≥100 impressions\n// GSC node outputs each row as a separate item, not nested in a 'rows' array\n\nconst filtered = items\n .filter(item => {\n const row = item.json;\n const impressions = parseInt(row.impressions) || 0;\n const ctr = parseFloat(row.ctr) || 0;\n const position = parseFloat(row.position) || 0;\n \n // Apply thresholds: ≥100 impressions, <2% CTR, position 6-20\n return impressions >= 100 && ctr < 0.02 && position > 5 && position <= 20;\n })\n .map(item => {\n const row = item.json;\n return {\n json: {\n query: row.query,\n page: row.page,\n position: parseFloat(row.position).toFixed(1),\n impressions: parseInt(row.impressions),\n clicks: parseInt(row.clicks),\n ctr: (parseFloat(row.ctr) * 100).toFixed(2),\n // Calculate potential\n ctr_potential: (0.04 * parseInt(row.impressions)), // Estimate clicks if CTR improved to 4%\n additional_clicks: Math.round((0.04 * parseInt(row.impressions)) - parseInt(row.clicks)),\n fetch_date: new Date().toISOString().split('T')[0]\n }\n };\n });\n\nreturn filtered;"
},
"id": "node_8",
"name": "Filter",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
640,
-16
],
"alwaysOutputData": true
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Extract title and meta description from Jina response\nconst markdown = $json.content || '';\n\n// Extract title from markdown (usually first line or after metadata)\nconst titleMatch = markdown.match(/^#\\s+(.+?)$/m) || markdown.match(/<title>(.+?)<\\/title>/);\nconst currentTitle = titleMatch ? titleMatch[1].substring(0, 60) : 'Not found';\n\n// Extract meta description if available in content\nconst metaMatch = markdown.match(/Description:\\s*(.+?)(?:\\n|$)/) ||\n markdown.match(/description['\\\"]?:\\s*['\\\"]?(.+?)['\\\"]?\\n/);\nconst currentMeta = metaMatch ? metaMatch[1].substring(0, 160) : 'Not found';\n\n// Get first 500 chars of main content for context\nconst contentPreview = markdown.substring(0, 500);\n\nreturn {\n ...$json,\n current_title: currentTitle,\n current_meta: currentMeta,\n content_preview: contentPreview,\n url_fetched: true\n};"
},
"id": "node_9",
"name": "Parse Page Title Meta",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1296,
-32
]
},
{
"parameters": {
"modelId": {
"__rl": true,
"value": "claude-3-5-haiku-20241022",
"mode": "list",
"cachedResultName": "claude-3-5-haiku-20241022"
},
"messages": {
"values": [
{
"content": "=You are an SEO optimization expert. This keyword is underperforming despite high impressions. Analyze the current page and provide specific optimization recommendations.\n\nKEYWORD: {{ $('Filter').item.json.query }}\nURL: {{ $json.url }}\n\nCURRENT PAGE:\n- Title: {{ $json.current_title }}\n- Meta Description: {{ $json.current_meta }}\n- Content Preview: {{ $json.content_preview }}\n\nPERFORMANCE DATA:\n- Position: {{ $('Filter').item.json.position }}\n- Impressions: {{ $('Filter').item.json.impressions }}\n- Clicks: {{ $('Filter').item.json.clicks }}\n- CTR: {{ $('Filter').item.json.ctr }}%\n- Potential Additional Clicks: {{ $('Filter').item.json.additional_clicks }} \n\nProvide optimization recommendations for THIS SPECIFIC URL to improve CTR for this keyword. Include:\n1. Optimized title tag (50-60 chars, include target keyword naturally)\n2. Optimized meta description (150-160 chars, compelling call-to-action)\n3. Content optimization suggestions (specific changes to page content)\n4. Hypothesis explaining why these changes will improve CTR\n5. Estimated CTR improvement percentage\n\nReturn ONLY valid JSON:\n{\n \"keyword\": \"the keyword\",\n \"url\": \"the url\",\n \"optimized_title\": \"new title here\",\n \"optimized_meta\": \"new meta description here\",\n \"content_recommendations\": \"specific content changes for this page\",\n \"hypothesis\": \"why these changes will improve CTR for this keyword\",\n \"ctr_improvement_estimate\": \"20-30%\"\n}"
}
]
},
"simplify": false,
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.anthropic",
"typeVersion": 1,
"position": [
1536,
80
],
"id": "node_10",
"name": "SEO optimization expert",
"credentials": {
"anthropicApi": {
"id": "{{ ANTHROPIC_CREDENTIAL_ID }}",
"name": "Anthropic account"
}
}
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
1072,
96
],
"id": "node_11",
"name": "Loop Over Items"
},
{
"parameters": {
"jsCode": "// Parse Claude's JSON response from Anthropic node - process ALL items\nreturn items.map(item => {\n const claudeResponse = item.json;\n const content = claudeResponse.content[0].text;\n\n let optimization;\n try {\n optimization = JSON.parse(content);\n } catch (e) {\n const jsonMatch = content.match(/```json\\s*([\\s\\S]*?)\\s*```/);\n if (jsonMatch) {\n optimization = JSON.parse(jsonMatch[1]);\n } else {\n optimization = {\n keyword: \"Parsing failed\",\n url: \"\",\n optimized_title: \"Manual review needed\",\n optimized_meta: \"Manual review needed\",\n content_recommendations: \"Manual review needed\",\n hypothesis: \"Parsing failed\",\n ctr_improvement_estimate: \"TBD\"\n };\n }\n }\n\n return {\n json: {\n keyword: optimization.keyword,\n url: optimization.url,\n optimized_title: optimization.optimized_title,\n optimized_meta: optimization.optimized_meta,\n content_recommendations: Array.isArray(optimization.content_recommendations) \n ? optimization.content_recommendations.join('\\n') \n : optimization.content_recommendations,\n hypothesis: optimization.hypothesis,\n ctr_improvement_estimate: optimization.ctr_improvement_estimate,\n analysis_date: new Date().toISOString().split('T')[0],\n status: 'pending_review'\n }\n };\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1888,
64
],
"id": "node_12",
"name": "Parse Claude Response"
}
],
"pinData": {},
"connections": {
"⏰ Weekly Schedule (Monday 8 AM)": {
"main": [
[
{
"node": "Query search analytics",
"type": "main",
"index": 0
}
]
]
},
"Query search analytics": {
"main": [
[
{
"node": "Filter",
"type": "main",
"index": 0
}
]
]
},
"Read URL content": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"📝 Create Notion Records": {
"main": [
[
{
"node": "📊 Aggregate Batch Results",
"type": "main",
"index": 0
}
]
]
},
"📊 Aggregate Batch Results": {
"main": [
[
{
"node": "📋 Generate Weekly Summary",
"type": "main",
"index": 0
}
]
]
},
"📋 Generate Weekly Summary": {
"main": [
[
{
"node": "💬 Send Slack Notification",
"type": "main",
"index": 0
}
]
]
},
"Filter": {
"main": [
[
{
"node": "Read URL content",
"type": "main",
"index": 0
}
]
]
},
"Parse Page Title Meta": {
"main": [
[
{
"node": "SEO optimization expert",
"type": "main",
"index": 0
}
]
]
},
"SEO optimization expert": {
"main": [
[
{
"node": "Parse Claude Response",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[
{
"node": "Parse Page Title Meta",
"type": "main",
"index": 0
}
],
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Parse Claude Response": {
"main": [
[
{
"node": "📝 Create Notion Records",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "",
"meta": {
"templateCredsSetupCompleted": false,
"instanceId": ""
},
"id": "",
"tags": []
}Built with ❤️ for the SEO community
Automate the boring stuff. Focus on strategy.