DEV Community

Cover image for How we stopped backlink spam signups on our link in bio platform (nofollow wasn't enough)
thinslatelabs
thinslatelabs

Posted on

How we stopped backlink spam signups on our link in bio platform (nofollow wasn't enough)

Short answer: if your free link in bio pages attract spam signups from backlink hunters, rel="nofollow" won't stop them. What worked for us was two changes: a Cloudflare Turnstile check on signup, so scripts can't create accounts cheaply, and "go links", so a published page no longer contains any link to the spammer's website. Every outbound link is rewritten to keepp.link/go/<handle>/<linkId> and a Cloudflare Worker redirects the click from the edge.

Below is the full story, with the code.

What is Keepp?

Keepp is a link in bio that works as a full website. Creators and small businesses get a page at keepp.link/yourname, or on their own domain, built from blocks. Next to their links, the same page can run a shop with Stripe checkout paid into the seller's own Stripe account, take bookings, collect forms and run a live chat. There's a free plan, and Claude and ChatGPT can build and edit a Keepp page directly through its MCP server.

Here are three example pages built from Keepp templates:

Three example Keepp link in bio pages: a linen shop, a strength coach and a coffee roaster

That's what a real Keepp page looks like: a profile, social links, sections, products and bookings. It's also why the spam pattern below stood out so clearly.

The problem: spam signups that only wanted a backlink

Anyone can sign up for free and publish a page. That's the product, and it also makes a link in bio platform a target for link building.

We noticed a pattern in new signups. A good share of them published a page with one thing on it: a single link to some other website. No photo, no bio, no products, nothing a person would build for their own audience. Then they never came back.

These were backlink hunters, or the scripts they run. A free profile page on a domain with some authority, holding a link to their site, is something they collect by the hundred.

Why nofollow links didn't stop backlink spam

Every outbound link on a published Keepp page was already marked:

<a href="https://example.com" target="_blank" rel="noopener nofollow ugc">
Enter fullscreen mode Exit fullscreen mode

nofollow ugc tells search engines a user put the link there and we don't vouch for it. In theory that removes the reason to spam us. In practice the spam signups kept coming, for a few reasons:

  • Since 2020, Google treats nofollow as a hint, not a rule. Link builders know this, and many still count nofollow links as worth having.
  • The tools doing this don't read rel attributes. They work from lists of "sites that give you a free profile with a link", sign up, paste the URL and move on.
  • A link is still a link. It can send referral traffic, get a new page crawled sooner, and pad out a backlink report sold to a client.

Nofollow made us a worse target, but it didn't take us off the list. Two things had to change: signing up had to get harder for a script, and the page itself had to be worthless as a backlink.

Step 1: Cloudflare Turnstile on email signup

Keepp has no passwords. You enter your email, we send a one-time code, and you type it in. That code request was the cheapest step to automate: one POST with an email address.

We put Cloudflare Turnstile in front of it. The sign-in page runs a Turnstile check in interaction-only mode and sends its token with every code request, with a fresh one for a resend. Most people never see it; when Cloudflare wants a closer look, a single tick box appears under the email field. The API refuses a code request without a valid token.

The check is a small Express middleware, so any route can use it:

function requireTurnstile({ action, allowUnreachable = false }, deps = {}) {
  const verify = deps.verify || verifyTurnstile;
  return async function turnstileMiddleware(req, res, next) {
    try {
      const token = req.body?.turnstileToken || req.get?.('cf-turnstile-response');
      if (!token) {
        return next(new AppError('Please complete the check and try again.', TURNSTILE_ERRORS.REQUIRED, 400));
      }
      const result = await verify({ token, remoteIp: clientIpFromRequest(req), action });
      if (!result.ok && allowUnreachable && result.reason === TURNSTILE_UNREACHABLE) return next();
      if (!result.ok) {
        return next(new AppError('We could not verify that you are human. Please try again.', TURNSTILE_ERRORS.FAILED, 403));
      }
      return next();
    } catch (err) {
      return next(err);
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

Two details worth getting right:

  1. Send Turnstile the visitor's real IP, not the proxy's. Behind a reverse proxy, req.ip is the proxy. clientIpFromRequest reads the address the visitor actually connected from.
  2. Decide what happens when Cloudflare is down. For sign-in we set allowUnreachable: true. If siteverify itself can't be reached, the request goes through and the route's per-IP and per-email rate limits still apply. Refusing would lock every real user out for the length of a Cloudflare outage. A missing token or a failed check is still refused.

Turnstile stops cheap bot signups. It doesn't stop a person, or a farm of people, signing up by hand. For them, the page itself had to stop being worth anything.

Step 2: go links, a Cloudflare Workers redirect for every outbound link

When a page is published, every web link the owner typed is now written as a link back to us:

https://keepp.link/go/<handle>/<linkId>
Enter fullscreen mode Exit fullscreen mode

A visitor who taps it gets a 302 to the real address. A crawler reading the page sees only links to keepp.link. There is no link to the spammer's site anywhere in the HTML, followed or not.

A Keepp fitness coach link in bio page with social icons and a programmes section

On a page like this one, the social icons, buttons and cards all point to keepp.link/go/... in the published HTML. To the visitor nothing changes: tap the YouTube icon, land on YouTube.

Here's how it's built.

The stored page never changes

Only the published render writes go links. The editor, previews and the stored page document keep the real URLs, so turning the feature off is just a republish.

Stable link ids without a schema change

Each block on a Keepp page has a short ref (8 hex characters), so a link block's id is its ref. A social icon is <blockRef>-<iconRef>. Items inside an article block have no id of their own, so they use <blockRef>-<hash of the URL>.

The redirect map lives in the edge snapshot

The map from link id to address is never stored by itself. It's rebuilt from the published page by the same render that writes the HTML, so the two can't disagree. Keepp already publishes each page as a snapshot to Cloudflare R2, and the map now travels in that snapshot as a go field.

The Cloudflare Worker answers the click

Our Worker already serves published link in bio pages from R2. Go links became one more route, checked before anything else:

async function goTarget(env, handle, linkId) {
  if (env.PAGES) {
    try {
      const object = await env.PAGES.get(pageSnapshotKey(handle));
      const fromSnapshot = object ? snapshotGoTarget(await object.json(), linkId) : undefined;
      if (fromSnapshot !== undefined) return fromSnapshot;
    } catch {
      // The API is always right, only slower.
    }
  }
  return fetchGoTarget(env.API_BASE, handle, linkId);
}

async function goRedirect(env, url) {
  const parsed = parseGoPath(url.pathname);
  const site = String(env.SITE_BASE || url.origin).replace(/\/+$/, '');
  if (!parsed) return null;
  const target = await goTarget(env, parsed.handle, parsed.linkId);
  return new Response(null, {
    status: 302,
    headers: {
      location: target || `${site}/${encodeURIComponent(parsed.handle)}`,
      'cache-control': 'no-store',
      'x-robots-tag': 'noindex, nofollow',
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

Most clicks resolve from the R2 snapshot at the edge. If there's no usable snapshot, the Worker asks the API, which rebuilds the map from the database. A link that no longer exists sends the visitor to the owner's page instead of an error. cache-control: no-store means a link the owner edits points to the new address straight away. Only http and https targets are ever redirected to, so a go link can't turn into a javascript: URL.

Keeping crawlers off /go/

robots.txt disallows /go/, and every redirect also sends x-robots-tag: noindex, nofollow. One trap: a crawler with its own group in robots.txt ignores the * group entirely. Our robots.txt names the AI crawlers individually to welcome them, so Disallow: /go/ has to be repeated in every group.

What stays a direct link

mailto, tel and sms links, links to keepp.link itself, and the Google Maps link Keepp builds from a location stay direct. A map's pasted place URL can be any address, so that one becomes a go link.

Click analytics still see the real destination

The page runtime reports outbound clicks to the owner's analytics. It reads the real host from a data-out-host attribute on the link, so owners still see that people went to YouTube or their shop, not "keepp.link".

Rolling it out, and the trap we hit

The rollout order: deploy the API, push the Worker and robots.txt, turn the feature on, restart the API, then republish every page.

We did the last steps in the wrong order. The republish tool reads its settings itself, but the API only reads them when it starts. We turned the feature on and republished before restarting the API. Every existing page got go links, then every page an owner published afterwards went back to raw links, because the running API still had the old setting. Restart with the new settings before republishing. It's now the first line of our rollout notes.

Results so far

Both changes went live on 28 October 2026. It's early, so no big numbers yet. The one-link spam signups have slowed down, and we expect them to keep falling as the lists these tools work from catch up: a Keepp page no longer gives a backlink to anyone.

Real users lose nothing. Their links work as before, each click is a fast redirect from the edge, and their analytics still show where people went.

FAQ

Does nofollow stop backlink spam?
Not on its own. Google has treated nofollow as a hint since 2020, and the bots behind spam signups don't check rel attributes. Removing the outbound link from the HTML takes away what they came for.

What are go links?
Go links are redirect URLs on your own domain, like keepp.link/go/handle/linkId, written in place of external links when a page is published. The visitor is redirected to the real address; the page's HTML only links to your domain.

Does Cloudflare Turnstile add friction to signup?
Very little. Keepp runs it in interaction-only mode on the email code request, so most people never see it and the rest tick one box. If Cloudflare can't be reached, the request falls back to normal rate limits instead of being refused.

Do go links slow down a link in bio page?
No. The page itself loads the same way. A click costs one 302 from a Cloudflare Worker, answered at the edge from the page's R2 snapshot in most cases.

What is the best link in bio for a small business or creator?
Look for one that works as a full website, so you can sell, take bookings, collect forms and chat with visitors on the same page, with payments going to your own account. That's what we built Keepp to be.

Top comments (0)