<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: RankLoop</title>
    <description>The latest articles on DEV Community by RankLoop (@rankloop).</description>
    <link>https://dev.to/rankloop</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4017366%2F1ad7345d-1a60-4c69-af8a-66727b44a5f9.png</url>
      <title>DEV Community: RankLoop</title>
      <link>https://dev.to/rankloop</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rankloop"/>
    <language>en</language>
    <item>
      <title>Smash Story: my rank tracker spent weeks reporting "&gt;100" for keywords sitting on page 2</title>
      <dc:creator>RankLoop</dc:creator>
      <pubDate>Fri, 17 Jul 2026 12:41:04 +0000</pubDate>
      <link>https://dev.to/rankloop/smash-story-my-rank-tracker-spent-weeks-reporting-100-for-keywords-sitting-on-page-2-33gh</link>
      <guid>https://dev.to/rankloop/smash-story-my-rank-tracker-spent-weeks-reporting-100-for-keywords-sitting-on-page-2-33gh</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that never crashed
&lt;/h2&gt;

&lt;p&gt;I run a small Google rank tracker (&lt;a href="https://getrankloop.com" rel="noopener noreferrer"&gt;RankLoop&lt;/a&gt;). Its one job: tell you your position for each keyword, daily, and email you when something moves.&lt;/p&gt;

&lt;p&gt;For weeks it reported "not in top 100" for keywords that were actually sitting on page 2. Every request returned 200. Rows were written. Dashboards updated. The alert emails — the core feature — simply never fired, because nothing ever "moved". The product looked perfectly healthy while being wrong for every user it had.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause
&lt;/h2&gt;

&lt;p&gt;Google quietly removed support for the 100-results-per-page parameter. My SERP provider kept accepting &lt;code&gt;num: 100&lt;/code&gt; without an error — and returned ~10 results. My code scanned the ten results it got, did not find the domain, and recorded "&amp;gt;100". Silently. For every keyword ranking 11-100.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smash
&lt;/h2&gt;

&lt;p&gt;Pagination, with two traps that will bite anyone doing this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Positions restart on every page — page 2 returns 1-10 again, so the absolute position is &lt;code&gt;(page - 1) * 10 + position&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Page 1 regularly returns 9 results (SERP features absorb slots). My first fix stopped paginating on a short page, which meant it stopped after page 1 almost every time. Only a fully empty page means you are done.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What caught the bug was not a log or a monitor: it was comparing my numbers against Search Console, which said position 17 while my tool said "&amp;gt;100". Two instruments, one truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before / after
&lt;/h2&gt;

&lt;p&gt;Before: zero alert emails, ever. The morning the fix shipped: the alerts fired for real for the first time — and one of my few real users got an email telling them their main keyword had climbed to #2. That email was the entire product finally working as promised, three weeks late.&lt;/p&gt;

&lt;p&gt;Full technical write-up with the credit-economics fallout (a not-found keyword costs 10x a ranked one): &lt;a href="https://dev.to/rankloop/google-quietly-killed-num100-and-my-rank-tracker-lied-for-weeks-2d38"&gt;https://dev.to/rankloop/google-quietly-killed-num100-and-my-rank-tracker-lied-for-weeks-2d38&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I keep
&lt;/h2&gt;

&lt;p&gt;If your product measures something, run a second instrument beside it and make them argue. A crash pages you; a wrong number just sits there looking healthy.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>debugging</category>
      <category>seo</category>
    </item>
    <item>
      <title>Google quietly killed num=100 — and my rank tracker lied for weeks</title>
      <dc:creator>RankLoop</dc:creator>
      <pubDate>Mon, 13 Jul 2026 16:19:26 +0000</pubDate>
      <link>https://dev.to/rankloop/google-quietly-killed-num100-and-my-rank-tracker-lied-for-weeks-2d38</link>
      <guid>https://dev.to/rankloop/google-quietly-killed-num100-and-my-rank-tracker-lied-for-weeks-2d38</guid>
      <description>&lt;p&gt;This is part 2 of the &lt;a href="https://getrankloop.com" rel="noopener noreferrer"&gt;RankLoop&lt;/a&gt; build log. Part 1 covered the stack. This one covers the week my product lied to every user it had — and how I found out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;A user (fine: me, dogfooding, plus a few real ones) reported that keywords sat at "&amp;gt;100" for weeks and never moved. Meanwhile Google Search Console showed those exact queries at positions 17–48. Two sources, same keyword, wildly different answers. And because nothing ever "moved", the alert emails — the core feature — never fired once.&lt;/p&gt;

