bun build --compile bundles the app and Bun runtime into bin/job-board (plus linux-x64/arm64 targets). New src/main.ts entry: serve (default) or 'scrape' once, with --port/-p overriding PORT. A compiled binary stores its database in data/ next to the executable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
3.6 KiB
Markdown
92 lines
3.6 KiB
Markdown
# Job Board
|
|
|
|
Scrapes job sites once a day, stores results in SQLite (`data/jobs.sqlite`), and shows them in a web UI where you can mark jobs as seen.
|
|
|
|
No runtime dependencies. It uses Bun's built-in SQLite, HTTP server and HTMLRewriter.
|
|
|
|
## Sources
|
|
|
|
| Source | How | Notes |
|
|
|---|---|---|
|
|
| slo-tech.com (Ljubljana) | HTML scrape | ~20 newest jobs |
|
|
| startup.si | HTML scrape (no public API or feed) | Site lists every posting since 2019 on one page, so only the last 90 days are kept (`startupSi(days)` in `src/scrapers/index.ts`) |
|
|
| adecco.com (Slovenia, IT) | Internal JSON API the site's job search uses (no public API) | Filtered by category; add more with `adecco("<CategoryFilter value>", "<label>")` |
|
|
| mojedelo.com (IT, Osrednjeslovenska) | Internal JSON API the site's search uses (no public API) | Takes a search URL copied from the site; add more with `mojeDelo("<label>", "<search URL>")` (don't rename the label later, it's part of the source ID) |
|
|
| optius.com (IT, Osrednjeslovenska) | Official RSS feed | Build a feed URL at https://www.optius.com/rss/ (its IDs differ from the search page's); add more with `optius("<label>", "<feed URL>")` (same label rule as MojeDelo) |
|
|
|
|
## Run
|
|
|
|
```sh
|
|
bun install
|
|
bun start # http://localhost:3000
|
|
bun start --port 8080 # or -p 8080
|
|
```
|
|
|
|
The server scrapes every 24h. On startup it waits out the rest of the 24h since the last scrape (or scrapes right away if none happened in the last 24h).
|
|
You can also click **Scrape now** in the UI, or run `bun run scrape` once (e.g. from cron).
|
|
|
|
Env vars: `PORT` (3000; `--port` overrides it), `SCRAPE_INTERVAL_HOURS` (24), `DB_PATH` (`data/jobs.sqlite`).
|
|
|
|
## Build a binary for a server
|
|
|
|
`bun build --compile` bundles the app and the Bun runtime into one executable (~90 MB), so the
|
|
server doesn't need Bun or `node_modules`:
|
|
|
|
```sh
|
|
bun run build # bin/job-board for this machine
|
|
bun run build:linux-x64 # bin/job-board-linux-x64
|
|
bun run build:linux-arm64 # bin/job-board-linux-arm64 (e.g. Raspberry Pi, ARM VPS)
|
|
```
|
|
|
|
On the server:
|
|
|
|
```sh
|
|
./job-board # web server on :3000, scrapes every 24h
|
|
./job-board --port 8080
|
|
./job-board scrape # scrape once and exit (exit code 1 if a scraper failed)
|
|
```
|
|
|
|
The database goes to `data/jobs.sqlite` next to the binary (override with `DB_PATH`).
|
|
The binaries target glibc Linux; for Alpine use `--target=bun-linux-x64-musl`, and for old CPUs
|
|
without AVX2 use `--target=bun-linux-x64-baseline`.
|
|
|
|
Example systemd unit (`/etc/systemd/system/job-board.service`):
|
|
|
|
```ini
|
|
[Unit]
|
|
Description=Job board
|
|
After=network-online.target
|
|
|
|
[Service]
|
|
ExecStart=/opt/job-board/job-board --port 3000
|
|
Restart=on-failure
|
|
User=jobboard
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
## UI
|
|
|
|
- **New / Seen / All** tabs. Opening a job, or clicking *Mark seen*, moves it to Seen.
|
|
- **no longer listed** means the job was missing from that source's latest successful scrape.
|
|
- Search filters on title, company and tags. Recent scrape runs (and errors) are listed at the bottom.
|
|
|
|
## Adding a site
|
|
|
|
1. Create `src/scrapers/<site>.ts` exporting an object that implements `Scraper` (`src/scrapers/types.ts`):
|
|
```ts
|
|
export const mySite: Scraper = {
|
|
id: "my-site", // stable, stored in DB
|
|
name: "My Site",
|
|
async scrape() {
|
|
const html = await fetchHtml("https://..."); // helper in ./http.ts
|
|
return [/* { externalId, title, url, company?, location?, tags?, postedAt? } */];
|
|
},
|
|
};
|
|
```
|
|
2. Add it to the list in `src/scrapers/index.ts`.
|
|
|
|
`externalId` must stay stable for a job, because it is what decides whether a job is new.
|
|
For another slo-tech city, just add `sloTech("Maribor")` to the list.
|