diff --git a/site/public/robots.txt b/site/public/robots.txt
new file mode 100644
index 00000000..df4a6efe
--- /dev/null
+++ b/site/public/robots.txt
@@ -0,0 +1,5 @@
+User-agent: *
+Allow: /
+Disallow: /admin/
+
+Sitemap: https://questlog.cc/sitemap.xml
diff --git a/site/public/sitemap.xml b/site/public/sitemap.xml
new file mode 100644
index 00000000..0d0848ef
--- /dev/null
+++ b/site/public/sitemap.xml
@@ -0,0 +1,33 @@
+
+
+
+ https://questlog.cc/
+ 2026-10-22
+ weekly
+ 1.0
+
+
+ https://questlog.cc/builder
+ 2026-10-22
+ weekly
+ 0.9
+
+
+ https://questlog.cc/wiki
+ 2026-10-22
+ weekly
+ 0.8
+
+
+ https://questlog.cc/cards
+ 2026-10-22
+ weekly
+ 0.7
+
+
+ https://questlog.cc/status
+ 2026-10-22
+ daily
+ 0.3
+
+
diff --git a/src/server/mod.rs b/src/server/mod.rs
index 03fea340..d12d136f 100644
--- a/src/server/mod.rs
+++ b/src/server/mod.rs
@@ -115,6 +115,8 @@ pub fn router(app: Arc>) -> Router {
.route("/builder", get(|| async { static_file("builder.html", PAGE) }))
.route("/status", get(|| async { static_file("status.html", PAGE) }))
.route("/cards", get(|| async { static_file("cards.html", PAGE) }))
+ .route("/robots.txt", get(|| async { static_file("robots.txt", PAGE) }))
+ .route("/sitemap.xml", get(|| async { static_file("sitemap.xml", PAGE) }))
.route("/admin/status", get(admin_status::))
.route("/admin/health", get(admin_health::))
// the admin page's shell is only served through its guarded address
@@ -435,6 +437,12 @@ mod tests {
assert_eq!(cc, if path == "/static/index.html" { ASSET } else { PAGE }, "{path}");
assert!(html && !body.is_empty());
}
+ for (path, ct, has) in [("/robots.txt", "text/plain; charset=utf-8", "Sitemap: https://questlog.cc/sitemap.xml"),
+ ("/sitemap.xml", "application/xml; charset=utf-8", "https://questlog.cc/wiki")] {
+ let (st, got, _, body) = raw(&format!("{base}{path}")).await;
+ assert_eq!((st, got.as_str()), (200, ct), "{path}");
+ assert!(String::from_utf8_lossy(&body).contains(has), "{path}");
+ }
for bad in ["/static/missing.css", "/static/", "/static/%2e%2e/Cargo.toml", "/static/..%2fCargo.toml", "/static/../Cargo.toml"] {
assert_eq!(raw(&format!("{base}{bad}")).await.0, 404, "{bad}");
}
diff --git a/src/server/pages.rs b/src/server/pages.rs
index a16aa1f8..62c832af 100644
--- a/src/server/pages.rs
+++ b/src/server/pages.rs
@@ -11,6 +11,7 @@ pub(super) fn content_type(path: &str) -> &'static str {
"css" => "text/css; charset=utf-8",
"js" => "text/javascript; charset=utf-8",
"json" => "application/json",
+ "xml" => "application/xml; charset=utf-8",
"txt" => "text/plain; charset=utf-8",
"png" => "image/png",
"webp" => "image/webp",