&lt;p&gt;The checks ran daily. Every request returned 200. Rows were written. Dashboards updated. Everything &lt;em&gt;looked&lt;/em&gt; healthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause
&lt;/h2&gt;

&lt;p&gt;My SERP provider call asked for 100 results per page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;num&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for a long time that worked. Then Google removed support for the 100-results-per-page parameter, and SERP APIs built on top of it started returning ~10 organic results per request — &lt;strong&gt;while still accepting &lt;code&gt;num: 100&lt;/code&gt; without an error&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So my code happily scanned the 9–10 results it got, didn't find the domain, and recorded "not in top 100". For every keyword ranking 11–100. Silently. For weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix (with two gotchas)
&lt;/h2&gt;

&lt;p&gt;Pagination. Obvious in hindsight, but two details will bite you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Positions restart on every page.&lt;/strong&gt; Page 2 returns positions 1–10 again, not 11–20. You have to compute the absolute position yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;absolute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;RESULTS_PER_PAGE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Page 1 regularly returns 9 results, not 10.&lt;/strong&gt; Featured snippets and other SERP furniture absorb organic slots. My first version stopped paginating when a page came back short — which meant it stopped after page 1 almost every time. The only reliable stop signal is a fully &lt;strong&gt;empty&lt;/strong&gt; page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;organic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// short pages are normal, empty means done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With early exit when the domain is found, a keyword ranking #17 costs 2 requests instead of 10.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody warns you about: cost
&lt;/h2&gt;

&lt;p&gt;Here's the brutal asymmetry: a keyword that ranks #5 costs 1 request to verify. A keyword that ranks &lt;strong&gt;nowhere&lt;/strong&gt; costs the full 10 — you have to scan all 100 positions to honestly say "not in top 100". Not-found keywords are your most expensive ones, and new users add lots of them.&lt;/p&gt;

&lt;p&gt;My fix: adaptive depth. If a keyword's &lt;em&gt;previous&lt;/em&gt; check already said not-in-top-100, the next check only scans the top 30, and a full top-100 sweep runs once a month to catch late climbers. New keywords and ranking keywords always get full depth. That one change cut the weekly API bill roughly in half without touching the product promise.&lt;/p&gt;

&lt;p&gt;And because this failure mode was &lt;em&gt;silent&lt;/em&gt;, I added the alarm I should have had from day one: if an entire check run fails (quota exhausted, key revoked, provider down), the system emails me instead of quietly freezing every dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;Your pipeline can be green — 200s everywhere, rows written, charts moving — and still be wrong. The bug wasn't in my code's logic; it was in an assumption about an upstream API that changed under my feet without an error message.&lt;/p&gt;

&lt;p&gt;The only thing that caught it was comparing against an &lt;strong&gt;independent source of truth&lt;/strong&gt; (Search Console). If your product measures something, find a second instrument that measures the same thing a different way, and check them against each other regularly. Mine now disagree by a position or two (normal SERP variance); the day they disagree by 40 again, I'll know in hours, not weeks.&lt;/p&gt;

&lt;p&gt;Funny epilogue: the morning the fix shipped, the alert emails fired for real for the first time — and one of my users learned their main keyword had climbed to #2. That email was the product doing, at last, exactly what &lt;a href="https://getrankloop.com/blog/wincher-alternative" rel="noopener noreferrer"&gt;the comparison pages&lt;/a&gt; promise it does.&lt;/p&gt;

&lt;p&gt;If you track rankings and want to sanity-check whether your current tool has the same silent bug: check any keyword you &lt;em&gt;know&lt;/em&gt; ranks on page 2, and see if your tracker says "&amp;gt;100". You might be surprised.&lt;/p&gt;

