diff --git a/LICENSE.md b/LICENSE.md
index 1d4c39d..4a6478c 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1 +1 @@
-See [public/licenses.txt](public/licenses.txt)
+See [public/licenses.toml](public/licenses.toml)
diff --git a/public/licenses.toml b/public/licenses.toml
new file mode 100644
index 0000000..0b04dfb
--- /dev/null
+++ b/public/licenses.toml
@@ -0,0 +1,72 @@
+# licenses.toml
+
+[copyright]
+title = "gallery.badmanners.xyz"
+description = "Bad Manners's self-hosted gallery."
+date = "2024"
+author = "Bad Manners <me@badmanners.xyz>"
+source = "https://git.badmanners.xyz/badmanners/gallery.badmanners.xyz"
+license = { name = "MIT", url = "https://opensource.org/license/mit" }
+notes = """
+All rights reserved.
+The MIT License applies only to the source code; see additional copyrights for details."""
+
+[[copyright.additional]]
+notes = "The briefcase logo is copyrighted and trademarked by me. All rights reserved."
+
+[[copyright.additional]]
+notes = """
+My characters, whether directly attributed to me or unattributed, are copyrighted and trademarked by me.
+All rights reserved."""
+
+[[copyright.additional]]
+description = "Content hosted on this website, i.e. the stories and game(s)."
+date = "2022-2024"
+author = "Bad Manners <me@badmanners.xyz>"
+license = { name = "CC-BY-NC-ND-4.0", url = "https://creativecommons.org/licenses/by-nc-nd/4.0/" }
+notes = "All rights reserved."
+
+[[copyright.additional]]
+notes = """
+All third-party copyrights, trademarks, and attributed characters belong to their respective owners, \
+and I'm not affiliated with any of them."""
+
+[[attributions]]
+title = "Noto Sans"
+type = "font"
+author = "Noto Project Authors"
+source = "https://github.com/notofonts/latin-greek-cyrillic"
+license = { name = "SIL Open Font License v1.1", url = "https://opensource.org/license/ofl-1-1" }
+
+[[attributions]]
+title = "Noto Serif"
+type = "font"
+author = "Noto Project Authors"
+source = "https://github.com/notofonts/latin-greek-cyrillic"
+license = { name = "SIL Open Font License v1.1", url = "https://opensource.org/license/ofl-1-1" }
+
+[[attributions]]
+title = "Font Awesome"
+description = "Generic icons."
+type = "icons"
+source = "https://fontawesome.com"
+license = { name = "CC-BY-4.0", url = "https://creativecommons.org/licenses/by/4.0/" }
+icons = [
+  "arrow-back",
+  "arrow-up",
+  "book",
+  "briefcase",
+  "chevron-left",
+  "chevron-right",
+  "circle-info",
+  "gamepad",
+  "home",
+  "magnifying-glass",
+  "moon",
+  "retweet",
+  "square-rss",
+  "star",
+  "sun",
+  "tags",
+  "triangle-exclamation",
+]
diff --git a/public/licenses.txt b/public/licenses.txt
deleted file mode 100644
index 4356766..0000000
--- a/public/licenses.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-The source code of this website is licensed under the MIT License: https://opensource.org/license/mit
-
-The stories and games hosted on the website are copyrighted by me and licensed under CC-BY-NC-ND-4.0: https://creativecommons.org/licenses/by-nc-nd/4.0/
-
-The briefcase logo, my characters, and any unattributed characters are copyrighted and trademarked by me, Bad Manners.
-
-The Noto Sans and Noto Serif typefaces are copyrighted to the Noto Project Authors and distributed under the SIL Open Font License v1.1: https://opensource.org/license/ofl-1-1
-
-The generic SVG icons were created by Font Awesome and are distributed under CC-BY-4.0: https://creativecommons.org/licenses/by/4.0/
-
-All third-party trademarks and attributed characters belong to their respective owners, and I (Bad Manners) am not affiliated with any of them.
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..e5898eb
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Disallow: .htaccess
+Disallow: /stories/drafts/
+Disallow: /games/drafts/
diff --git a/scripts/deploy-lftp.ts b/scripts/deploy-lftp.ts
index bfafbf0..c6c5914 100644
--- a/scripts/deploy-lftp.ts
+++ b/scripts/deploy-lftp.ts
@@ -28,9 +28,7 @@ async function deployLftp({ host, user, password, targetFolder, sourceFolder, as
     },
   );
   await new Promise((resolve, reject) => {
-    process.on("close", (code) =>
-      (code === 0) ? resolve(0) : reject(`lftp failed with code ${code}`),
-    );
+    process.on("close", (code) => (code === 0 ? resolve(0) : reject(`lftp failed with code ${code}`)));
   });
 }
 
