Chicago Free Museum Days

A Bluesky bot that posts weekly updates about free museum days in Chicago, built on Val Town.

🤖 Follow the bot: chicagomuseumdays.bsky.social

Overview

Many Chicago museums offer free admission days for Illinois residents, but finding this information is scattered and hard to track. This project:

  1. Scrapes free museum day data from Choose Chicago
  2. Normalizes dates (handles ranges, recurring patterns like "every Wednesday")
  3. Stores data in SQLite with manual override support
  4. Posts weekly summaries to Bluesky every Monday morning
  5. Serves data via a public JSON API

Architecture

Choose Chicago (HTML)
    ↓
scrapeChooseChicago() → [{institution, rawLines[], url}]
    ↓
normalizeDates() → [{institution, date, time, proof_required, notes}]
    ↓
loadToSQLite() → scraped_free_days table
    ↓
    ├── bot.cron.ts → Bluesky post (weekly)
    └── api.http.ts → JSON API (on-demand)

Val Structure

FileTypeDescription
db.tsScriptSQLite database functions
scrapeChooseChicago.tsScriptHTML scraper using cheerio
utils.tsScriptDate normalization with chrono-node
scraper.cron.tsCronMonthly scraper orchestrator
bot.cron.tsCronWeekly Bluesky poster
api.http.tsHTTPPublic JSON API

API Endpoints

Once deployed, the HTTP val exposes:

EndpointDescription
GET /API documentation
GET /free-days?start=YYYY-MM-DD&end=YYYY-MM-DDFree days in date range
GET /upcomingNext 7 days
GET /todayToday's free days
GET /institutionsList all museums
GET /healthHealth check

Example Response

{ "data": [ { "institution": "Art Institute of Chicago", "date": "2025-01-27", "time": "11am-close", "proof_required": "Illinois residents with ID", "notes": null } ], "count": 1 }

Database

Two SQLite tables with override support:

  • scraped_free_days — Auto-populated by scrapers, replaced on each run
  • free_days_overrides — Manual corrections that take precedence

Uses institution_key (slugified name) for reliable matching across sources. Overrides can also cancel incorrect scraped entries via is_cancelled.

Setup

1. Deploy to Val Town

Copy the backend files to your Val Town project. Files ending in .cron.ts become cron vals, .http.ts becomes an HTTP val.

2. Set Environment Variables

In Val Town secrets, add:

BLUESKY_HANDLE=your-handle.bsky.social
BLUESKY_PASSWORD=your-app-password

Tip: Use a Bluesky App Password instead of your main password.

3. Initialize Data

Run scraper.cron.ts manually to populate initial data.

4. Configure Cron Schedules

ValScheduleCron Expression
scraper.cron.tsFirst Monday of month0 14 1-7 * 1 (UTC)
bot.cron.tsEvery Monday 8am CentralSee note below

Timezone Note: Val Town cron runs in UTC. For 8am Central Time:

  • Winter (CST, UTC-6): 0 14 * * 1
  • Summer (CDT, UTC-5): 0 13 * * 1

Date Parsing

The utils module handles various date formats:

  • Specific dates: "January 15" → 2025-01-15
  • Date ranges: "January 5 - February 28" → expands to individual dates
  • Recurring: "every Wednesday" → generates dates for next 12 months
  • Weekdays: "weekdays only" → generates Mon-Fri for next 12 months

Lines are classified as either date-ish or note-ish using pattern matching, since Choose Chicago mixes dates and eligibility requirements in the same lists.

Dependencies

Contributing

Data corrections can be added via the free_days_overrides table:

import { addOverride } from "./db.ts"; await addOverride("Art Institute of Chicago", "2025-02-15", { time: "10am-5pm", notes: "Presidents Day special hours", override_reason: "Confirmed via museum website", });

To cancel an incorrect scraped entry:

await addOverride("Some Museum", "2025-03-01", { is_cancelled: true, override_reason: "Event was cancelled per museum announcement", });

Maintenance

Monthly:

  • Review scraper logs for parsing failures
  • Add overrides for unparseable dates

Quarterly:

  • Audit data against official museum websites
  • Update parser patterns if needed

Resources

License

MIT