diff --git a/main.go b/main.go
index e153ae39f06042a4a80a7a7efd64e3c7e7b4c1ef..8e3ff474ab63667e5bbcda75de23b853d3a600dc 100644
--- a/main.go
+++ b/main.go
@@ -1,496 +1,42 @@
 package main
 
 import (
-	"crypto/rand"
 	"database/sql"
-	"encoding/base64"
-	"encoding/json"
-	"fmt"
 	"github.com/go-redis/redis"
 	_ "github.com/lib/pq"
-	"html/template"
-	"io"
-	"mime/multipart"
 	"net/http"
-	"os"
-	"path/filepath"
-	"time"
-	"path"
 )
 
-func writeBody(reader io.ReadCloser, path string) error {
-	out, err := os.Create(path)
-	if err != nil {
-		return err
-	}
-	defer out.Close()
-
-	_, err = io.Copy(out, reader)
-	if err != nil {
-		return err
-	}
-	return out.Close()
-}
-
-func detectMimeType(path string) (string, error) {
-	file, err := os.Open(path)
-	if err != nil {
-		return "", err
-	}
-
-	buffer := make([]byte, 512)
-	_, err = file.Read(buffer)
-	if err != nil {
-		return "", err
-	}
-
-	return http.DetectContentType(buffer), nil
-}
-
-func generateId() string {
-	buffer := make([]byte, 4)
-	rand.Read(buffer)
-
-	return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(buffer)
-}
-
-func createImage(config *Config, body io.ReadCloser, fileHeader *multipart.FileHeader) (Image, error) {
-	id := generateId()
-	path := filepath.Join(config.SourceFolder, id)
-
-	err := writeBody(body, path)
-	if err != nil {
-		return Image{}, err
-	}
-
-	mimeType, err := detectMimeType(path)
-	if err != nil {
-		return Image{}, err
-	}
-
-	image := Image{
-		Id:           id,
-		OriginalName: filepath.Base(fileHeader.Filename),
-		CreatedAt:    time.Now(),
-		MimeType:     mimeType,
-	}
-	return image, nil
-}
-
-type UserInfo struct {
-	Id    string
-	Name  string
-	Email string
-}
-
-func parseUser(r *http.Request) UserInfo {
-	return UserInfo{
-		r.Header.Get("X-Auth-Subject"),
-		r.Header.Get("X-Auth-Username"),
-		r.Header.Get("X-Auth-Email"),
-	}
-}
-
-type UploadData struct {
-	User    UserInfo
-	Results []Result
-}
-
-func returnResult(w http.ResponseWriter, templateName string, data interface{}) error {
-	pageTemplate, err := template.ParseFiles(
-		"templates/_base.html",
-		"templates/_header.html",
-		"templates/_navigation.html",
-		"templates/_footer.html",
-		fmt.Sprintf("templates/%s", templateName),
-	)
-	if err != nil {
-		return err
-	}
-
-	err = pageTemplate.Execute(w, data)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
 func main() {
 	config := NewConfigFromEnv()
 
-	client := redis.NewClient(&redis.Options{
-		Addr:     config.Redis.Address,
-		Password: config.Redis.Password,
-	})
-
 	db, err := sql.Open(config.Database.Format, config.Database.Url)
 	if err != nil {
 		panic(err)
 	}
 
-	imageServer := http.FileServer(http.Dir(config.TargetFolder))
-	assetServer := http.FileServer(http.Dir("assets"))
-
-	http.HandleFunc("/upload/", func(w http.ResponseWriter, r *http.Request) {
-		if r.Method == "POST" {
-			user := parseUser(r)
-
-			err := r.ParseMultipartForm(32 << 20)
-			if err != nil {
-				if err = returnResult(w, "upload.html", UploadData{
-					user,
-					[]Result{{
-						Success: false,
-						Errors:  []string{err.Error()},
-					}},
-				}); err != nil {
-					panic(err)
-				}
-			}
-
-			var images []Image
-			var ids []string
-
-			m := r.MultipartForm
-			files := m.File["file"]
-			for _, header := range files {
-				file, err := header.Open()
-				if err != nil {
-					if err = returnResult(w, "upload.html", UploadData{
-						user,
-						[]Result{{
-							Success: false,
-							Errors:  []string{err.Error()},
-						}},
-					}); err != nil {
-						panic(err)
-					}
-					return
-				}
-				image, err := createImage(&config, file, header)
-				if err != nil {
-					if err = returnResult(w, "upload.html", UploadData{
-						user,
-						[]Result{{
-							Success: false,
-							Errors:  []string{err.Error()},
-						}},
-					}); err != nil {
-						panic(err)
-					}
-					return
-				}
-
-				images = append(images, image)
-				ids = append(ids, image.Id)
-			}
-
-			pubsub := client.Subscribe(config.ResultChannel)
-			waiting := make(map[string]bool)
-			for _, image := range images {
-				_, err = db.Exec("INSERT INTO images (id, owner, created_at, original_name, type) VALUES ($1, $2, $3, $4, $5)", image.Id, user.Id, image.CreatedAt, image.OriginalName, image.MimeType)
-				if err != nil {
-					panic(err)
-				}
-
-				data, err := json.Marshal(image)
-				if err != nil {
-					if err = returnResult(w, "upload.html", UploadData{
-						user,
-						[]Result{{
-							Success: false,
-							Errors:  []string{err.Error()},
-						}},
-					}); err != nil {
-						panic(err)
-					}
-					return
-				}
-
-				fmt.Printf("Created task %s at %d\n", image.Id, time.Now().Unix())
-				client.RPush(fmt.Sprintf("queue:%s", config.ImageQueue), data)
-				fmt.Printf("Submitted task %s at %d\n", image.Id, time.Now().Unix())
-
-				waiting[image.Id] = true
-			}
-
-			var results []Result
-			for len(waiting) != 0 {
-				message, err := pubsub.ReceiveMessage()
-				if err != nil {
-					if err = returnResult(w, "upload.html", UploadData{
-						user,
-						[]Result{{
-							Success: false,
-							Errors:  []string{err.Error()},
-						}},
-					}); err != nil {
-						panic(err)
-					}
-					return
-				}
-
-				result := Result{}
-				err = json.Unmarshal([]byte(message.Payload), &result)
-				if err != nil {
-					if err = returnResult(w, "upload.html", UploadData{
-						user,
-						[]Result{{
-							Success: false,
-							Errors:  []string{err.Error()},
-						}},
-					}); err != nil {
-						panic(err)
-					}
-					return
-				}
-
-				fmt.Printf("Returned task %s at %d\n", result.Id, time.Now().Unix())
-
-				if _, ok := waiting[result.Id]; ok {
-					delete(waiting, result.Id)
-
-					results = append(results, result)
-				}
-			}
-
-			if err = returnResult(w, "upload.html", UploadData{
-				user,
-				results,
-			}); err != nil {
-				panic(err)
-			}
-			return
-		} else {
-			user := parseUser(r)
-			if err = returnResult(w, "upload.html", UploadData{
-				user,
-				[]Result{},
-			}); err != nil {
-				panic(err)
-			}
-		}
-	})
-
-	http.Handle("/i/", http.StripPrefix("/i/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-
-		user := parseUser(r)
-
-		type ImageDetailData struct {
-			User   UserInfo
-			Image  Image
-			IsMine bool
-		}
-
-		_, imageId := path.Split(r.URL.Path)
-		result, err := db.Query(`
-			SELECT
-				id,
-				owner,
-				coalesce(title,  ''),
-				coalesce(description, ''),
-        		coalesce(created_at, to_timestamp(0)),
-				coalesce(original_name, ''),
-				coalesce(type, '')
-			FROM images
-			WHERE id = $1
-			`, imageId)
-		if err != nil {
-			panic(err)
-		}
-
-		var info Image
-		if result.Next() {
-			var owner string
-			err := result.Scan(&info.Id, &owner, &info.Title, &info.Description, &info.CreatedAt, &info.OriginalName, &info.MimeType)
-			if err != nil {
-				panic(err)
-			}
-
-			switch r.PostFormValue("action") {
-			case "update":
-				_, err = db.Exec(
-					"UPDATE images SET title = $1, description = $2 WHERE id = $3 AND owner = $4",
-					r.PostFormValue("title"),
-					r.PostFormValue("description"),
-					info.Id,
-					user.Id,
-				)
-				if err != nil {
-					panic(err)
-				}
-				return
-			case "delete":
-				_, err = db.Exec("DELETE FROM images WHERE id = $1 AND owner = $2", info.Id, user.Id)
-				if err != nil {
-					panic(err)
-				}
-				for _, definition := range config.Sizes {
-					os.Remove(path.Join(config.TargetFolder, fmt.Sprintf("%s%s", info.Id, definition.Suffix)))
-				}
-				return
-			default:
-				if err = returnResult(w, "image_detail.html", ImageDetailData{
-					user,
-					info,
-					owner == user.Id,
-				}); err != nil {
-					panic(err)
-				}
-				return
-			}
-		}
-
-		w.WriteHeader(http.StatusNotFound)
-		fmt.Fprint(w, "Image not found")
-	})))
-
-	http.Handle("/a/", http.StripPrefix("/a/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		user := parseUser(r)
-
-		type AlbumImage struct {
-			Id string
-			Title string
-			Description string
-			Position int
-		}
-
-		type Album struct {
-			Id string
-			Title string
-			Description string
-			CreatedAt time.Time
-			Images []AlbumImage
-		}
-
-		type AlbumDetailData struct {
-			User   UserInfo
-			Album Album
-			IsMine bool
-		}
-
-		_, albumId := path.Split(r.URL.Path)
-		result, err := db.Query(`
-			SELECT
-				id,
-				owner,
-				coalesce(title,  ''),
-				coalesce(description, ''),
-        		coalesce(created_at, to_timestamp(0))
-			FROM albums
-			WHERE id = $1
-			`, albumId)
-		if err != nil {
-			panic(err)
-		}
-
-		var info Album
-		if result.Next() {
-			var owner string
-			err := result.Scan(&info.Id, &owner, &info.Title, &info.Description, &info.CreatedAt)
-			if err != nil {
-				panic(err)
-			}
-
-			result, err := db.Query(`
-			SELECT
-				image,
-				title,
-				description,
-				position
-			FROM album_images
-			WHERE album = $1
-			ORDER BY position ASC
-			`, albumId)
-			if err != nil {
-				panic(err)
-			}
-
-			for result.Next() {
-				var image AlbumImage
-				err := result.Scan(&image.Id, &owner, &image.Title, &image.Description, &image.Position)
-				if err != nil {
-					panic(err)
-				}
-
-				info.Images = append(info.Images, image)
-			}
-
-			if err = returnResult(w, "album_detail.html", AlbumDetailData{
-				user,
-				info,
-				owner == user.Id,
-			}); err != nil {
-				panic(err)
-			}
-
-			return
-		}
-
-		w.WriteHeader(http.StatusNotFound)
-		fmt.Fprint(w, "Image not found")
-	})))
-
-	http.HandleFunc("/me/images/", func(w http.ResponseWriter, r *http.Request) {
-		user := parseUser(r)
-
-		type ImageListData struct {
-			User   UserInfo
-			Images []Image
-		}
-
-		result, err := db.Query(`
-			SELECT
-				id,
-				coalesce(title,  ''),
-				coalesce(description, ''),
-        		coalesce(created_at, to_timestamp(0)),
-				coalesce(original_name, ''),
-				coalesce(type, '')
-			FROM images
-			WHERE owner = $1
-			`, user.Id)
-		if err != nil {
-			panic(err)
-		}
-
-		var images []Image
-		for result.Next() {
-			var info Image
-			err := result.Scan(&info.Id, &info.Title, &info.Description, &info.CreatedAt, &info.OriginalName, &info.MimeType)
-			if err != nil {
-				panic(err)
-			}
-			images = append(images, info)
-		}
+	pageContext := PageContext{
+		&config,
+		redis.NewClient(&redis.Options{
+			Addr:     config.Redis.Address,
+			Password: config.Redis.Password,
+		}),
+		db,
+		http.FileServer(http.Dir(config.TargetFolder)),
+		http.FileServer(http.Dir("assets")),
+	}
 
-		if err = returnResult(w, "me_images.html", ImageListData{
-			user,
-			images,
-		}); err != nil {
-			panic(err)
-		}
-	})
+	http.Handle("/upload/", pageUpload(pageContext))
 
