diff --git a/site/docs/index.html b/site/docs/index.html
index 8d94df6c..fafec789 100644
--- a/site/docs/index.html
+++ b/site/docs/index.html
@@ -4,9 +4,21 @@
-
The humans are the day shift : you talk the idea through, write the spec and cut it into tickets. The agents work the third shift : give thirdshift an Issue URL and it implements the issue, reviews its own work and delivers a pull request ready for review, or merges it when asked. Start a Run whenever you like, or let your operating system start passes on a schedule.
+
The humans are the day shift : you talk the idea through, write the spec and cut it into tickets. Coding agents work the third shift : give thirdshift a GitHub issue URL and it implements the issue, reviews its own work and delivers a pull request ready for review, or merges it when asked. Start a Run whenever you like, or let your operating system start passes on a schedule.
How thirdshift works : an engineer's guide from issue to pull request, through Worktrees, agent review and Delivery. Documentation covers installation, commands and configuration.
Built on Matt Pocock's skills: aihero.dev/skills · mattpocock/skills . Read every prompt and skill thirdshift sends .
diff --git a/site/prompts/index.html b/site/prompts/index.html
index 22ec9192..563f8d28 100644
--- a/site/prompts/index.html
+++ b/site/prompts/index.html
@@ -4,7 +4,20 @@
Prompts and skills · thirdshift
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/site/robots.txt b/site/robots.txt
index c2a49f4f..7b5a0094 100644
--- a/site/robots.txt
+++ b/site/robots.txt
@@ -1,2 +1,4 @@
User-agent: *
Allow: /
+
+Sitemap: https://thirdshift.app/sitemap.xml
diff --git a/site/sitemap.xml b/site/sitemap.xml
new file mode 100644
index 00000000..d8d1f997
--- /dev/null
+++ b/site/sitemap.xml
@@ -0,0 +1,7 @@
+
+
+ https://thirdshift.app/
+ https://thirdshift.app/docs/
+ https://thirdshift.app/how-it-works/
+ https://thirdshift.app/prompts/
+
diff --git a/site/tests/README.md b/site/tests/README.md
index 041422dc..ee153b31 100644
--- a/site/tests/README.md
+++ b/site/tests/README.md
@@ -12,6 +12,11 @@ npm test
The tests start a local server on an unused port and close it and the browser
before exiting. CI runs them in the `site-test` job.
+The page tests also consume the site without JavaScript to check canonical URLs,
+distinct search metadata, homepage site-name structured data and sitemap discovery
+through `robots.txt`. When adding a public page, give it a canonical URL and unique
+title and description, then include it in `site/sitemap.xml` and the page tests.
+
The user manual's source is `docs/guide.md` at the repository root. After editing
it or `site/generate-docs.mjs`, run `cd site && npm run docs` and commit the generated
`site/docs/index.html` in the same pull request. `npm test` first runs
diff --git a/site/tests/pages.test.cjs b/site/tests/pages.test.cjs
index 2934995a..764e86da 100644
--- a/site/tests/pages.test.cjs
+++ b/site/tests/pages.test.cjs
@@ -5,6 +5,8 @@ const { readFile } = require("node:fs/promises");
const { join } = require("node:path");
const { chromium } = require("playwright");
+const publicPaths = ["/", "/docs/", "/how-it-works/", "/prompts/"];
+
let server, browser, origin;
before(async () => {
server = createServer(async (request, response) => {
@@ -35,6 +37,79 @@ async function pageForTest(t, options) {
return page;
}
+test("every public page identifies its canonical URL, purpose and share metadata without JavaScript", async (t) => {
+ const page = await pageForTest(t, { javaScriptEnabled: false });
+ const titles = new Set();
+ const descriptions = new Set();
+ for (const path of publicPaths) {
+ assert.equal((await page.goto(`${origin}${path}`)).status(), 200);
+ assert.equal(await page.locator('link[rel="canonical"]').count(), 1, path);
+ assert.equal(await page.locator('link[rel="canonical"]').getAttribute("href"), `https://thirdshift.app${path}`);
+ const title = await page.title();
+ const description = await page.locator('meta[name="description"]').getAttribute("content");
+ assert.match(title, /thirdshift/);
+ assert.ok(description.length > 50, path);
+ titles.add(title);
+ descriptions.add(description);
+ for (const [selector, expected] of [
+ ['meta[property="og:type"]', "website"],
+ ['meta[property="og:site_name"]', "thirdshift"],
+ ['meta[property="og:url"]', `https://thirdshift.app${path}`],
+ ['meta[property="og:title"]', title],
+ ['meta[property="og:description"]', description],
+ ['meta[property="og:image"]', "https://thirdshift.app/share-card.png"],
+ ['meta[name="twitter:card"]', "summary_large_image"],
+ ['meta[name="twitter:title"]', title],
+ ['meta[name="twitter:description"]', description],
+ ['meta[name="twitter:image"]', "https://thirdshift.app/share-card.png"],
+ ]) {
+ const tag = page.locator(`head ${selector}`);
+ assert.equal(await tag.count(), 1, `${path}: ${selector}`);
+ assert.equal(await tag.getAttribute("content"), expected, `${path}: ${selector}`);
+ }
+ assert.equal(await page.locator('meta[name="robots"][content*="noindex"]').count(), 0);
+ assert.equal(await page.locator("h1").count(), 1, path);
+ assert.ok((await page.locator("body").textContent()).includes("GitHub"), path);
+ }
+ assert.equal(titles.size, 4, "page titles distinguish the four search results");
+ assert.equal(descriptions.size, 4, "each page has its own search description");
+ await page.goto(`${origin}/`);
+ assert.match(await page.title(), /GitHub issues.*pull requests/);
+});
+
+test("robots advertises a valid sitemap of reachable canonical pages", async (t) => {
+ const page = await pageForTest(t, { javaScriptEnabled: false });
+ const robots = await page.request.get(`${origin}/robots.txt`);
+ assert.equal(robots.status(), 200);
+ assert.match(await robots.text(), /^Allow: \/$/m);
+ assert.match(await robots.text(), /^Sitemap: https:\/\/thirdshift\.app\/sitemap\.xml$/m);
+ const sitemap = await page.request.get(`${origin}/sitemap.xml`);
+ assert.equal(sitemap.status(), 200);
+ const urls = await page.evaluate(xml => {
+ const document = new DOMParser().parseFromString(xml, "application/xml");
+ if (document.querySelector("parsererror")) throw new Error("Invalid sitemap XML");
+ if (document.documentElement.namespaceURI !== "http://www.sitemaps.org/schemas/sitemap/0.9") {
+ throw new Error("Missing sitemap namespace");
+ }
+ return [...document.querySelectorAll("urlset > url > loc")].map(node => node.textContent);
+ }, await sitemap.text());
+ // The metadata test checks reachability and canonical URLs for this inventory.
+ assert.deepEqual(urls.sort(), publicPaths.map(path => `https://thirdshift.app${path}`));
+});
+
+test("the homepage declares the site name as static structured data", async (t) => {
+ const page = await pageForTest(t, { javaScriptEnabled: false });
+ await page.goto(`${origin}/`);
+ assert.equal(await page.locator('script[type="application/ld+json"]').count(), 1);
+ const site = JSON.parse(await page.locator('script[type="application/ld+json"]').textContent());
+ assert.equal(site["@context"], "https://schema.org");
+ assert.equal(site["@type"], "WebSite");
+ assert.equal(site.name, "thirdshift");
+ assert.equal(site.url, "https://thirdshift.app/");
+ assert.equal(site.url, await page.locator('link[rel="canonical"]').getAttribute("href"));
+ assert.equal(site.name, await page.locator('meta[property="og:site_name"]').getAttribute("content"));
+});
+
test("the homepage links to the shared Markdown manual without JavaScript", async (t) => {
const page = await pageForTest(t, { javaScriptEnabled: false, viewport: { width: 320, height: 900 } });
await page.goto(`${origin}/`);