&lt;p&gt;Questions about the pagination details or the credit math — ask away. Part 3 will probably be about what happened when real traffic hit the free plan.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>debugging</category>
      <category>buildinpublic</category>
      <category>node</category>
    </item>
    <item>
      <title>I'm building a $19 rank tracker in public — the stack, and why "daily" is harder than it sounds</title>
      <dc:creator>RankLoop</dc:creator>
      <pubDate>Mon, 06 Jul 2026 09:05:02 +0000</pubDate>
      <link>https://dev.to/rankloop/im-building-a-19-rank-tracker-in-public-the-stack-and-why-daily-is-harder-than-it-sounds-43fc</link>
      <guid>https://dev.to/rankloop/im-building-a-19-rank-tracker-in-public-the-stack-and-why-daily-is-harder-than-it-sounds-43fc</guid>
      <description>&lt;p&gt;I do SEO for my own side projects, and for years I bounced between rank trackers. The expensive ones (Ahrefs, Semrush) are overkill when all I want is "did my 200 keywords move today?". The cheap ones had two recurring problems that drove me up the wall:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Daily" that wasn't daily.&lt;/strong&gt; A lot of budget trackers quietly refresh every 2–5 days and still call it daily. When you're reacting to a Google update, that lag is the whole game.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing friction.&lt;/strong&gt; Cancel-by-email-only, surprise charges after you thought you'd stopped, keyword counts that double-count mobile vs desktop so your "500" is really 250.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I started building the thing I actually wanted. It's called &lt;a href="https://getrankloop.com" rel="noopener noreferrer"&gt;RankLoop&lt;/a&gt;, and this is the build log — the stack, the decisions, and the unglamorous parts that turned out to matter most.&lt;/p&gt;

&lt;p&gt;I'm writing this at the &lt;em&gt;start&lt;/em&gt;, not after some victory lap. No revenue screenshots here. It's live, the free plan is open, and I'm looking for people to poke holes in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one design decision everything else hangs off
&lt;/h2&gt;

&lt;p&gt;Most trackers store your position for a keyword: "you're #7." I decided to store the &lt;strong&gt;entire top 10 of every search, on every check.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That sounds like a lot more data (it is), but it's the feature I always wished I had: when you drop from #4 to #7, you can look back and see &lt;em&gt;exactly which page moved in above you and when&lt;/em&gt;. Ranking is relative — knowing "I dropped" is useless without "because these three competitors climbed."&lt;/p&gt;

&lt;p&gt;Concretely, each check writes a row with the keyword, your position, and a JSON snapshot of the top 10 (URL + title + position). History becomes a time series you can diff. It costs storage, but storage is cheap and the insight isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack (and why, as a solo dev)
&lt;/h2&gt;

&lt;p&gt;Nothing exotic. The bias was: fewer moving parts, fewer things that can page me at 2am.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16 on Vercel.&lt;/strong&gt; App Router, route handlers for the API, one deploy target. As a team of one, "git push and it's live" is worth a lot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase&lt;/strong&gt; for Postgres + Auth + &lt;strong&gt;Row Level Security&lt;/strong&gt;. More on RLS below — it's doing more work than any other single piece.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lemon Squeezy&lt;/strong&gt; as the billing layer. This one deserves a paragraph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend&lt;/strong&gt; for transactional email (alerts, welcome, support). This one deserves a &lt;em&gt;cautionary&lt;/em&gt; paragraph.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why a Merchant of Record instead of Stripe directly
&lt;/h3&gt;

&lt;p&gt;If you sell software to people in the EU as a solo founder, you inherit VAT/tax obligations that are genuinely miserable to handle correctly. A Merchant of Record (Lemon Squeezy, Paddle) becomes the seller of record and handles that global tax mess for you. You trade a bit of margin for not having to become a part-time tax compliance department. For a one-person shop that trade is a no-brainer.&lt;/p&gt;

