Dev.to WebDev 🛠 Dev 👁 0 📖 3 min read

Filtering Freelance Job Leads with Recency Windows and Board Rules

Why stale leads waste time When I started pulling freelance gigs for my own web‑development business, I quickly realized that most of the listings I scraped were either already filled or older than a week. Chasing thos

Why stale leads waste time

When I started pulling freelance gigs for my own web‑development business, I quickly realized that most of the listings I scraped were either already filled or older than a week. Chasing those posts on my phone meant wasted minutes and a lower response rate. The fix is simple: filter out anything that isn’t fresh and apply a few board‑specific rules before the lead ever reaches my inbox.

Setting up a recency window

A recency window is just a date check. I keep only jobs posted within the last 7 days. Anything older is discarded. The logic lives in a tiny Node function so it can be reused across all my scrapers.

// returns true if the post is newer than `days` days
function isRecent(postDate, days = 7) {
  const now = new Date();
  const posted = new Date(postDate);
  const diff = (now - posted) / (1000 * 60 * 60 * 24);
  return diff <= days;
}

I call this right after I parse the raw HTML from a board. The function is deliberately small – it’s easier to audit and to adjust if a board starts showing dates in a different format.

Defining board‑specific rules

Each job board has quirks. Some tag every entry with a “remote‑only” flag, others embed the budget in the title. I maintain a JSON map that describes the filters I need per board.

{
  "upwork": {
    "excludeKeywords": ["senior", "lead"],
    "minBudget": 300
  },
  "freelancermap": {
    "requiredKeywords": ["wordpress", "e‑commerce"],
    "excludeKeywords": ["full‑time"]
  }
}

The script reads this map and applies the appropriate checks. For example, on Upwork I drop any posting that contains senior or lead because I focus on mid‑level work, and I ignore gigs below $300.

function passesBoardRules(board, post) {
  const rules = boardRules[board];
  const title = post.title.toLowerCase();

  if (rules.requiredKeywords) {
    const hasAll = rules.requiredKeywords.every(k => title.includes(k));
    if (!hasAll) return false;
  }

  if (rules.excludeKeywords) {
    const hasBad = rules.excludeKeywords.some(k => title.includes(k));
    if (hasBad) return false;
  }

  if (rules.minBudget && post.budget < rules.minBudget) return false;
  return true;
}

Because the rules are data‑driven, adding a new board is just a matter of extending the JSON file – no code changes needed.

Putting it together in a Node script

The core of my lead‑scanner lives in a single index.js file that runs on my home automation server (a modest Raspberry Pi). The flow is:

  1. Fetch the RSS or HTML feed for each board.
  2. Parse each item into a uniform object {title, date, budget, url}.
  3. Run isRecent and passesBoardRules.
  4. Push the surviving leads to a private Slack channel via webhook.
const fetch = require('node-fetch');
const boardRules = require('./boardRules.json');
const webhook = process.env.SLACK_WEBHOOK;

async function scanBoard(board, feedUrl) {
  const resp = await fetch(feedUrl);
  const text = await resp.text();
  const posts = parseFeed(text); // implementation omitted for brevity

  const fresh = posts.filter(p => isRecent(p.date) && passesBoardRules(board, p));
  for (const p of fresh) await sendToSlack(p);
}

async function main() {
  await Promise.all([
    scanBoard('upwork', 'https://www.upwork.com/ab/feed/jobs.rss'),
    scanBoard('freelancermap', 'https://www.freelancermap.com/rss')
  ]);
}

main().catch(console.error);

The script is intentionally short; the heavy lifting (HTML parsing, RSS handling) is delegated to well‑maintained npm packages. This keeps the custom logic focused on filtering, which is the part that actually saves me time.

Running it daily

I schedule the script with cron on the Pi:

0 8 * * * /usr/bin/node /home/kaleb/lead‑scanner/index.js >> /var/log/lead‑scanner.log 2>&1

Running at 8 AM gives me a fresh batch of qualified leads before I start my client work. The log file helps me spot any board that changed its markup – a quick visual check each week is enough to keep the parser reliable.

Takeaways

  • Recency matters – a 7‑day window cuts stale posts by roughly 60 % in my experience.
  • Data‑driven rules let you adapt to each board without touching code.
  • Small, composable functions make the filter easy to audit and extend.
  • Automation at home (even on a low‑cost device) is sufficient for a solo developer.

I build sites and automations for small businesses at dreamfuturestech.com.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.