ROI Engine For βΊ SEO Tools βΊ Rank Checker
π SEO Rank Checker
Check real Google rankings β single or bulk keywords, with location targeting and SERP features.
π₯ Check Details
βοΈ Setup β 2 Steps Required
β οΈ Please enter your SerpAPI key.
π Free plan: 100 searches/month β Keys are never stored on any server.
π Deploy Cloudflare Worker β Free (5 min setup)
1
Go to workers.cloudflare.com β Sign up free β Click "Create Worker"
2
Delete the default code β paste the cloudflare-worker.js code (from the ZIP file)
3
Click "Save and Deploy" β Copy the Worker URL (e.g.
https://rank-api.yourname.workers.dev)
4
Paste that URL in Step 1 above β Done! β
π‘ Cloudflare Worker free plan: 100,000 requests/day β completely free forever.
π‘ Connect SerpAPI for live data β demo shows simulated results.
π’
0
Top 10 Rankings
π
β
Avg Position
π
0
Improved
β
0
Not Found
π Results
| # | Keyword | Rank | Ξ | Ranking URL | SERP Features | Difficulty | Vol/mo |
|---|
π₯§ Position Distribution
π 7-Day Trend
π Rank History
Daily rank snapshots β see how positions change over time.
π History Log
βοΈ Competitor Analysis
Compare 3 domains head-to-head for the same keywords.
π Enter Domains
π Comparison
βοΈ Head-to-Head Rankings
π Ranking Trends
30-day ranking movement and position breakdown.
π 30-Day Avg Position
π₯§ Position Breakdown
π Keyword Movement
| Keyword | 30 Days Ago | Today | Change | Trend |
|---|
π API Options & Accuracy Fix
Root cause analysis + correct API setup for accurate Google rankings.
π΄ Root Cause Analysis β Why Rankings Were Inaccurate
β Cause 1 β No pws=0
Google personalizes results by default. Without
pws=0, you get YOUR search history results, not real rankings. This alone can shift positions by 10β30 places.β Cause 2 β Wrong Location Params
Using only
gl=us is not enough. Without cr=countryUS + location=United States, Google may return mixed global results that shift rankings significantly.β Cause 3 β Fetching Only 10 Results
Default Google shows 10 results per page. If your keyword ranks at position 33, fetching only 10 results returns "not found." Must set
num=100 to catch positions up to #100.β Cause 4 β www vs non-www Mismatch
If your domain is
yoursite.com but Google shows www.yoursite.com, a simple .includes(domain) match fails. Must strip www + https before comparing.β Cause 5 β Cached Results
Some APIs cache SERP results for hours. A ranking you check at 2pm may show data from 8am. Must use
no_cache=true to force fresh results every check.β Cause 6 β Counting Ads as Results
If you parse
ads or shopping_results alongside organic, your position count shifts. Must parse organic_results ONLY.β
Complete Fix β All 10 Params Required for Accuracy
q=keyword // user's exact keyword
gl=us // country code
hl=en // language = English
cr=countryUS // country restrict filter
location=United States // city/country precision
pws=0 // β MOST CRITICAL: disable personalization
safe=off // no safe search bias
num=100 // fetch 100 results (find position 33+)
no_cache=true // always fresh, never cached
output=json // structured JSON response
gl=us // country code
hl=en // language = English
cr=countryUS // country restrict filter
location=United States // city/country precision
pws=0 // β MOST CRITICAL: disable personalization
safe=off // no safe search bias
num=100 // fetch 100 results (find position 33+)
no_cache=true // always fresh, never cached
output=json // structured JSON response
π SERP API Comparison
SerpAPI β RECOMMENDED
Most popular. Google, Bing, Yahoo. Location + device targeting. 100 free/month. JSON output.
Free: 100/mo$50/mo β 5Kserpapi.com
ValueSERP
Cheapest accurate option. 1000 free/month. Full SERP including snippets, ads, local pack.
Free: 1K/mo$15/mo β 5Kvalueserp.com
ScaleSerp
Very affordable. Great docs. Supports all Google SERP types β News, Images, Maps included.
Free: 100/mo$9/mo β 5Kscaleserp.com
Zenserp
Simple API, great for bulk. Supports 100+ countries. Fast response. Good for high-volume tracking.
$29/mo β 5Kzenserp.com
BrightData SERP
Enterprise grade. Real browser rendering. Highest accuracy. For agencies with 10K+ keywords.
From $500/mobrightdata.com
βοΈ .env Setup
# Add your API keys to .env
SERPAPI_KEY=your_serpapi_key
VALUESERP_KEY=your_valueserp_key
MONGODB_URI=mongodb://localhost:27017/rankchecker
PORT=3001
NODE_ENV=production
SERPAPI_KEY=your_serpapi_key
VALUESERP_KEY=your_valueserp_key
MONGODB_URI=mongodb://localhost:27017/rankchecker
PORT=3001
NODE_ENV=production
π» Sample Code
Production-ready backend + frontend code for your rank checker.
π₯οΈ Node.js Backend β Rank Fetch Route
// rank-checker/server/routes/rank.js // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // ROOT CAUSE FIXES: // 1. Added pws=0 β disables personalization (critical!) // 2. Added num=100 β fetch 100 results to find deep rankings // 3. Added location param β city/state level targeting // 4. Organic-only parsing β skip ads, knowledge panels, maps // 5. domain.includes() β partial match to catch www + non-www // 6. US default β no global/personalized results // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ const express = require('express'); const axios = require('axios'); const RankHistory = require('../models/RankHistory'); const rateLimit = require('express-rate-limit'); const router = express.Router(); // ββ Supported locations ONLY (no global/irrelevant regions) ββ const ALLOWED_COUNTRIES = { us: { gl: 'us', hl: 'en', location: 'United States', cr: 'countryUS' }, gb: { gl: 'gb', hl: 'en', location: 'United Kingdom', cr: 'countryGB' }, ca: { gl: 'ca', hl: 'en', location: 'Canada', cr: 'countryCA' }, au: { gl: 'au', hl: 'en', location: 'Australia', cr: 'countryAU' }, }; const limiter = rateLimit({ windowMs: 60000, max: 30 }); router.use(limiter); // POST /api/rank/check router.post('/check', async (req, res) => { const { domain, keywords, country = 'us', device = 'desktop' } = req.body; // Validate country β reject unsupported regions if (!ALLOWED_COUNTRIES[country]) return res.status(400).json({ error: 'Unsupported country. Use: us, gb, ca, au' }); if (!domain || !keywords?.length) return res.status(400).json({ error: 'domain + keywords required' }); const geo = ALLOWED_COUNTRIES[country]; // Process in batches of 10 (parallel) const results = []; for (let i = 0; i < keywords.length; i += 10) { const chunk = keywords.slice(i, i + 10); const batch = await Promise.allSettled( chunk.map(kw => checkKeyword(domain, kw, geo, device)) ); batch.forEach((r, j) => results.push({ keyword: chunk[j], ...(r.status === 'fulfilled' ? r.value : { position: null, error: r.reason?.message }) })); } await RankHistory.create({ domain, country, geo, checkedAt: new Date(), results }); res.json({ success: true, domain, country, location: geo.location, results }); }); async function checkKeyword(domain, keyword, geo, device) { const { data } = await axios.get('https://serpapi.com/search', { params: { q: keyword, gl: geo.gl, // FIX 1: geo-country code (us/gb/ca/au) hl: geo.hl, // FIX 2: language = English always cr: geo.cr, // FIX 3: countryUS/GB/CA/AU filter location: geo.location, // FIX 4: city/country name for accuracy pws: '0', // FIX 5: disable personalization (CRITICAL) safe: 'off', // FIX 6: no safe search bias num: '100', // FIX 7: get 100 results (find deep ranks) device: device, // desktop or mobile no_cache: 'true', // FIX 8: always fresh results api_key: process.env.SERPAPI_KEY, output: 'json', } }); // FIX 9: Parse ONLY organic_results β skip ads, shopping, local pack const organics = data.organic_results || []; let position = null, rankingUrl = null; const cleanDomain = domain.replace(/^(https?:\/\/)?(www\.)?/, '').toLowerCase(); organics.forEach((result, idx) => { if (position) return; // stop after first match const resLink = result.link.replace(/^(https?:\/\/)?(www\.)?/, '').toLowerCase(); // FIX 10: match www + non-www versions of domain if (resLink.startsWith(cleanDomain) || resLink.includes(`/${cleanDomain}/`)) { position = idx + 1; rankingUrl = result.link; } }); // Detect SERP features (organic only β not ads) const serpFeatures = []; const fs = data.featured_snippet; if (fs?.link?.includes(cleanDomain)) serpFeatures.push('featured'); if (data.local_results?.places?.some(p => p.website?.includes(cleanDomain))) serpFeatures.push('map'); if (data.knowledge_graph?.website?.includes(cleanDomain)) serpFeatures.push('knowledge'); return { position, rankingUrl, serpFeatures, totalResults: organics.length }; } module.exports = router;
# rank_checker/api/rank.py β Accurate location-based rank checking # ROOT CAUSE FIXES applied (same as Node.js version) import requests, os from flask import Flask, request, jsonify from pymongo import MongoClient from datetime import datetime from concurrent.futures import ThreadPoolExecutor import re app = Flask(__name__) db = MongoClient(os.getenv('MONGODB_URI')).rank_checker # Allowed countries only β no global/irrelevant regions ALLOWED_COUNTRIES = { 'us': {'gl':'us', 'hl':'en', 'location':'United States', 'cr':'countryUS'}, 'gb': {'gl':'gb', 'hl':'en', 'location':'United Kingdom', 'cr':'countryGB'}, 'ca': {'gl':'ca', 'hl':'en', 'location':'Canada', 'cr':'countryCA'}, 'au': {'gl':'au', 'hl':'en', 'location':'Australia', 'cr':'countryAU'}, } def clean_domain(domain): return re.sub(r'^(https?://)?(www\.)?', '', domain).lower() def check_keyword(domain, keyword, geo): clean = clean_domain(domain) resp = requests.get('https://serpapi.com/search', params={ 'q': keyword, 'gl': geo['gl'], # FIX 1: country code 'hl': geo['hl'], # FIX 2: language = English 'cr': geo['cr'], # FIX 3: country restrict 'location': geo['location'], # FIX 4: precise location 'pws': '0', # FIX 5: no personalization 'safe': 'off', # FIX 6: no safe search bias 'num': '100', # FIX 7: fetch 100 results 'no_cache': 'true', # FIX 8: always fresh 'api_key': os.environ['SERPAPI_KEY'] }, timeout=15) data = resp.json() # FIX 9: organic_results ONLY β skip ads, shopping, local organics = data.get('organic_results', []) position, url = None, None for i, r in enumerate(organics): # FIX 10: match www + non-www res_link = clean_domain(r.get('link', '')) if res_link.startswith(clean) or clean in res_link: position, url = i + 1, r['link'] break features = [] fs = data.get('featured_snippet', {}) if clean in clean_domain(fs.get('link', '')): features.append('featured') if data.get('local_results'): features.append('local_pack') return {'keyword': keyword, 'position': position, 'ranking_url': url, 'serp_features': features} @app.route('/api/rank/check', methods=['POST']) def check_rank(): body = request.get_json() domain = body['domain'] keywords = body['keywords'] country = body.get('country', 'us') if country not in ALLOWED_COUNTRIES: return jsonify({'error':'Unsupported country. Use: us, gb, ca, au'}), 400 geo = ALLOWED_COUNTRIES[country] with ThreadPoolExecutor(max_workers=10) as ex: results = list(ex.map(lambda kw: check_keyword(domain, kw, geo), keywords)) db.rank_history.insert_one({ 'domain': domain, 'country': country, 'location': geo['location'], 'checked_at': datetime.utcnow(), 'results': results }) return jsonify({'success': True, 'location': geo['location'], 'results': results})
ποΈ MongoDB Model + CSV Export
// models/RankHistory.js const { Schema, model } = require('mongoose'); const resultSchema = new Schema({ keyword: String, position: Number, rankingUrl: String, serpFeatures: [String], change: Number }); const historySchema = new Schema({ domain: { type: String, required: true, index: true }, country: { type: String, default: 'us' }, checkedAt: { type: Date, default: Date.now, index: true }, results: [resultSchema] }, { timestamps: true }); historySchema.index({ domain: 1, checkedAt: -1 }); module.exports = model('RankHistory', historySchema); // routes/export.js β CSV export const { Parser } = require('json2csv'); router.get('/export/csv/:domain', async (req, res) => { const doc = await RankHistory .findOne({ domain: req.params.domain }).sort({ checkedAt: -1 }); const csv = new Parser({ fields:['keyword','position','rankingUrl','change'] }) .parse(doc.results); res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', `attachment; filename="${req.params.domain}.csv"`); res.send(csv); });
πΊοΈ Build Roadmap
Step-by-step plan to ship a full SaaS rank checker.
π 4-Phase Development Plan
1
Phase 1 β Foundation (Week 1β2)
Set up Node.js/Express server, MongoDB connection, SerpAPI integration, basic rank fetching with error handling and rate limiting.
Node.js + ExpressMongoDBSerpAPIRate LimitingError Handling
2
Phase 2 β Dashboard (Week 3β4)
Build React/Next.js frontend with results table, Chart.js graphs, single + bulk input, location selector, CSV and PDF export.
Next.js 14Tailwind CSSChart.jsCSV ExportReact Query
3
Phase 3 β Advanced Features (Week 5β6)
Daily cron jobs for automated tracking, rank history charts, competitor comparison, email alerts for ranking drops/rises.
Cron JobsEmail AlertsCompetitor Compare30-Day TrendsKW Difficulty
4
Phase 4 β SaaS Launch (Week 7β8)
User auth (JWT/NextAuth), Stripe subscription plans, white-label PDF reports, Google Search Console integration, multi-user support.
JWT AuthStripeGSC APIWhite-labelMulti-tenant
π¦ Recommended Tech Stack
π₯οΈ Frontend
Next.js 14 App Router
Tailwind CSS
Chart.js / Recharts
React Query
Zustand
Tailwind CSS
Chart.js / Recharts
React Query
Zustand
βοΈ Backend
Node.js + Express
MongoDB + Mongoose
BullMQ (job queue)
Node-cron
Nodemailer
MongoDB + Mongoose
BullMQ (job queue)
Node-cron
Nodemailer
π APIs
SerpAPI (primary)
ValueSERP (backup)
Google Search Console
Stripe (payments)
DataForSEO (optional)
ValueSERP (backup)
Google Search Console
Stripe (payments)
DataForSEO (optional)
π Deploy
Vercel (frontend)
Railway (API)
MongoDB Atlas (DB)
Cloudflare CDN
Upstash Redis
Railway (API)
MongoDB Atlas (DB)
Cloudflare CDN
Upstash Redis
Need Higher Rankings?
This tool shows where you rank β we help you rank higher. Get your free SEO audit today.
π Free SEO Audit π¬ WhatsApp