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 @@ -Documentation · thirdshift +thirdshift documentation · Install, configure and run coding agents + + + + + + + + + + + + diff --git a/site/generate-docs.mjs b/site/generate-docs.mjs index 7c673a9d..27b672df 100644 --- a/site/generate-docs.mjs +++ b/site/generate-docs.mjs @@ -31,9 +31,21 @@ const page = ` -Documentation · thirdshift +thirdshift documentation · Install, configure and run coding agents + + + + + + + + + + + + diff --git a/site/how-it-works/index.html b/site/how-it-works/index.html index b2876edf..d1371ece 100644 --- a/site/how-it-works/index.html +++ b/site/how-it-works/index.html @@ -6,6 +6,18 @@ How thirdshift works · thirdshift + + + + + + + + + + + + diff --git a/site/index.html b/site/index.html index 32dbfb8c..dc62e860 100644 --- a/site/index.html +++ b/site/index.html @@ -3,22 +3,31 @@ -thirdshift: your issues go to press overnight - +thirdshift · GitHub issues to pull requests with coding agents + + + - - + + - - + + @@ -53,7 +62,7 @@

Your issues go to press overnight.

-

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}/`);