Recipes

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.

backup.mjs
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}`)
Want a backup without writing code? Settings → Export gives you the same thing on any plan.

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.

slack.mjs
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.

digest.py
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.

stars.mjs
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.