diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..958fba8cbdb9c967c24232d70ac0049189f69647
--- /dev/null
+++ b/README.md
@@ -0,0 +1,24 @@
+# Boxes - a home inventory system
+
+I live out in the country and have a number of hobbies.  Some projects
+require certain tools or materials which can take time to acquire.
+In an effort to make this easier to organise, I wanted a way to store
+information on where I put things over the weeks, months and years it
+takes to gain the skills or things I need.
+
+The model for this app is that contents go in boxes and boxes go in
+locations.  Contents can be tagged.  So you can have a "bird house"
+tag for a bird house project and look for where all the things you need
+for a bird house.
+
+## References
+
+  * Simple PWA [tutorial](https://www.geeksforgeeks.org/making-a-simple-pwa-under-5-minutes/)
+    and also [this tutorial](https://viblo.asia/p/creating-a-basic-progressive-web-app-using-vanillajs-m68Z0RVX5kG)
+  * App manifest [generator](https://app-manifest.firebaseapp.com/)
+  * Echo [embed resources](https://echo.labstack.com/docs/cookbook/embed-resources)
+  * [Mithril](https://mithril.js.org/)
+  * Pure Go [database/sql](https://pkg.go.dev/database/sql) implementation
+    of a [sqlite driver](https://pkg.go.dev/modernc.org/sqlite)
+  * [sqlc docs](https://docs.sqlc.dev/en/latest/index.html)
+  * [sqlite docs](https://www.sqlite.org/docs.html)
diff --git a/api/boxes.yaml b/api/boxes.yaml
index 1f95a39c44bb2acd6b40538c792fee37856cd5fc..3a3be92e7e279f737cc4dbb0b098a28c788c9c7f 100644
--- a/api/boxes.yaml
+++ b/api/boxes.yaml
@@ -15,7 +15,7 @@ servers:
   - url: https://localhost:8080/
 paths:
 
-  /location/{lid}:
+  /api/location/{lid}:
     get:
       summary: Location
       description: Get a location.
@@ -89,7 +89,7 @@ paths:
         '404':
           $ref: '#/components/responses/Error'
 
-  /locations:
+  /api/locations:
     get:
       summary: Locations
       description: Get a list of locations.
@@ -104,7 +104,7 @@ paths:
         '500':
           $ref: '#/components/responses/Error'
 
-  /box/{bid}:
+  /api/box/{bid}:
     get:
       summary: Get a box
       description: |
@@ -180,7 +180,7 @@ paths:
         '404':
           $ref: '#/components/responses/Error'
 
-  /boxes:
+  /api/boxes:
     get:
       summary: Boxes
       description: Get a list of boxes.
@@ -196,7 +196,7 @@ paths:
         '500':
           $ref: '#/components/responses/Error'
 
-  /content/{cid}:
+  /api/content/{cid}:
     get:
       summary: Content
       description: Get a content.
@@ -270,7 +270,7 @@ paths:
         '404':
           $ref: '#/components/responses/Error'
 
-  /contents:
+  /api/contents:
     get:
       summary: Contents
       description: Get a list of contents.
diff --git a/database/query.sql b/database/query.sql
index 4e8da7ca68022db13f15574dff5109ca55de15d3..ac8dc0b75a9424841f700b7699b793d3eaaef1d4 100644
--- a/database/query.sql
+++ b/database/query.sql
@@ -82,4 +82,16 @@ SELECT * FROM contents;
 SELECT * FROM contents
 WHERE description LIKE ?;
 
+-- name: GetContentsWithBox :many
+SELECT * FROM contents
+WHERE box = ?;
+
+-- name: GetContentsWithLocation :many
+SELECT c.* FROM contents AS c, boxes AS b
+WHERE c.box = b.id AND b.location = ?;
+
+-- name: GetContentsWithTag :many
+SELECT * FROM contents
+WHERE tags LIKE ?;
+
 -- vim:et
diff --git a/server/contents.go b/server/contents.go
index f7cb4a26593f195abf714feab47d106c4e7b0420..4e4024e0ce80791a77435cd455a093cc97613370 100644
--- a/server/contents.go
+++ b/server/contents.go
@@ -135,10 +135,24 @@ func (ep *Endpoints) DeleteContent(ctx context.Context, req api.DeleteContentReq
 func (ep *Endpoints) GetContents(ctx context.Context, req api.GetContentsRequestObject) (api.GetContentsResponseObject, error) {
 	var contents []store.Content
 	var err error
-	if req.Params.Filter == nil {
-		contents, err = ep.db.GetContents(ctx)
-	} else {
+	if req.Params.Filter != nil {
 		contents, err = ep.db.GetContentsWithFilter(ctx, fmt.Sprintf("%%%s%%", *req.Params.Filter))
+	} else if req.Params.Box == nil {
+		contents, err = ep.db.GetContentsWithBox(ctx, *req.Params.Box)
+	} else if req.Params.Location == nil {
+		contents, err = ep.db.GetContentsWithLocation(ctx, *req.Params.Location)
+	} else if req.Params.Tags == nil {
+		if len(*req.Params.Tags) == 1 {
+			contents, err = ep.db.GetContentsWithTag(ctx, sql.NullString{
+				String: fmt.Sprintf("%%,%s,%%", (*req.Params.Tags)[0]),
+				Valid:  true,
+			})
+		} else {
+			// TODO: figure out more than one tag.
+			contents, err = ep.db.GetContents(ctx)
+		}
+	} else {
+		contents, err = ep.db.GetContents(ctx)
 	}
 	if err != nil {
 		log.Printf("error: %+v", err)
diff --git a/server/server.go b/server/server.go
index 19952150e7e27e4f9cf6de33e686af1b85d568b3..1673dcb46aea3468ad480ceb59ac9f4b9a3fe01f 100644
--- a/server/server.go
+++ b/server/server.go
@@ -5,12 +5,16 @@ import (
 	"context"
 	"database/sql"
 	"fmt"
+	"io/fs"
+	"mime"
+	"net/http"
 
 	// The sqlite db and migration driver.
 	_ "github.com/golang-migrate/migrate/v4/database/sqlite"
 
 	"git.lyda.ie/kevin/boxes/api"
 	"git.lyda.ie/kevin/boxes/database"
+	"git.lyda.ie/kevin/boxes/web"
 	"github.com/golang-migrate/migrate/v4"
 	"github.com/golang-migrate/migrate/v4/source/iofs"
 	"github.com/labstack/echo/v4"
@@ -19,6 +23,10 @@ import (
 	"modernc.org/sqlite"
 )
 
+func init() {
+	mime.AddExtensionType(".ico", "image/vnd.microsoft.icon")
+}
+
 // Run starts the metrics and API servers.
 func Run(cmd *cobra.Command, _ []string) {
 	// Get configuration and basic pieces.
@@ -55,6 +63,14 @@ func Run(cmd *cobra.Command, _ []string) {
 	db, err := sql.Open("sqlite", dbfile)
 	cobra.CheckErr(err)
 
+	// Get the static files served.
+	filesTree, err := fs.Sub(web.Files, "files")
+	cobra.CheckErr(err)
+	filesHandler := http.FileServer(http.FS(filesTree))
+	e.GET("/", echo.WrapHandler(filesHandler))
+	e.GET("/*.*", echo.WrapHandler(filesHandler))
+	//e.GET("/images/*", echo.WrapHandler(http.StripPrefix("/images/", filesHandler)))
+
 	// Get the endpoint handler set up.
 	endpoints := NewEndpoints(db)
 	server := api.NewStrictHandler(endpoints, nil)
diff --git a/web/files/favicon.ico b/web/files/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..6d1f201f8a6cbfac967b665da57701b99dd3e183
Binary files /dev/null and b/web/files/favicon.ico differ
diff --git a/web/files/images/icon-192x192.png b/web/files/images/icon-192x192.png
new file mode 100644
index 0000000000000000000000000000000000000000..dd77ff709ad3aa6362a6071593671b00fee0f357
Binary files /dev/null and b/web/files/images/icon-192x192.png differ
diff --git a/web/files/images/icon-512x512.png b/web/files/images/icon-512x512.png
new file mode 100644
index 0000000000000000000000000000000000000000..5c2e10f35194116c39ea9f82f0a254ad987e7f5f
Binary files /dev/null and b/web/files/images/icon-512x512.png differ
diff --git a/web/files/index.html b/web/files/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..6acad3c9e39582cf1652423ab3e3eae4500c94a1
--- /dev/null
+++ b/web/files/index.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<html>
+<head>
+
+<!-- Responsive -->
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<meta http-equiv="X-UA-Compatible" content="ie=edge">
+
+<!-- Title -->
+<title>PWA Tutorial</title>
+
+<!-- Meta Tags required for
+	Progressive Web App -->
+<meta name="apple-mobile-web-app-status-bar" content="#aa7700">
+<meta name="theme-color" content="black">
+
+<!-- Manifest File link -->
+<link rel="manifest" href="manifest.json">
+</head>
+
+<body>
+<h1>Boxes</h1>
+
+<div id="app"></div>
+
+<footer>
+  <p>Credits:</p>
+  <ul>
+    <li><a href="https://www.flaticon.com/free-icons/open-box" title="open box icons">Open box icons created by yoyonpujiono - Flaticon</a></li>
+  </ul>
+</footer>
+
+<!--
+<script src="https://cdnjs.cloudflare.com/ajax/libs/mithril/2.2.11/mithril.min.js" integrity="sha512-2/N5u5OSxz7VyKmGbko8Jwx6o8fudoJ70t/Nvu16UoKCyqeDNfvxkDTmj11ibe4fwXSDdojQAxuV+y1Ut1JSkQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
+-->
+<script src="https://cdnjs.cloudflare.com/ajax/libs/mithril/2.2.11/mithril.js" integrity="sha512-HQxrYG+jkimFdOc2fdtpe7urylm7yRsmYekrLMACDZk8GwF7UBnEfjG/e86r+lPUvfNsifVdQKK5iS6IBe70Og==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
+<script>
+  var LocationsRoute = {
+    view: function() {
+        return m("p", "TODO: a list of locations");
+    }
+  }
+
+  var BoxesRoute = {
+    view: function() {
+        return m("p", "TODO: a list of boxes");
+    }
+  }
+
+  var ContentsRoute = {
+    view: function() {
+        return m("p", "TODO: a list of contents");
+    }
+  }
+
+	window.addEventListener('load', () => {
+    registerSW();
+    var root = document.getElementById("app");
+    m.route(root,
+      "/", {
+        "/": LocationsRoute,
+        "/locations": LocationsRoute,
+        "/Boxes": BoxesRoute,
+        "/Contents": ContentsRoute,
+      }
+    );
+	});
+
+	// Register the Service Worker
+	async function registerSW() {
+    if ('serviceWorker' in navigator) {
+      try {
+      await navigator
+          .serviceWorker
+          .register('serviceworker.js');
+      }
+      catch (e) {
+      console.log('SW registration failed');
+      }
+    }
+    console.log('SW registration succeeded');
+  }
+
+</script>
+</body>
+</html>
diff --git a/web/files/manifest.json b/web/files/manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..05e08e152562ddfa3b4cd9bf0472a248dda01793
--- /dev/null
+++ b/web/files/manifest.json
@@ -0,0 +1,22 @@
+{
+	"name":"Boxes",
+	"short_name":"PWA",
+	"start_url":"index.html",
+	"display":"standalone",
+	"background_color":"#5900b3",
+	"theme_color":"black",
+	"scope": ".",
+	"description":"This is a home inventory tool.",
+	"icons":[
+	{
+	"src":"images/icon-192x192.png",
+	"sizes":"192x192",
+	"type":"image/png"
+	},
+	{
+	"src":"images/icon-512x512.png",
+	"sizes":"512x512",
+	"type":"image/png"
+	}
+]
+}
diff --git a/web/files/serviceworker.js b/web/files/serviceworker.js
new file mode 100644
index 0000000000000000000000000000000000000000..b00e37163e1136db2b76151cb5331682b1f1ed9c
--- /dev/null
+++ b/web/files/serviceworker.js
@@ -0,0 +1,19 @@
+var staticCacheName = "pwa";
+
+self.addEventListener("install", function (e) {
+  e.waitUntil(
+    caches.open(staticCacheName).then(function (cache) {
+    return cache.addAll(["/"]);
+    })
+  );
+});
+
+self.addEventListener("fetch", function (event) {
+  console.log(event.request.url);
+
+  event.respondWith(
+    caches.match(event.request).then(function (response) {
+    return response || fetch(event.request);
+    })
+  );
+});
diff --git a/web/web.go b/web/web.go
new file mode 100644
index 0000000000000000000000000000000000000000..9bd1c4e4fc53b8ddd764c96b042d2295f4be06d2
--- /dev/null
+++ b/web/web.go
@@ -0,0 +1,9 @@
+// Package web contains the files for the static web elements.
+package web
+
+import "embed"
+
+// Files contains all the static files that make up the website.
+//
+//go:embed files
+var Files embed.FS