&lt;p&gt;It also let me make cancellation &lt;strong&gt;self-serve from the dashboard in one click&lt;/strong&gt; — which, given that billing traps were half my motivation, felt non-negotiable.&lt;/p&gt;

&lt;h3&gt;
  
  
  RLS is the whole security model, not a nice-to-have
&lt;/h3&gt;

&lt;p&gt;Every table that holds user data (domains, keywords, checks, reports) has Row Level Security policies so a user can only ever read/write their own rows — enforced in the database, not in my application code. The app could have a bug; the database still won't hand user A's keywords to user B.&lt;/p&gt;

&lt;p&gt;The subtle bit: the columns that decide a user's &lt;em&gt;plan and billing status&lt;/em&gt; live on the profile row, and those are only writable by the service role (the billing webhook), never by the user's own session. Otherwise anyone could &lt;code&gt;PATCH&lt;/code&gt; their own row to "plan: pro" and give themselves the paid tier for free. If you're building multi-tenant on Supabase, audit that specific thing — the gap between "RLS is on" and "RLS is actually scoped correctly" is where the bugs live.&lt;/p&gt;

&lt;h3&gt;
  
  
  The email deliverability rabbit hole
&lt;/h3&gt;

&lt;p&gt;Here's the one that ate a full day and taught me the most.&lt;/p&gt;

&lt;p&gt;Transactional email "works" in dev instantly, so you assume it's fine. Then you realize your first emails went out from a shared sandbox sender and landed straight in spam — because they weren't authenticated for your domain. Fixing it meant actually understanding the three records everyone waves at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SPF&lt;/strong&gt; — which servers are allowed to send for your domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DKIM&lt;/strong&gt; — a cryptographic signature proving the mail wasn't forged in transit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DMARC&lt;/strong&gt; — what receivers should do when SPF/DKIM fail, plus reporting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get those set up on your sending subdomain &lt;em&gt;before&lt;/em&gt; you send a single real email, or you'll poison your domain reputation with your own test messages. Domain reputation is slow to build and slower to repair. I learned this the annoying way so you don't have to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it actually is right now
&lt;/h2&gt;

&lt;p&gt;Honest status:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free plan: 10 keywords, checked daily for your first week, then every Monday. No card.&lt;/li&gt;
&lt;li&gt;Paid: $19/mo for 200 keywords across 5 domains, all checked daily; $49/mo for 500 keywords across 20 domains with 300 checked daily; $99/mo for 1,000 keywords across unlimited domains with 600 checked daily. On the plans above Starter, the rest are checked every Monday.&lt;/li&gt;
&lt;li&gt;Full top-10 history, one-email alerts (only when a keyword enters/leaves the top 10 or crosses your threshold — not daily noise), shareable read-only report links for clients, CSV export.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it is &lt;em&gt;not&lt;/em&gt;: a keyword research tool, a backlink index, or an all-in-one suite. It tracks Google positions reliably and gets out of your way. That's the whole pitch.&lt;/p&gt;

&lt;p&gt;If you're weighing it against a specific tool, I keep honest comparison pages with the real pricing math — the &lt;a href="https://getrankloop.com/blog/wincher-alternative" rel="noopener noreferrer"&gt;Wincher comparison&lt;/a&gt; is the most complete one, including where Wincher is genuinely the better pick.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd love feedback on
&lt;/h2&gt;

&lt;p&gt;If you track rankings for your own projects or clients, I'd genuinely like to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is stored top-10 history something you'd use, or do you only care about your own number?&lt;/li&gt;
&lt;li&gt;What's the smallest feature that would make you switch from whatever you use now?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can try the free plan (no card) at &lt;strong&gt;&lt;a href="https://getrankloop.com" rel="noopener noreferrer"&gt;getrankloop.com&lt;/a&gt;&lt;/strong&gt; and tell me where it falls short. I'm building this in the open, so critical feedback is the useful kind.&lt;/p&gt;

&lt;p&gt;More build-log posts to come — next one is probably the scheduling layer that makes the daily checks actually fire on time, which was less trivial than I expected.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>saas</category>
      <category>nextjs</category>
      <category>indiehackers</category>
    </item>
  </channel>
</rss>
