Skip to main content

My home-baked bookmark manager

·4 mins

Context #

A few days ago I stumbled upon the blog of Stephen Ango (the creator of Obsidian, the app I used for all my notes and writing) and found out about his Obsidian Web Clipper, a bookmarklet to save websites as Obsidian notes.

A bookmarklet is a bookmark stored in a web browser that contains JavaScript code.

His bookmarklet does the following (source):

  • extract the readable content from the page and convert it into Markdown
  • create a frontmatter using information found in the page’s meta section
  • create a note in Obsidian using the Obsidian URI protocol
A webpage’s readable content is what remains after you remove clutter such as buttons, ads, and so on from it. The library is what enables the Firefox Reader View.

Essentially, an application to locally store bookmarks in ~150 lines of JavaScript code that can be run on any device where a compatible browser and Obsidian have been installed. No cloud, no additional app/code to install: brilliant! …but then my eyes fell on the Troubleshooting section:

This bookmarklet may not work on all websites and browsers. […] The most common error is that a website or the browser itself is blocking third party code execution. This is commonly due to the connect-src Content Security Policy (CSP) used by some websites.

A Content Security Policy is a layer of security meant to protect against cross-site scripting and data injection attacks: it works by restricting the protocols that are accepted (for example, enforcing HTTPS) and instructing the browser to only trust executable scripts received from a given list of domains.

At this point I was already so interested in the topic that I decided to roll out my own solution.

bookmarkd #

After a few early-morning programming sessions, I published bookmarkd: it’s a Go application that can run either as a standalone HTTP server or as a Vercel Serverless Function.

It accepts HTTP requests to bookmark a webpage and then performs the following steps:

  • fetch the readable content using go-readability, a porting of Mozilla’s readability library
  • sanitize its HTML content using bluemonday
  • convert its content to Markdown using html-to-markdown
  • create a minimal frontmatter
  • create a note in Obsidian using the Obsidian URI Protocol

Since I wanted a bookmark manager available on all my devices, having a publicly-accessible backend service was an important requirement for me: I don’t have a private server, so being able to run code as a serverless function was the natural next step in my investigation and I ended up looking at Vercel.

Vercel Serverless Function #

The Vercel Serverless Functions’ runtime for Go makes a developer’s life really easy: any .go file placed in the /api folder that defines a func(http.ResponseWriter, *http.Request) function gets deployed as a serverless function, where the filename becomes the last element of the path.

For example, a handler like the following (the function name is not relevant)

func Handle(w http.ResponseWriter, r *http.Request) {

placed in the /api/bookmarks.go file is deployed at https://<project_url>/api/bookmarks.

The only catch I have found so far is that the function doesn’t redirect me straight to Obsidian: instead, it redirects me to a webpage with a Found link. If I click it, I’m then prompted to create a note in Obsidian.

Thanks to their choice of relying on standard net/http handler functions, I was able to reuse the very same code to serve an equivalent endpoint in an HTTP server.

Bonus: the first 100k function invocations are free of charge, and they are… well, more than enough to power my application for several years 🙂

HTTP server #

As I have mentioned, my repository also contains a standalone HTTP server (powered by chi), which exists both as an option to run a private server and to enable the bulk migration of bookmarks without consuming serverless invocations in Vercel (more on that in a follow-up post).

Bookmarklet #

As with the original web clipper, I’m also using a bookmarklet as a trigger: it prompts the user to (optionally) provide some tags, then it sends an HTTP request to the backend service, which takes it over from there.