Recipes
Six small programs you can copy, in JavaScript or Python: a Slack command, a digest, a reading list, a star mirror, a notes sync, a backup.
Six small programs, one for every idea on the developers page. Each is complete — set WEBBITES_KEY and run it. Every snippet comes in JavaScript and Python; switch with the tabs above the code.
A Slack “/save” command
Type /save https://example.com tag:reading in Slack and it lands in your library. Slack wants a reply within three seconds, so answer first and save afterwards.
import express from 'express'
const app = express()
const BASE = 'https://api.webbites.io/v1/api'
app.post('/slack/save', express.urlencoded({ extended: true }), async (req, res) => {
// Slack sends the whole command line as one string: "https://… tag:reading"
const words = (req.body.text || '').trim().split(/\s+/)
const url = words.find(w => w.startsWith('http'))
const tags = words.filter(w => w.startsWith('tag:')).map(w => w.slice(4))
if (!url) return res.json({ text: 'Give me a URL to save.' })
// Answer inside Slack's three second window, then do the work.
res.json({ response_type: 'in_channel', text: `Saving ${url}…` })
const saved = await fetch(`${BASE}/bookmarks`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WEBBITES_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ url, tags: tags.length ? tags : undefined }),
}).then(r => r.json())
// The response_url lets you edit that first reply once you know how it went.
await fetch(req.body.response_url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
replace_original: true,
text: saved.id ? `Saved — it will finish processing in a moment.` : 'That did not save.',
}),
})
})
app.listen(3000) In production, verify Slack's own X-Slack-Signature header before you act on a request — the same idea as verifying our webhook signature, with Slack's secret.
A weekly digest
What did you save this week? This groups the last seven days by tag and prints markdown you can paste into a newsletter, a standup note or an email.
const BASE = 'https://api.webbites.io/v1/api'
const headers = { Authorization: `Bearer ${process.env.WEBBITES_KEY}` }
const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
const byTag = new Map()
let before = null
let done = false
while (!done) {
const params = new URLSearchParams({ limit: '100' })
if (before) params.set('before', before)
const page = await fetch(`${BASE}/bookmarks?${params}`, { headers }).then(r => r.json())
for (const bookmark of page.bookmarks) {
// Newest first, so the first one older than the window ends the walk.
if (new Date(bookmark.createdAt) < since) { done = true; break }
for (const tag of bookmark.tags?.length ? bookmark.tags : ['untagged']) {
byTag.set(tag, [...(byTag.get(tag) || []), bookmark])
}
}
before = page.nextBefore
if (!before) done = true
}
console.log(`# Saved since ${since.toISOString().slice(0, 10)}\n`)
for (const [tag, items] of [...byTag].sort((a, b) => b[1].length - a[1].length)) {
console.log(`## ${tag} (${items.length})`)
for (const b of items) console.log(`- [${b.title}](${b.url})`)
console.log()
}A public reading list
Everything under one tag, turned into a static HTML page you can host anywhere. New save, new entry, no CMS — run it whenever the tag changes, or on a schedule.
import { writeFile } from 'node:fs/promises'
const BASE = 'https://api.webbites.io/v1/api'
const TAG = 'reading'
const headers = { Authorization: `Bearer ${process.env.WEBBITES_KEY}` }
// Everything under one tag, newest first.
const items = []
let before = null
while (true) {
const params = new URLSearchParams({ tag: TAG, limit: '100' })
if (before) params.set('before', before)
const page = await fetch(`${BASE}/bookmarks?${params}`, { headers }).then(r => r.json())
items.push(...page.bookmarks)
if (!page.nextBefore || page.bookmarks.length < 100) break
before = page.nextBefore
}
const esc = s => s.replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]))
const rows = items
.map(b => ` <li><a href="${esc(b.url)}">${esc(b.title || b.url)}</a></li>`)
.join('\n')
const html = [
'<!doctype html>',
'<meta charset="utf-8">',
'<title>Reading list</title>',
`<h1>Reading list (${items.length})</h1>`,
'<ul>',
rows,
'</ul>',
].join('\n')
await writeFile('reading-list.html', html)
console.log(`Wrote ${items.length} links to reading-list.html`)Mirror your GitHub stars
Every repository you star, saved and tagged automatically. Run it on a schedule; the tag makes the second run cheap to reconcile.
const GITHUB_USER = 'elrumo'
const BASE = 'https://api.webbites.io/v1/api'
const headers = {
Authorization: `Bearer ${process.env.WEBBITES_KEY}`,
'Content-Type': 'application/json',
}
const sleep = ms => new Promise(r => setTimeout(r, ms))
// What is already in the library, so a second run is not a second save.
const saved = new Set()
let before = null
while (true) {
const params = new URLSearchParams({ tag: 'github', limit: '100' })
if (before) params.set('before', before)
const page = await fetch(`${BASE}/bookmarks?${params}`, { headers }).then(r => r.json())
page.bookmarks.forEach(b => saved.add(b.url))
if (!page.nextBefore || page.bookmarks.length < 100) break
before = page.nextBefore
}
const stars = await fetch(
`https://api.github.com/users/${GITHUB_USER}/starred?per_page=100`,
{ headers: { Accept: 'application/vnd.github+json' } },
).then(r => r.json())
for (const repo of stars) {
if (saved.has(repo.html_url)) continue
await fetch(`${BASE}/bookmarks`, {
method: 'POST',
headers,
body: JSON.stringify({
url: repo.html_url,
tags: ['github', repo.language?.toLowerCase()].filter(Boolean),
note: repo.description || undefined,
}),
})
console.log('saved', repo.full_name)
await sleep(1100) // stay inside the per-minute allowance
}Sync into your notes
Write every new save into an Obsidian vault (or any folder) as a markdown note — front matter, summary and all — straight from the bookmark.saved webhook.
import { writeFile, mkdir } from 'node:fs/promises'
import { join } from 'node:path'
import express from 'express'
const VAULT = process.env.OBSIDIAN_VAULT || './notes'
const app = express()
// bookmark.saved fires once the screenshot, summary and tags are ready.
app.post('/webhooks/webbites', express.json(), async (req, res) => {
res.sendStatus(200) // acknowledge inside a few seconds; write afterwards.
const b = req.body?.data
if (!b) return
const note = [
'---',
`title: ${JSON.stringify(b.title || b.url)}`,
`url: ${b.url}`,
`tags: [${(b.tags || []).join(', ')}]`,
`saved: ${b.createdAt}`,
'---',
'',
b.summary || '',
'',
`[Open original](${b.url})`,
].join('\n')
const slug = (b.title || b.id).toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 60)
await mkdir(VAULT, { recursive: true })
await writeFile(join(VAULT, `${slug}.md`), note)
console.log('wrote', slug)
})
app.listen(3000) Verify the webhook signature before you write anything to disk — the delivery id in X-WebBites-Delivery also lets you skip retries you have already handled.
Nightly backups
Walks every page with the cursor and writes one JSON file. Useful as a cron job, and the shortest possible demonstration of pagination.
import { writeFile } from 'node:fs/promises'
const BASE = 'https://api.webbites.io/v1/api'
const headers = { Authorization: `Bearer ${process.env.WEBBITES_KEY}` }
const PAGE = 100
const all = []
let before = null
while (true) {
const params = new URLSearchParams({ limit: String(PAGE) })
if (before) params.set('before', before)
const res = await fetch(`${BASE}/bookmarks?${params}`, { headers })
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`)
const page = await res.json()
all.push(...page.bookmarks)
process.stdout.write(`\r${all.length} bookmarks…`)
if (!page.nextBefore || page.bookmarks.length < PAGE) break
before = page.nextBefore
}
const name = `webbites-${new Date().toISOString().slice(0, 10)}.json`
await writeFile(name, JSON.stringify(all, null, 2))
console.log(`\nWrote ${all.length} bookmarks to ${name}`)Build something else?
A Raycast or Alfred command, an RSS reader that files what you finish, a Discord bot, a research folder that fills itself — they are all the same two calls. If you make something, tell us about it; if you get stuck, the playground is the fastest way to see what an endpoint really returns.
