Upcoming Meetup Developer Events
v1PublishedThe top ~10 upcoming developer-related Meetup.com events for a given location, sorted soonest-first.
Output & API
Preview the latest data, download it, or call this collector as an API.
| count | 10 |
|---|---|
| events | |
| source | meetup.com |
| keywords | developer |
| location | us--ca--San Francisco |
| scrapedAt | 2026-06-13T09:35:07.447Z |
Parameters
--keywordsstringSearch term for events, e.g. 'developer', 'AI', 'cybersecurity'. default "developer" · e.g. "developer"
--locationstringMeetup location slug, e.g. 'us--ca--San Francisco' or 'gb--eng--London'. Format is country--region--city. default "us--ca--San Francisco" · e.g. "us--ca--San Francisco"
--max-itemsnumberMaximum number of events to return. default 10 · e.g. 10
Marketplace
Publish this collector so others can deploy it — you keep ownership.
0 runs in 14d · published 14h ago
Versions
Every build and self-heal appends a version. Pin one to lock runs to it.
v1builtapprovedcurrent14h ago
How this script collects data
import Firecrawl from "@mendable/firecrawl-js";
import * as cheerio from "cheerio";
import { parseArgs } from "node:util";
const apiKey = process.env.FIRECRAWL_API_KEY;
if (!apiKey) {
console.error("FIRECRAWL_API_KEY is not set");
process.exit(1);
}
const firecrawl = new Firecrawl({ apiKey });
// --- Parameters -------------------------------------------------------------
const { values: flags } = parseArgs({
strict: true,
options: {
keywords: { type: "string" }, // --keywords=developer (default "developer")
location: { type: "string" }, // --location="us--ca--San Francisco" (Meetup location slug)
"max-items": { type: "string" }, // --max-items=10 (default 10)
},
});
const keywords = (flags.keywords ?? "developer").trim();
// Meetup uses an internal location slug, e.g. "us--ca--San Francisco" or "gb--eng--London".
const location = (flags.location ?? "us--ca--San Francisco").trim();
const maxItems = Math.max(1, Number(flags["max-items"] ?? "10"));
if (!Number.isFinite(maxItems)) {
console.error("--max-items must be a number");
process.exit(1);
}
type Event = {
id: string;
title: string;
url: string;
group: string | null;
startTime: string | null; // ISO 8601, e.g. "2026-06-16T22:00:00-07:00"
startTimeDisplay: string | null; // human label, e.g. "Tue, Jun 16 · 3:00 PM PDT"
attendees: number | null;
status: string | null; // e.g. "Waitlist", or null when open
};
function buildSearchUrl(): string {
const params = new URLSearchParams({
keywords,
source: "EVENTS",
location,
});
return `https://www.meetup.com/find/?${params.toString()}`;
}
async function main() {
const searchUrl = buildSearchUrl();
console.error(`Scraping Meetup events: ${searchUrl}`);
const doc = await firecrawl.scrape(searchUrl, {
formats: ["html"],
integration: "prometheus",
});
const html = doc.html;
if (!html) {
throw new Error("no HTML returned for the Meetup find page");
}
const $ = cheerio.load(html);
const cards = $("div[data-eventref]");
if (cards.length === 0) {
throw new Error(
"no event cards (div[data-eventref]) found on the Meetup find page — layout may have changed or no events for this location/keywords",
);
}
const now = Date.now();
const seen = new Set<string>();
const events: Event[] = [];
cards.each((_, el) => {
const card = $(el);
const id = (card.attr("data-eventref") || "").trim();
if (!id || seen.has(id)) return;
const anchor = card.find('a[data-event-label="Event Card"]').first();
const rawHref = anchor.attr("href") || card.find("a[href]").first().attr("href") || "";
if (!rawHref) return;
// Drop tracking query params to keep a clean, stable event URL.
const url = rawHref.split("?")[0];
const title = (card.find("h3").first().attr("title") || card.find("h3").first().text() || "").trim();
if (!title) return;
const timeEl = card.find("time").first();
const rawDatetime = (timeEl.attr("datetime") || "").trim();
// Meetup appends a "[America/Los_Angeles]" zone id that breaks Date parsing — strip it.
const startTime = rawDatetime ? rawDatetime.replace(/\[[^\]]*\]\s*$/, "") : null;
const startTimeDisplay = timeEl.text().trim() || null;
// Group line is rendered as "by <Group Name>".
let group: string | null = null;
card.find("div").each((_, d) => {
if (group) return;
const t = $(d).text().trim();
if (/^by\s+\S/.test(t) && $(d).children().length === 0) {
group = t.replace(/^by\s+/, "").trim();
}
});
// Attendee count is its own span like "10 attendees" — match the span text
// exactly so a nearby rating (e.g. "4.8") can't bleed into the number.
let attendees: number | null = null;
card.find("span").each((_, s) => {
if (attendees !== null) return;
const m = $(s).text().trim().match(/^([\d,]+)\s+attendees?$/i);
if (m) attendees = Number(m[1].replace(/,/g, ""));
});
// Status badge (e.g. "Waitlist", "Sold Out"); null means open for registration.
let status: string | null = null;
const badge = card.find("span").filter((_, s) => {
const t = $(s).text().trim();
return /^(Waitlist|Sold Out|Cancelled|Almost full|Full)$/i.test(t);
}).first();
if (badge.length) status = badge.text().trim();
// Scope: upcoming/open only — drop events whose start time is in the past.
if (startTime) {
const ts = Date.parse(startTime);
if (Number.isFinite(ts) && ts < now) return;
}
seen.add(id);
events.push({ id, title, url, group, startTime, startTimeDisplay, attendees, status });
});
// Sort soonest-first, then keep the top N.
events.sort((a, b) => {
const ta = a.startTime ? Date.parse(a.startTime) : Number.POSITIVE_INFINITY;
const tb = b.startTime ? Date.parse(b.startTime) : Number.POSITIVE_INFINITY;
return ta - tb;
});
const out = {
source: "meetup.com",
keywords,
location,
scrapedAt: new Date().toISOString(),
count: Math.min(events.length, maxItems),
events: events.slice(0, maxItems),
};
process.stdout.write(JSON.stringify(out));
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Deploy this collector to unlock schedules, the API endpoint, and destinations.