DocsGuides
Recipes
Three small programs you can copy: a backup, a Slack command, a digest.
Four small programs that do something useful on their own. Each one is complete — set WEBBITES_KEY and run it.
Back up your library to a file
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}`)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.
import os
from collections import defaultdict
from datetime import datetime, timedelta, timezone
import requests
BASE = "https://api.webbites.io/v1/api"
HEADERS = {"Authorization": f"Bearer {os.environ['WEBBITES_KEY']}"}
SINCE = datetime.now(timezone.utc) - timedelta(days=7)
by_tag = defaultdict(list)
before = None
done = False
while not done:
params = {"limit": 100}
if before:
params["before"] = before
page = requests.get(f"{BASE}/bookmarks", params=params, headers=HEADERS, timeout=30).json()
for bookmark in page["bookmarks"]:
saved_at = datetime.fromisoformat(bookmark["createdAt"].replace("Z", "+00:00"))
# Newest first, so the first one older than the window ends the walk.
if saved_at < SINCE:
done = True
break
for tag in bookmark.get("tags") or ["untagged"]:
by_tag[tag].append(bookmark)
before = page.get("nextBefore")
if not before:
done = True
print(f"# Saved since {SINCE:%d %b}\n")
for tag, items in sorted(by_tag.items(), key=lambda kv: -len(kv[1])):
print(f"## {tag} ({len(items)})")
for bookmark in items:
print(f"- [{bookmark['title']}]({bookmark['url']})")
print()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
}Build something else?
A Raycast or Alfred command, an RSS reader that files what you finish, a public reading list on your own site, a research folder that syncs into your notes — 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.
