Contributions welcome! → github.com/tecrade/HackNotify
A fully free, open-source pipeline that automatically discovers hackathon listings from any URL you add, classifies them by region (Kerala / India / Global) and mode (Online / Offline / Hybrid), and displays them on a beautiful light-themed React portal — hosted for $0 with automated 48-hour updates via GitHub Actions.
Most scrapers break when a website redesigns its HTML. HackNotify doesn't — because it reads pages semantically using AI:
- Fetch —
scraper/fetch_page.pyfetches the page. Automatically detects whether it's an HTML page or a direct API JSON endpoint and processes each appropriately (nomarkdownify/ recursion issues for API URLs). - Clean — HTML pages are stripped of boilerplate, navigation, ads etc. and converted to clean Markdown. API JSON responses are structured into readable item lists.
- Extract — The clean text is sent to Google Gemini 2.5 Flash (free tier) with a fixed JSON schema. The AI extracts every hackathon field without brittle CSS selectors.
- Store — The structured data is saved to
data/hackathons.jsonand automatically synced tofrontend/public/data/hackathons.json. - Display — A Vite + React + Tailwind frontend reads the JSON and renders filterable hackathon cards.
scraper/sources.py ← Add/edit URLs here (HTML pages OR API endpoints)
│
▼
scraper/fetch_page.py
├── ApiJsonProcessor → direct API JSON response → structured Markdown
├── EmbeddedStateExtractor → __NEXT_DATA__ / JSON-LD extraction
└── DomCleaner → HTML → clean Markdown (with recursion protection)
│
▼
scraper/ai_extract.py
├── Gemini 2.5 Flash (primary)
├── Fallback: gemini-2.0-flash → gemini-1.5-flash
├── 503/429 retry with exponential backoff + jitter
└── Per-chunk error isolation (one failed chunk ≠ abort URL)
│
▼
scraper/scrape.py (orchestrator, de-duplication, normalization)
│
├──► data/hackathons.json
└──► frontend/public/data/hackathons.json
│
▼
React + Vite + Tailwind frontend
Edit scraper/sources.py and append any hackathon listing or API URL to SOURCE_URLS. That's it — no custom parsers needed:
SOURCE_URLS = [
"https://devfolio.co/hackathons", # HTML listing
"https://devpost.com/api/hackathons?status[]=open&per_page=50", # API JSON
"https://unstop.com/api/public/opportunity/search-result?...", # API JSON
"https://your-new-site.com/hackathons", # Just add it!
]Go to https://aistudio.google.com/apikey and generate a free key (no credit card required).
cd scraper
python -m venv venv && source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # Paste your GEMINI_API_KEY into .env
python scrape.pyThis writes data/hackathons.json and automatically syncs it to frontend/public/data/hackathons.json.
cd frontend
npm install
npm run devOpen the printed local URL — filter hackathons by region, mode, and type, or search by title / theme / organizer.
A netlify.toml is included at the repo root:
[build]
base = "frontend"
publish = "dist"
command = "npm run build"- Push the repo to GitHub.
- Go to Netlify → Add new site → Import an existing project.
- Select your repo — Netlify auto-detects
netlify.tomland configures everything. - Click Deploy site. Done!
.github/workflows/deploy.yml builds the React app and publishes it to GitHub Pages whenever frontend/ or the data file changes.
Enable once: Settings → Pages → Source → GitHub Actions.
.github/workflows/scrape.yml runs the full scraping pipeline automatically every 48 hours (every 2 days):
on:
schedule:
- cron: "0 3 */2 * *" # Every 48h at 03:00 UTC (~08:30 IST)
workflow_dispatch: {} # Trigger manually from the Actions tabSetup (one time):
- Go to your GitHub repo → Settings → Secrets and variables → Actions → New repository secret.
- Name:
GEMINI_API_KEY| Value: your Gemini API key - Save.
After every scrape run, the workflow automatically commits updated data/hackathons.json files and pushes — triggering a new Netlify/Pages deploy automatically.
| Feature | Details |
|---|---|
| API JSON detection | Auto-detects JSON content-type or JSON body, bypasses HTML parsing entirely |
| 503 High Demand retry | Exponential backoff with jitter (4s, 10s, 20s, 40s...) |
| Model fallback chain | gemini-2.5-flash → gemini-2.0-flash → gemini-1.5-flash |
| Chunk isolation | Failed chunks don't abort the whole URL — rest of chunks still processed |
| Recursion protection | DomCleaner catches markdownify stack overflows, falls back to plain text |
| De-duplication | Hackathons de-duplicated by title + date key across all sources |
Every hackathon object in data/hackathons.json:
{
"title": "string",
"date": "string",
"venue": "string",
"mode": "online | offline | hybrid",
"region": "kerala | india | global",
"registration_url": "string",
"prize_pool": "string",
"prize_details": "string",
"max_participants": "string",
"theme": "string",
"hackathon_type": "software | hardware | hybrid",
"organizer": "string",
"source_url": "string"
}scraper/
sources.py ← Add/remove source URLs here
fetch_page.py HTML + API JSON fetching and preprocessing
ai_extract.py Gemini AI extraction, retries, and model fallbacks
scrape.py Orchestrator — normalize, dedupe, write JSON
requirements.txt
.env.example
data/
hackathons.json Latest scraped dataset (checked into repo)
frontend/
src/
App.jsx Root app, filtering logic
components/
Header.jsx Hero banner + live tracker pill + GitHub link
FilterBar.jsx Sticky region/mode/type filter + search
HackathonCard.jsx Card component with badges, specs, CTA
EmptyState.jsx Empty filter results state
data/
taxonomy.js Region/mode/type label + badge configuration
public/data/ hackathons.json served to browser
tailwind.config.js
vite.config.js
.github/workflows/
scrape.yml Cron every 48h: scrape → commit → push
deploy.yml On push: build frontend → deploy to GitHub Pages
netlify.toml Zero-config Netlify deployment
ai_extract.py is intentionally small and self-contained. To swap to another LLM (e.g. Groq's Llama, OpenAI GPT-4o-mini):
- Replace
_get_client()and_extract_chunk_with_model()with your provider's SDK. - Keep the
SCHEMA_INSTRUCTIONSprompt and output JSON schema unchanged.
Contributions are welcome and encouraged!
- Add a new source: Edit
scraper/sources.pyand add a URL. - Improve AI extraction: Tweak
SCHEMA_INSTRUCTIONSinai_extract.py. - Fix the frontend: All UI lives in
frontend/src/. - File a bug or feature request: Open an issue.
- Submit a pull request: Fork → branch → PR against
main.
Open source — use, fork, and extend freely.
Made with ❤️ by Tecrade