diff --git a/scripts/export-story.ts b/scripts/export-story.ts
index f594444..9987fdc 100644
--- a/scripts/export-story.ts
+++ b/scripts/export-story.ts
@@ -33,8 +33,8 @@ const fetchRetry = fetchRetryWrapper(global.fetch);
 
 const isLibreOfficeRunning = async () =>
   new Promise<boolean>((res) => {
-    spawn("ps", ["-ax"], {stdio: 'pipe'});
-    const lines = createInterface({ input: spawn("ps", ["-ax"], {stdio: 'pipe'}).stdout });
+    spawn("ps", ["-ax"], { stdio: "pipe" });
+    const lines = createInterface({ input: spawn("ps", ["-ax"], { stdio: "pipe" }).stdout });
     lines.on("line", (line) => {
       if (line.includes("libreoffice") && line.includes("--writer")) {
         res(true);
@@ -65,11 +65,11 @@ async function exportStory(slug: string, options: { outputDir: string }) {
 
   /* Spawn Astro dev server */
   console.log("Starting Astro development server...");
-  const devServerProcess = spawn("./node_modules/.bin/astro", ["dev"], { stdio: 'pipe' });
+  const devServerProcess = spawn("./node_modules/.bin/astro", ["dev"], { stdio: "pipe" });
   let astroURL: string | null = null;
   try {
     astroURL = await new Promise<string>((resolve, reject) => {
-      const localServerRegex = /Local\s+(http:\/\/\S+)/
+      const localServerRegex = /Local\s+(http:\/\/\S+)/;
       const lines = createInterface({ input: devServerProcess.stdout });
       lines.on("line", (line) => {
         const match = localServerRegex.exec(line);
@@ -104,13 +104,11 @@ async function exportStory(slug: string, options: { outputDir: string }) {
     if (!response.ok) {
       throw new Error(`Failed to reach API (status code ${response.status})`);
     }
-    const data: { story: string, description: Record<string, string>, thumbnail: string | null } = await response.json();
+    const data: { story: string; description: Record<string, string>; thumbnail: string | null } =
+      await response.json();
     await Promise.all(
       Object.entries(data.description).map(async ([filename, description]) => {
-        return await writeFile(
-          join(outputDir, filename),
-          description,
-        );
+        return await writeFile(join(outputDir, filename), description);
       }),
     );
     if (data.thumbnail) {
@@ -144,11 +142,9 @@ async function exportStory(slug: string, options: { outputDir: string }) {
   await writeFile(join(outputDir, `${slug}.md`), storyText.replaceAll(/=(?==)/g, "= ").replaceAll("*", "\\*"));
   const tempDir = await mkdtemp(join(tmpdir(), "export-story-"));
   await writeFile(join(tempDir, "temp.txt"), storyText.replaceAll(/\n\n+/g, "\n"));
-  spawnSync(
-    "libreoffice",
-    ["--convert-to", "rtf:Rich Text Format", "--outdir", tempDir, join(tempDir, "temp.txt")],
-    { stdio: "ignore" },
-  );
+  spawnSync("libreoffice", ["--convert-to", "rtf:Rich Text Format", "--outdir", tempDir, join(tempDir, "temp.txt")], {
+    stdio: "ignore",
+  });
   const rtfText = await readFile(join(tempDir, "temp.rtf"), "utf-8");
   const rtfStyles = getRTFStyles(rtfText);
   await writeFile(