-	http.Handle("/assets/", http.StripPrefix("/assets/", assetServer))
-	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		if r.URL.Path == "/" {
-			user := parseUser(r)
+	http.Handle("/i/", http.StripPrefix("/i/", pageImageDetail(pageContext)))
+	http.Handle("/me/i/", http.StripPrefix("/me/i/", pageImageDetail(pageContext)))
 
-			type IndexData struct {
-				User UserInfo
-			}
+	http.Handle("/a/", http.StripPrefix("/a/", pageAlbumDetail(pageContext)))
+	http.Handle("/me/a/", http.StripPrefix("/me/a/", pageAlbumDetail(pageContext)))
 
-			if err = returnResult(w, "index.html", IndexData{
-				user,
-			}); err != nil {
-				panic(err)
-			}
-		} else {
-			imageServer.ServeHTTP(w, r)
-		}
-	})
+	http.Handle("/me/images/", pageImageList(pageContext))
+	http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets"))))
+	http.Handle("/", pageIndex(pageContext))
 
 	err = http.ListenAndServe(":8080", nil)
 	if err != nil {
diff --git a/page_album_detail.go b/page_album_detail.go
new file mode 100644
index 0000000000000000000000000000000000000000..a3050c9262c1b6c95df7684e0080df0585b33bf5
--- /dev/null
+++ b/page_album_detail.go
@@ -0,0 +1,81 @@
+package main
+
+import (
+	"net/http"
+	"fmt"
+	"time"
+	"path"
+)
+
+type AlbumDetailData struct {
+	User   UserInfo
+	Album Album
+	IsMine bool
+}
+
+func pageAlbumDetail(ctx PageContext) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		user := parseUser(r)
+
+		_, albumId := path.Split(r.URL.Path)
+		result, err := ctx.Database.Query(`
+			SELECT
+				id,
+				owner,
+				coalesce(title,  ''),
+				coalesce(description, ''),
+        		coalesce(created_at, to_timestamp(0))
+			FROM albums
+			WHERE id = $1
+			`, albumId)
+		if err != nil {
+			panic(err)
+		}
+
+		var info Album
+		if result.Next() {
+			var owner string
+			err := result.Scan(&info.Id, &owner, &info.Title, &info.Description, &info.CreatedAt)
+			if err != nil {
+				panic(err)
+			}
+
+			result, err := ctx.Database.Query(`
+			SELECT
+				image,
+				title,
+				description,
+				position
+			FROM album_images
+			WHERE album = $1
+			ORDER BY position ASC
+			`, albumId)
+			if err != nil {
+				panic(err)
+			}
+
+			for result.Next() {
+				var image AlbumImage
+				err := result.Scan(&image.Id, &owner, &image.Title, &image.Description, &image.Position)
+				if err != nil {
+					panic(err)
+				}
+
+				info.Images = append(info.Images, image)
+			}
+
+			if err = formatTemplate(w, "album_detail.html", AlbumDetailData{
+				user,
+				info,
+				owner == user.Id,
+			}); err != nil {
+				panic(err)
+			}
+
+			return
+		}
+
+		w.WriteHeader(http.StatusNotFound)
+		fmt.Fprint(w, "Album not found")
+	})
+}
diff --git a/page_album_list.go b/page_album_list.go
new file mode 100644
index 0000000000000000000000000000000000000000..62d66ddad5020ae5ff1f8d517364d77e6c5e44c2
--- /dev/null
+++ b/page_album_list.go
@@ -0,0 +1,10 @@
+package main
+
+import (
+	"net/http"
+)
+
+func pageAlbumList(ctx PageContext) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+	})
+}
diff --git a/page_image_detail.go b/page_image_detail.go
new file mode 100644
index 0000000000000000000000000000000000000000..0ed2fa1c88e2557c96b7eb9a01de95d420887da0
--- /dev/null
+++ b/page_image_detail.go
@@ -0,0 +1,83 @@
+package main
+
+import (
+	"fmt"
+	_ "github.com/lib/pq"
+	"net/http"
+	"os"
+	"path"
+)
+
+type ImageDetailData struct {
+	User   UserInfo
+	Image  Image
+	IsMine bool
+}
+
+func pageImageDetail(ctx PageContext) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		user := parseUser(r)
+
+		_, imageId := path.Split(r.URL.Path)
+		result, err := ctx.Database.Query(`
+			SELECT
+				id,
+				owner,
+				coalesce(title,  ''),
+				coalesce(description, ''),
+        		coalesce(created_at, to_timestamp(0)),
+				coalesce(original_name, ''),
+				coalesce(type, '')
+			FROM images
+			WHERE id = $1
+			`, imageId)
+		if err != nil {
+			panic(err)
+		}
+
+		var info Image
+		if result.Next() {
+			var owner string
+			err := result.Scan(&info.Id, &owner, &info.Title, &info.Description, &info.CreatedAt, &info.OriginalName, &info.MimeType)
+			if err != nil {
+				panic(err)
+			}
+
+			switch r.PostFormValue("action") {
+			case "update":
+				_, err = ctx.Database.Exec(
+					"UPDATE images SET title = $1, description = $2 WHERE id = $3 AND owner = $4",
+					r.PostFormValue("title"),
+					r.PostFormValue("description"),
+					info.Id,
+					user.Id,
+				)
+				if err != nil {
+					panic(err)
+				}
+				return
+			case "delete":
+				_, err = ctx.Database.Exec("DELETE FROM images WHERE id = $1 AND owner = $2", info.Id, user.Id)
+				if err != nil {
+					panic(err)
+				}
+				for _, definition := range ctx.Config.Sizes {
+					os.Remove(path.Join(ctx.Config.TargetFolder, fmt.Sprintf("%s%s", info.Id, definition.Suffix)))
+				}
+				return
+			default:
+				if err = formatTemplate(w, "image_detail.html", ImageDetailData{
+					user,
+					info,
+					owner == user.Id,
+				}); err != nil {
+					panic(err)
+				}
+				return
+			}
+		}
+
+		w.WriteHeader(http.StatusNotFound)
+		fmt.Fprint(w, "Image not found")
+	})
+}
diff --git a/page_image_list.go b/page_image_list.go
new file mode 100644
index 0000000000000000000000000000000000000000..d3592d6308d224e8b17d003bf2c30aa0b8c32a8b
--- /dev/null
+++ b/page_image_list.go
@@ -0,0 +1,48 @@
+package main
+
+import (
+	"net/http"
+)
+
+type ImageListData struct {
+	User   UserInfo
+	Images []Image
+}
+
+func pageImageList(ctx PageContext) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		user := parseUser(r)
+
+		result, err := ctx.Database.Query(`
+			SELECT
+				id,
+				coalesce(title,  ''),
+				coalesce(description, ''),
+        		coalesce(created_at, to_timestamp(0)),
+				coalesce(original_name, ''),
+				coalesce(type, '')
+			FROM images
+			WHERE owner = $1
+			`, user.Id)
+		if err != nil {
+			panic(err)
+		}
+
+		var images []Image
+		for result.Next() {
+			var info Image
+			err := result.Scan(&info.Id, &info.Title, &info.Description, &info.CreatedAt, &info.OriginalName, &info.MimeType)
+			if err != nil {
+				panic(err)
+			}
+			images = append(images, info)
+		}
+
+		if err = formatTemplate(w, "image_list.html", ImageListData{
+			user,
+			images,
+		}); err != nil {
+			panic(err)
+		}
+	})
+}
diff --git a/page_index.go b/page_index.go
new file mode 100644
index 0000000000000000000000000000000000000000..7586d20fcdff3f8480862b051ad57d5a55f2f918
--- /dev/null
+++ b/page_index.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+	"net/http"
+)
+
+type IndexData struct {
+	User UserInfo
+}
+
+func pageIndex(ctx PageContext) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/" {
+			user := parseUser(r)
+
+			if err := formatTemplate(w, "index.html", IndexData{
+				user,
+			}); err != nil {
+				panic(err)
+			}
+		} else {
+			ctx.Images.ServeHTTP(w, r)
+		}
+	})
+}
diff --git a/page_upload.go b/page_upload.go
new file mode 100644
index 0000000000000000000000000000000000000000..b4acedde6d46a58da58145c20078c3ab98edeba2
--- /dev/null
+++ b/page_upload.go
@@ -0,0 +1,221 @@
+package main
+
+import (
+	"net/http"
+	"fmt"
+	"time"
+	"encoding/json"
+	"io"
+	"mime/multipart"
+	"path/filepath"
+	"os"
+	"encoding/base64"
+	"crypto/rand"
+)
+
+type UploadData struct {
+	User    UserInfo
+	Results []Result
+}
+
+func detectMimeType(path string) (string, error) {
+	file, err := os.Open(path)
+	if err != nil {
+		return "", err
+	}
+
+	buffer := make([]byte, 512)
+	_, err = file.Read(buffer)
+	if err != nil {
+		return "", err
+	}
+
+	return http.DetectContentType(buffer), nil
+}
+
+func generateId() string {
+	buffer := make([]byte, 4)
+	rand.Read(buffer)
+
+	return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(buffer)
+}
+
+func writeBody(reader io.ReadCloser, path string) error {
+	out, err := os.Create(path)
+	if err != nil {
+		return err
+	}
+	defer out.Close()
+
+	_, err = io.Copy(out, reader)
+	if err != nil {
+		return err
+	}
+	return out.Close()
+}
+
+func createImage(config *Config, body io.ReadCloser, fileHeader *multipart.FileHeader) (Image, error) {
+	id := generateId()
+	path := filepath.Join(config.SourceFolder, id)
+
+	err := writeBody(body, path)
+	if err != nil {
+		return Image{}, err
+	}
+
+	mimeType, err := detectMimeType(path)
+	if err != nil {
+		return Image{}, err
+	}
+
+	image := Image{
+		Id:           id,
+		OriginalName: filepath.Base(fileHeader.Filename),
+		CreatedAt:    time.Now(),
+		MimeType:     mimeType,
+	}
+	return image, nil
+}
+
+func pageUpload(ctx PageContext) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.Method == "POST" {
+			user := parseUser(r)
+
+			err := r.ParseMultipartForm(32 << 20)
+			if err != nil {
+				if err = formatTemplate(w, "upload.html", UploadData{
+					user,
+					[]Result{{
+						Success: false,
+						Errors:  []string{err.Error()},
+					}},
+				}); err != nil {
+					panic(err)
+				}
+			}
+
+			var images []Image
+			var ids []string
+
+			m := r.MultipartForm
+			files := m.File["file"]
+			for _, header := range files {
+				file, err := header.Open()
+				if err != nil {
+					if err = formatTemplate(w, "upload.html", UploadData{
+						user,
+						[]Result{{
+							Success: false,
+							Errors:  []string{err.Error()},
+						}},
+					}); err != nil {
+						panic(err)
+					}
+					return
+				}
+				image, err := createImage(ctx.Config, file, header)
+				if err != nil {
+					if err = formatTemplate(w, "upload.html", UploadData{
+						user,
+						[]Result{{
+							Success: false,
+							Errors:  []string{err.Error()},
+						}},
+					}); err != nil {
+						panic(err)
+					}
+					return
+				}
+
+				images = append(images, image)
+				ids = append(ids, image.Id)
+			}
+
+			pubsub := ctx.Redis.Subscribe(ctx.Config.ResultChannel)
+			waiting := make(map[string]bool)
+			for _, image := range images {
+				_, err = ctx.Database.Exec("INSERT INTO images (id, owner, created_at, original_name, type) VALUES ($1, $2, $3, $4, $5)", image.Id, user.Id, image.CreatedAt, image.OriginalName, image.MimeType)
+				if err != nil {
+					panic(err)
+				}
+
+				data, err := json.Marshal(image)
+				if err != nil {
+					if err = formatTemplate(w, "upload.html", UploadData{
+						user,
+						[]Result{{
+							Success: false,
+							Errors:  []string{err.Error()},
+						}},
+					}); err != nil {
+						panic(err)
+					}
+					return
+				}
+
+				fmt.Printf("Created task %s at %d\n", image.Id, time.Now().Unix())
+				ctx.Redis.RPush(fmt.Sprintf("queue:%s", ctx.Config.ImageQueue), data)
+				fmt.Printf("Submitted task %s at %d\n", image.Id, time.Now().Unix())
+
+				waiting[image.Id] = true
+			}
+
+			var results []Result
+			for len(waiting) != 0 {
+				message, err := pubsub.ReceiveMessage()
+				if err != nil {
+					if err = formatTemplate(w, "upload.html", UploadData{
+						user,
+						[]Result{{
+							Success: false,
+							Errors:  []string{err.Error()},
+						}},
+					}); err != nil {
+						panic(err)
+					}
+					return
+				}
+
+				result := Result{}
+				err = json.Unmarshal([]byte(message.Payload), &result)
+				if err != nil {
+					if err = formatTemplate(w, "upload.html", UploadData{
+						user,
+						[]Result{{
+							Success: false,
+							Errors:  []string{err.Error()},
+						}},
+					}); err != nil {
+						panic(err)
+					}
+					return
+				}
+
+				fmt.Printf("Returned task %s at %d\n", result.Id, time.Now().Unix())
+
+				if _, ok := waiting[result.Id]; ok {
+					delete(waiting, result.Id)
+
+					results = append(results, result)
+				}
+			}
+
+			if err = formatTemplate(w, "upload.html", UploadData{
+				user,
+				results,
+			}); err != nil {
+				panic(err)
+			}
+			return
+		} else {
+			user := parseUser(r)
+			if err := formatTemplate(w, "upload.html", UploadData{
+				user,
+				[]Result{},
+			}); err != nil {
+				panic(err)
+			}
+		}
+	})
+}
diff --git a/templates/me_images.html b/templates/image_list.html
similarity index 100%
rename from templates/me_images.html
rename to templates/image_list.html
diff --git a/util.go b/util.go
new file mode 100644
index 0000000000000000000000000000000000000000..04d90064258df846fc367c8c70f62b1d8f28c6d4
--- /dev/null
+++ b/util.go
@@ -0,0 +1,73 @@
+package main
+
+import (
+	"fmt"
+	"net/http"
+	"html/template"
+	"time"
+	"database/sql"
+	"github.com/go-redis/redis"
+	"encoding/base64"
+	"io"
+	"mime/multipart"
+	"path/filepath"
+	"os"
+	"crypto/rand"
+)
+
+type UserInfo struct {
+	Id    string
+	Name  string
+	Email string
+}
+
+type PageContext struct {
+	Config      *Config
+	Redis       *redis.Client
+	Database    *sql.DB
+	Images      http.Handler
+	AssetServer http.Handler
+}
+
+type AlbumImage struct {
+	Id string
+	Title string
+	Description string
+	Position int
+}
+
+type Album struct {
+	Id string
+	Title string
+	Description string
+	CreatedAt time.Time
+	Images []AlbumImage
+}
+
+func parseUser(r *http.Request) UserInfo {
+	return UserInfo{
+		r.Header.Get("X-Auth-Subject"),
+		r.Header.Get("X-Auth-Username"),
+		r.Header.Get("X-Auth-Email"),
+	}
+}
+
+func formatTemplate(w http.ResponseWriter, templateName string, data interface{}) error {
+	pageTemplate, err := template.ParseFiles(
+		"templates/_base.html",
+		"templates/_header.html",
+		"templates/_navigation.html",
+		"templates/_footer.html",
+		fmt.Sprintf("templates/%s", templateName),
+	)
+	if err != nil {
+		return err
+	}
+
+	err = pageTemplate.Execute(w, data)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}