Recipes

DocsGuides

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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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}`)
Want a backup without writing code? Settings → Export gives you the same thing on any plan.

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.