diff --git a/.changeset/hot-llamas-clap.md b/.changeset/hot-llamas-clap.md
new file mode 100644
index 0000000..6bb275b
--- /dev/null
+++ b/.changeset/hot-llamas-clap.md
@@ -0,0 +1,5 @@
+---
+"nsite-ts": minor
+---
+
+Add simple landing page
diff --git a/.env b/.env
index bafcc40..30a8ff4 100644
--- a/.env
+++ b/.env
@@ -1,6 +1,2 @@
 CACHE_PATH="in-memory"
-
-NOSTR_RELAYS=wss://nos.lol,wss://relay.damus.io
-BLOSSOM_SERVERS=https://cdn.hzrd149.com
-
-NGINX_HOST='nginx'
+LOOKUP_RELAYS=wss://user.kindpag.es,wss://purplepag.es
diff --git a/docker-compose.yml b/docker-compose.yml
index 6dd837c..3035b7c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,7 +15,8 @@ services:
 
   nsite:
     build: .
+    image: ghcr.io/hzrd149/nsite-ts:master
     environment:
-      NOSTR_RELAYS: wss://nos.lol,wss://relay.damus.io
+      LOOKUP_RELAYS: wss://user.kindpag.es,wss://purplepag.es
     ports:
       - 3000:80
diff --git a/nginx/default.conf b/nginx/default.conf
index 8ff7ac4..c946f5f 100644
--- a/nginx/default.conf
+++ b/nginx/default.conf
@@ -6,7 +6,7 @@ server {
 		proxy_cache request_cache;
 		proxy_cache_valid 200 60m;
 		proxy_cache_valid 404 10m;
-		proxy_cache_key $scheme$proxy_host$request_uri;
+		proxy_cache_key $scheme$host$request_uri;
 		proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
 
 		add_header X-Cache $upstream_cache_status;
diff --git a/public/index.html b/public/index.html
index 24ab39d..9e3679a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -14,33 +14,19 @@
     </script>
   </head>
   <body>
-    <label>relays</label>
-    <br />
-    <textarea type="text" id="relays" cols="50" rows="4"></textarea>
-    <br />
-    <br />
-    <label>blossom servers</label>
-    <br />
-    <textarea type="text" id="servers" cols="50" rows="4"></textarea>
-    <br />
-    <br />
-    <input type="file" id="files" webkitdirectory directory multiple />
-    <button id="upload-button">Upload nsite</button>
-    <div
-      id="log"
-      style="
-        max-height: 50em;
-        max-width: 80em;
-        width: 100%;
-        border: 1px solid gray;
-        min-height: 8em;
-        margin: 0.5em 0;
-        overflow: auto;
-        font-size: 0.8em;
-        gap: 0.1em;
-        white-space: pre;
-      "
-    ></div>
+    <h1>nsite-ts</h1>
+    <a href="https://github.com/hzrd149/nsite-ts" target="_blank">Source Code</a>
+
+    <h2>Latest nsites:</h2>
+    <div id="sites"></div>
+
+    <template id="site">
+      <div class="site">
+        <a class="pubkey link"></a>
+        <span class="date"></span>
+      </div>
+    </template>
+
     <script type="module" src="./main.js"></script>
   </body>
 </html>
diff --git a/public/main.js b/public/main.js
index a0dda2b..92ba0ed 100644
--- a/public/main.js
+++ b/public/main.js
@@ -1,142 +1,32 @@
-import { multiServerUpload, BlossomClient } from "blossom-client-sdk";
-import { SimplePool } from "nostr-tools";
+import { nip19, SimplePool } from "nostr-tools";
 
-const logContainer = document.getElementById("log");
-function log(...args) {
-  const el = document.createElement("div");
-  el.innerText = args.join(" ");
-  logContainer.appendChild(el);
-}
+const seen = new Set();
+function addSite(event) {
+  if (seen.has(event.pubkey)) return;
+  seen.add(event.pubkey);
 
-const uploadButton = document.getElementById("upload-button");
+  try {
+    const template = document.getElementById("site");
+    const site = template.content.cloneNode(true);
+    const npub = nip19.npubEncode(event.pubkey);
 
-/** @type {HTMLInputElement} */
-const filesInput = document.getElementById("files");
+    site.querySelector(".pubkey").textContent = npub;
+    site.querySelector(".link").href = new URL("/", `${location.protocol}//${npub}.${location.host}`).toString();
 
-/**
- * @param {FileSystemFileEntry} fileEntry
- * @returns {File}
- */
-export function readFileSystemFile(fileEntry) {
-  return new Promise((res, rej) => {
-    fileEntry.file(
-      (file) => res(file),
-      (err) => rej(err),
-    );
-  });
-}
-
-/**
- * @param {FileSystemDirectoryEntry} directory
- * @returns {FileSystemEntry[]}
- */
-export function readFileSystemDirectory(directory) {
-  return new Promise((res, rej) => {
-    directory.createReader().readEntries(
-      (entries) => res(entries),
-      (err) => rej(err),
-    );
-  });
-}
-
-/**
- * uploads a file system entry to blossom servers
- * @param {FileSystemEntry} entry
- * @returns {{file: File, path: string, sha256: string}[]}
- */
-async function readFileSystemEntry(entry) {
-  const files = [];
-  if (entry instanceof FileSystemFileEntry && entry.isFile) {
-    try {
-      const file = await readFileSystemFile(entry);
-      const sha256 = await BlossomClient.getFileSha256(file);
-      const path = entry.fullPath;
-
-      files.push({ file, path, sha256 });
-    } catch (e) {
-      log("Failed to add" + entry.fullPath);
-      log(e.message);
-    }
-  } else if (entry instanceof FileSystemDirectoryEntry && entry.isDirectory) {
-    const entries = await readFileSystemDirectory(entry);
-    for (const e of entries) files.push(...(await readFileSystemEntry(e)));
+    document.getElementById("sites").appendChild(site);
+  } catch (error) {
+    console.log("Failed to add site", event);
+    console.log(error);
   }
-
-  return files;
-}
-
-/**
- * uploads a file system entry to blossom servers
- * @param {FileList} list
- * @returns {{file: File, path: string, sha256: string}[]}
- */
-async function readFileList(list) {
-  const files = [];
-  for (const file of list) {
-    const path = file.webkitRelativePath ? file.webkitRelativePath : file.name;
-    const sha256 = await BlossomClient.getFileSha256(file);
-    files.push({ file, path, sha256 });
-  }
-  return files;
 }
 
 const pool = new SimplePool();
 
-/**
- * uploads a file system entry to blossom servers
- * @param {{file:File, path:string}} files
- * @param {import("blossom-client-sdk").Signer} signer
- * @param {*} auth
- * @param {string[]} servers
- * @param {string[]} relays
- */
-async function uploadFiles(files, signer, auth, servers, relays) {
-  for (const { file, path, sha256 } of files) {
-    try {
-      const upload = multiServerUpload(servers, file, signer, auth);
-
-      let published = false;
-      for await (let { blob } of upload) {
-        if (!published) {
-          const signed = await signer({
-            kind: 34128,
-            content: "",
-            created_at: Math.round(Date.now() / 1000),
-            tags: [
-              ["d", path],
-              ["x", sha256],
-            ],
-          });
-          await pool.publish(relays, signed);
-
-          log("Published", path, sha256, signed.id);
-        }
-      }
-    } catch (error) {
-      log(`Failed to upload ${path}`, error);
-    }
-  }
-}
-
-uploadButton.addEventListener("click", async () => {
-  if (!window.nostr) return alert("Missing NIP-07 signer");
-
-  const signer = (draft) => window.nostr.signEvent(draft);
-  const relays = document.getElementById("relays").value.split(/\n|,/);
-  const servers = document.getElementById("servers").value.split(/\n|,/);
-
-  try {
-    if (filesInput.files) {
-      const files = await readFileList(filesInput.files);
-
-      // strip leading dir
-      for (const file of files) file.path = file.path.replace(/^[^\/]+\//, "/");
-
-      log(`Found ${files.length} files`);
-
-      await uploadFiles(files, signer, undefined, servers, relays);
-    }
-  } catch (error) {
-    alert(`Failed to upload files: ${error.message}`);
-  }
-});
+console.log("Loading sites");
+pool.subscribeMany(
+  ["wss://relay.damus.io", "wss://nos.lol", "wss://nostr.wine"],
+  [{ kinds: [34128], "#d": ["/index.html"] }],
+  {
+    onevent: addSite,
+  },
+);
diff --git a/public/upload/index.html b/public/upload/index.html
new file mode 100644
index 0000000..57c2ebd
--- /dev/null
+++ b/public/upload/index.html
@@ -0,0 +1,46 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>nsite</title>
+    <script type="importmap">
+      {
+        "imports": {
+          "blossom-client-sdk": "https://esm.run/blossom-client-sdk",
+          "nostr-tools": "https://esm.run/nostr-tools"
+        }
+      }
+    </script>
+  </head>
+  <body>
+    <label>relays</label>
+    <br />
+    <textarea type="text" id="relays" cols="50" rows="4"></textarea>
+    <br />
+    <br />
+    <label>blossom servers</label>
+    <br />
+    <textarea type="text" id="servers" cols="50" rows="4"></textarea>
+    <br />
+    <br />
+    <input type="file" id="files" webkitdirectory directory multiple />
+    <button id="upload-button">Upload nsite</button>
+    <div
+      id="log"
+      style="
+        max-height: 50em;
+        max-width: 80em;
+        width: 100%;
+        border: 1px solid gray;
+        min-height: 8em;
+        margin: 0.5em 0;
+        overflow: auto;
+        font-size: 0.8em;
+        gap: 0.1em;
+        white-space: pre;
+      "
+    ></div>
+    <script type="module" src="./upload.js"></script>
+  </body>
+</html>
diff --git a/public/upload/upload.js b/public/upload/upload.js
new file mode 100644
index 0000000..a0dda2b
--- /dev/null
+++ b/public/upload/upload.js
@@ -0,0 +1,142 @@
+import { multiServerUpload, BlossomClient } from "blossom-client-sdk";
+import { SimplePool } from "nostr-tools";
+
+const logContainer = document.getElementById("log");
+function log(...args) {
+  const el = document.createElement("div");
+  el.innerText = args.join(" ");
+  logContainer.appendChild(el);
+}
+
+const uploadButton = document.getElementById("upload-button");
+
+/** @type {HTMLInputElement} */
+const filesInput = document.getElementById("files");
+
+/**
+ * @param {FileSystemFileEntry} fileEntry
+ * @returns {File}
+ */
+export function readFileSystemFile(fileEntry) {
+  return new Promise((res, rej) => {
+    fileEntry.file(
+      (file) => res(file),
+      (err) => rej(err),
+    );
+  });
+}
+
+/**
+ * @param {FileSystemDirectoryEntry} directory
+ * @returns {FileSystemEntry[]}
+ */
+export function readFileSystemDirectory(directory) {
+  return new Promise((res, rej) => {
+    directory.createReader().readEntries(
+      (entries) => res(entries),
+      (err) => rej(err),
+    );
+  });
+}
+
+/**
+ * uploads a file system entry to blossom servers
+ * @param {FileSystemEntry} entry
+ * @returns {{file: File, path: string, sha256: string}[]}
+ */
+async function readFileSystemEntry(entry) {
+  const files = [];
+  if (entry instanceof FileSystemFileEntry && entry.isFile) {
+    try {
+      const file = await readFileSystemFile(entry);
+      const sha256 = await BlossomClient.getFileSha256(file);
+      const path = entry.fullPath;
+
+      files.push({ file, path, sha256 });
+    } catch (e) {
+      log("Failed to add" + entry.fullPath);
+      log(e.message);
+    }
+  } else if (entry instanceof FileSystemDirectoryEntry && entry.isDirectory) {
+    const entries = await readFileSystemDirectory(entry);
+    for (const e of entries) files.push(...(await readFileSystemEntry(e)));
+  }
+
+  return files;
+}
+
+/**
+ * uploads a file system entry to blossom servers
+ * @param {FileList} list
+ * @returns {{file: File, path: string, sha256: string}[]}
+ */
+async function readFileList(list) {
+  const files = [];
+  for (const file of list) {
+    const path = file.webkitRelativePath ? file.webkitRelativePath : file.name;
+    const sha256 = await BlossomClient.getFileSha256(file);
+    files.push({ file, path, sha256 });
+  }
+  return files;
+}
+
+const pool = new SimplePool();
+
+/**
+ * uploads a file system entry to blossom servers
+ * @param {{file:File, path:string}} files
+ * @param {import("blossom-client-sdk").Signer} signer
+ * @param {*} auth
+ * @param {string[]} servers
+ * @param {string[]} relays
+ */
+async function uploadFiles(files, signer, auth, servers, relays) {
+  for (const { file, path, sha256 } of files) {
+    try {
+      const upload = multiServerUpload(servers, file, signer, auth);
+
+      let published = false;
+      for await (let { blob } of upload) {
+        if (!published) {
+          const signed = await signer({
+            kind: 34128,
+            content: "",
+            created_at: Math.round(Date.now() / 1000),
+            tags: [
+              ["d", path],
+              ["x", sha256],
+            ],
+          });
+          await pool.publish(relays, signed);
+
+          log("Published", path, sha256, signed.id);
+        }
+      }
+    } catch (error) {
+      log(`Failed to upload ${path}`, error);
+    }
+  }
+}
+
+uploadButton.addEventListener("click", async () => {
+  if (!window.nostr) return alert("Missing NIP-07 signer");
+
+  const signer = (draft) => window.nostr.signEvent(draft);
+  const relays = document.getElementById("relays").value.split(/\n|,/);
+  const servers = document.getElementById("servers").value.split(/\n|,/);
+
+  try {
+    if (filesInput.files) {
+      const files = await readFileList(filesInput.files);
+
+      // strip leading dir
+      for (const file of files) file.path = file.path.replace(/^[^\/]+\//, "/");
+
+      log(`Found ${files.length} files`);
+
+      await uploadFiles(files, signer, undefined, servers, relays);
+    }
+  } catch (error) {
+    alert(`Failed to upload files: ${error.message}`);
+  }
+});
diff --git a/src/env.ts b/src/env.ts
index d31b1be..57cdc4f 100644
--- a/src/env.ts
+++ b/src/env.ts
@@ -1,6 +1,7 @@
 import "dotenv/config";
 import xbytes from "xbytes";
 
+const LOOKUP_RELAYS = process.env.LOOKUP_RELAYS?.split(",") ?? ["wss://user.kindpag.es/", "wss://purplepag.es/"];
 const NOSTR_RELAYS = process.env.NOSTR_RELAYS?.split(",") ?? [];
 const BLOSSOM_SERVERS = process.env.BLOSSOM_SERVERS?.split(",") ?? [];
 
@@ -9,6 +10,4 @@ const MAX_FILE_SIZE = process.env.MAX_FILE_SIZE ? xbytes.parseSize(process.env.M
 const NGINX_HOST = process.env.NGINX_HOST;
 const CACHE_PATH = process.env.CACHE_PATH;
 
-if (NOSTR_RELAYS.length === 0) throw new Error("Requires at least one relay in NOSTR_RELAYS");
-
-export { NOSTR_RELAYS, BLOSSOM_SERVERS, MAX_FILE_SIZE, NGINX_HOST, CACHE_PATH };
+export { NOSTR_RELAYS, LOOKUP_RELAYS, BLOSSOM_SERVERS, MAX_FILE_SIZE, NGINX_HOST, CACHE_PATH };
diff --git a/src/events.ts b/src/events.ts
index 584de88..417b103 100644
--- a/src/events.ts
+++ b/src/events.ts
@@ -1,6 +1,7 @@
 import { extname, isAbsolute, join } from "path";
 import { NSITE_KIND } from "./const.js";
 import ndk from "./ndk.js";
+import { NDKRelaySet } from "@nostr-dev-kit/ndk";
 
 export function getSearchPaths(path: string) {
   const paths = [path];
@@ -10,10 +11,10 @@ export function getSearchPaths(path: string) {
 
   // also look for relative paths
   for (const p of Array.from(paths)) {
-    if (isAbsolute(p)) paths.push(path.replace(/^\//, ""));
+    if (isAbsolute(p)) paths.push(p.replace(/^\//, ""));
   }
 
-  return paths;
+  return paths.filter((p) => !!p);
 }
 
 export function parseNsiteEvent(event: { pubkey: string; tags: string[][] }) {
@@ -28,9 +29,13 @@ export function parseNsiteEvent(event: { pubkey: string; tags: string[][] }) {
     };
 }
 
-export async function getNsiteBlobs(pubkey: string, path: string) {
+export async function getNsiteBlobs(pubkey: string, path: string, relays?: string[]) {
   const paths = getSearchPaths(path);
-  const events = await ndk.fetchEvents({ kinds: [NSITE_KIND], "#d": paths, authors: [pubkey] });
+  const events = await ndk.fetchEvents(
+    { kinds: [NSITE_KIND], "#d": paths, authors: [pubkey] },
+    {},
+    relays && NDKRelaySet.fromRelayUrls(relays, ndk, true),
+  );
 
   return Array.from(events)
     .map(parseNsiteEvent)
diff --git a/src/index.ts b/src/index.ts
index 43d9976..8385149 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -13,7 +13,8 @@ import { resolveNpubFromHostname } from "./helpers/dns.js";
 import { getNsiteBlobs } from "./events.js";
 import { downloadFile, getUserBlossomServers } from "./blossom.js";
 import { BLOSSOM_SERVERS } from "./env.js";
-import { userServers } from "./cache.js";
+import { userRelays, userServers } from "./cache.js";
+import { getUserOutboxes } from "./ndk.js";
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
@@ -49,8 +50,16 @@ app.use(async (ctx, next) => {
   const pubkey = (ctx.state.pubkey = await resolveNpubFromHostname(ctx.hostname));
 
   if (pubkey) {
+    console.log(`${pubkey}: Fetching relays`);
+
+    let relays = await userRelays.get<string[] | undefined>(pubkey);
+    if (!relays) {
+      relays = await getUserOutboxes(pubkey);
+      if (relays) await userRelays.set(pubkey, relays);
+    }
+
     console.log(`${pubkey}: Searching for ${ctx.path}`);
-    const blobs = await getNsiteBlobs(pubkey, ctx.path);
+    const blobs = await getNsiteBlobs(pubkey, ctx.path, relays);
 
     if (blobs.length === 0) {
       ctx.status = 404;
@@ -58,7 +67,7 @@ app.use(async (ctx, next) => {
       return;
     }
 
-    let servers = await userServers.get<string[]>(pubkey);
+    let servers = await userServers.get<string[] | undefined>(pubkey);
     if (!servers) {
       console.log(`${pubkey}: Searching for blossom servers`);
       servers = (await getUserBlossomServers(pubkey)) ?? [];
diff --git a/src/ndk.ts b/src/ndk.ts
index 69f4729..4a00495 100644
--- a/src/ndk.ts
+++ b/src/ndk.ts
@@ -1,10 +1,17 @@
 import NDK from "@nostr-dev-kit/ndk";
-import { NOSTR_RELAYS } from "./env.js";
+import { LOOKUP_RELAYS, NOSTR_RELAYS } from "./env.js";
 
 const ndk = new NDK({
-  explicitRelayUrls: NOSTR_RELAYS,
+  explicitRelayUrls: [...LOOKUP_RELAYS, ...NOSTR_RELAYS],
 });
 
 ndk.connect();
 
+export async function getUserOutboxes(pubkey: string) {
+  const mailboxes = await ndk.fetchEvent({ kinds: [10002], authors: [pubkey] });
+  if (!mailboxes) return;
+
+  return mailboxes.tags.filter((t) => t[0] === "r" && (t[2] === undefined || t[2] === "write")).map((t) => t[1]);
+}
+
 export default ndk;