diff --git a/cmd/liber/liber.go b/cmd/liber/liber.go
index d2a2d348eb0ded9a03851b2c207b11716edc3155..c25ef769b8cc670073aff8f924ff940039d4fa10 100644
--- a/cmd/liber/liber.go
+++ b/cmd/liber/liber.go
@@ -168,11 +168,9 @@ func doSearch(db *liber.Database, query string) {
 }
 
 func doHttpServer(db *liber.Database, addr string) {
-	storage := &liber.FileStorage{
-		Root:    *bookDir,
-		Nesting: 2,
-	}
-	server := liber.NewHttpServer(db, storage, addr)
+	storage := liber.NewFileStorage(expandTilde(*bookDir), 2)
+	cache := liber.NewFileStorage(filepath.Join(expandTilde(*databaseDir), "cache"), 2)
+	server := liber.NewHttpServer(db, storage, cache, addr)
 	log.Fatal(server.ListenAndServe())
 }
 
@@ -217,7 +215,7 @@ func main() {
 			log.SetOutput(logf)
 		}
 
-		doUpdate(db, *bookDir)
+		doUpdate(db, expandTilde(*bookDir))
 	} else if *remotesync != "" {
 		doSync(db, *remotesync)
 	} else if *search {
diff --git a/database.go b/database.go
index 0b7ef2b8e6e45f2b07b079d2eaaabc4d71de7185..fb88395b798bbcb268b0467ec5eadc984643ac73 100644
--- a/database.go
+++ b/database.go
@@ -2,6 +2,7 @@ package liber
 
 import (
 	"bytes"
+	cryptorand "crypto/rand"
 	"encoding/binary"
 	"encoding/json"
 	"errors"
@@ -34,6 +35,13 @@ func (id BookId) Key() []byte {
 	return buf.Bytes()
 }
 
+func init() {
+	// Seed the RNG to a random value.
+	var seed int64
+	binary.Read(cryptorand.Reader, binary.LittleEndian, &seed)
+	rand.Seed(seed)
+}
+
 func NewID() BookId {
 	return BookId(rand.Int63())
 }
diff --git a/files.go b/files.go
index 013f2e4473dae58801c38e99d957e63ef7f4ac89..2f399f2be868179cdb74b73b11c5b9ab956a508f 100644
--- a/files.go
+++ b/files.go
@@ -11,6 +11,13 @@ type FileStorage struct {
 	Nesting int
 }
 
+func NewFileStorage(root string, nesting int) *FileStorage {
+	return &FileStorage{
+		Root:    root,
+		Nesting: nesting,
+	}
+}
+
 // Path of the file corresponding to the given key, relative to the
 // root directory.
 func (s *FileStorage) Path(key string) string {
diff --git a/htdocs/templates/_book.html b/htdocs/templates/_book.html
index 51b92fd6bdffd93c72b2158e61507028ea371eae..bb154cee1d38bfd1687b13c6e04e59506c8f766e 100644
--- a/htdocs/templates/_book.html
+++ b/htdocs/templates/_book.html
@@ -2,7 +2,7 @@
   <div class="row">
     <div class="book-icon">
       {{if .CoverPath}}
-      <img src="/book/cover/{{.Id}}">
+      <img src="/book/thumb/{{.Id}}">
       {{end}}
     </div>
     <div class="book-body">
diff --git a/web.go b/web.go
index 6f2d0f9aca3e7c79fe12e3684ba84c687d6766c2..3a173c9db76301e5e4572235012ab42f1187b68f 100644
--- a/web.go
+++ b/web.go
@@ -9,6 +9,7 @@ import (
 	"io"
 	"log"
 	"net/http"
+	"os/exec"
 	"path/filepath"
 	"strconv"
 	"strings"
@@ -26,6 +27,7 @@ var (
 type uiServer struct {
 	db      *Database
 	storage *FileStorage
+	cache   *FileStorage
 }
 
 type pagination struct {
@@ -186,6 +188,46 @@ func (s *uiServer) handleShowCover(book *Book, w http.ResponseWriter, req *http.
 	http.ServeContent(w, req, "", time.Time{}, f)
 }
 
+const thumbnailSize = "150x150"
+
+func (s *uiServer) handleShowThumbnail(book *Book, w http.ResponseWriter, req *http.Request) {
+	if book.CoverPath == "" {
+		http.NotFound(w, req)
+		return
+	}
+
+	w.Header().Set("Cache-Control", "public")
+
+	cachedPath := s.cache.Path(fmt.Sprintf("%d.thumb.jpg", book.Id))
+	if cachedFile, err := s.cache.Open(cachedPath); err == nil {
+		// Thumbnail is cached.
+		defer cachedFile.Close()
+		w.Header().Set("ETag", "T"+book.Id.String())
+		http.ServeContent(w, req, "", time.Time{}, cachedFile)
+		return
+	}
+
+	cachedFile, err := s.cache.Create(cachedPath)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	defer cachedFile.Close()
+
+	data, err := exec.Command("convert", book.CoverPath, "-geometry", thumbnailSize,
+		"-quality", "60", "jpeg:-").Output()
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "image/jpeg")
+	w.Header().Set("Content-Length", strconv.Itoa(len(data)))
+	w.Header().Set("ETag", "T"+book.Id.String())
+	cachedFile.Write(data)
+	w.Write(data)
+}
+
 func (s *uiServer) handleHome(w http.ResponseWriter, req *http.Request) {
 	render("index.html", w, nil)
 }
@@ -199,7 +241,7 @@ func render(templateName string, w http.ResponseWriter, ctx interface{}) {
 	io.Copy(w, &buf)
 }
 
-func NewHttpServer(db *Database, storage *FileStorage, addr string) *http.Server {
+func NewHttpServer(db *Database, storage, cache *FileStorage, addr string) *http.Server {
 	var err error
 	tpl, err = template.New("liber").Funcs(template.FuncMap{
 		"join": strings.Join,
@@ -209,7 +251,7 @@ func NewHttpServer(db *Database, storage *FileStorage, addr string) *http.Server
 	}
 
 	syncsrv := &syncServer{db, storage}
-	uisrv := &uiServer{db, storage}
+	uisrv := &uiServer{db, storage, cache}
 
 	r := mux.NewRouter()
 	r.Handle("/static/{path:.*}", http.StripPrefix("/static/",
@@ -217,6 +259,7 @@ func NewHttpServer(db *Database, storage *FileStorage, addr string) *http.Server
 	r.HandleFunc("/api/sync/upload", syncsrv.handleSyncUpload).Methods("POST")
 	r.HandleFunc("/api/sync/diff", syncsrv.handleDiffRequest).Methods("POST")
 	r.Handle("/book/cover/{id:[0-9]+}", uisrv.withBook(uisrv.handleShowCover))
+	r.Handle("/book/thumb/{id:[0-9]+}", uisrv.withBook(uisrv.handleShowThumbnail))
 	r.Handle("/book/{id:[0-9]+}", uisrv.withBook(uisrv.handleShowBook))
 	r.Handle("/dl/{id:[0-9]+}", uisrv.withBook(uisrv.handleDownloadBook))
 	r.HandleFunc("/suggest", uisrv.handleAutocomplete)