diff --git a/launch/verify-launch.mjs b/launch/verify-launch.mjs
index 40edc11..97eab13 100644
--- a/launch/verify-launch.mjs
+++ b/launch/verify-launch.mjs
@@ -19,6 +19,9 @@ const report = JSON.parse(readFileSync(reportPath, "utf8"));
const index = JSON.parse(readFileSync(indexPath, "utf8"));
const requirements = JSON.parse(readFileSync(requirementsPath, "utf8"));
const html = readFileSync(join(site, "index.html"), "utf8");
+const headers = readFileSync(join(site, "_headers"), "utf8");
+const robots = readFileSync(join(site, "robots.txt"), "utf8");
+const sitemap = readFileSync(join(site, "sitemap.xml"), "utf8");
function assert(condition, message) {
if (!condition) throw new Error(message);
@@ -82,6 +85,18 @@ const requiredCopy = [
for (const copy of requiredCopy) assert(html.includes(copy), `Homepage is missing required copy: ${copy}`);
assert(!/official leaderboard[^<]{0,80}(live|launched|ready)/i.test(html), "Homepage overclaims official leaderboard readiness");
assert(html.includes("https://github.com/aronchick/gloss"), "Homepage must lead to GitHub");
+assert(html.includes(''), "Homepage canonical URL is missing");
+assert(html.includes('"codeRepository": "https://github.com/aronchick/gloss"'), "Homepage structured data must lead to GitHub");
+assert(robots.includes("Allow: /") && robots.includes("Sitemap: https://gloss.tools/sitemap.xml"), "robots.txt must expose the public site map");
+assert(sitemap.includes("https://gloss.tools/"), "sitemap.xml must contain the production homepage");
+for (const header of [
+ "X-Content-Type-Options: nosniff",
+ "Referrer-Policy: strict-origin-when-cross-origin",
+ "Permissions-Policy: camera=(), microphone=(), geolocation=()",
+ "X-Frame-Options: DENY",
+]) {
+ assert(headers.includes(header), `Cloudflare Pages header is missing: ${header}`);
+}
const localAssetPattern = /(?:href|src)="\/(?!\/)([^"?#]*)/g;
for (const match of html.matchAll(localAssetPattern)) {
diff --git a/site/_headers b/site/_headers
new file mode 100644
index 0000000..6e03f76
--- /dev/null
+++ b/site/_headers
@@ -0,0 +1,11 @@
+/*
+ X-Content-Type-Options: nosniff
+ Referrer-Policy: strict-origin-when-cross-origin
+ Permissions-Policy: camera=(), microphone=(), geolocation=()
+ X-Frame-Options: DENY
+
+/evidence/*
+ Cache-Control: public, max-age=300, must-revalidate
+
+/media/*
+ Cache-Control: public, max-age=3600
diff --git a/site/index.html b/site/index.html
index 835ab36..3ac5809 100644
--- a/site/index.html
+++ b/site/index.html
@@ -4,10 +4,14 @@
+
+
+
+
@@ -15,6 +19,19 @@
+
Skip to content
diff --git a/site/robots.txt b/site/robots.txt
new file mode 100644
index 0000000..e25e194
--- /dev/null
+++ b/site/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://gloss.tools/sitemap.xml
diff --git a/site/sitemap.xml b/site/sitemap.xml
new file mode 100644
index 0000000..9fb7907
--- /dev/null
+++ b/site/sitemap.xml
@@ -0,0 +1,8 @@
+
+
+
+ https://gloss.tools/
+ weekly
+ 1.0
+
+