| 1 | /** |
| 2 | * The public site: the home page and the product, comparison and company pages in site/. Every page |
| 3 | * renders for visitors with its search and social tags and structured data, is served as Markdown, |
| 4 | * links only to pages that exist, and is listed in the sitemap, llms.txt and llms-full.txt. |
| 5 | * Needs src/docs/content.generated.ts (`pnpm build` or `pnpm build:docs`). |
| 6 | */ |
| 7 | import { describe, expect, it } from "vitest"; |
| 8 | import { app } from "../../src/app.js"; |
| 9 | import { web } from "../../src/web/routes.js"; |
| 10 | import { slugify } from "../../src/web/markdown.js"; |
| 11 | import type { Env } from "../../src/env.js"; |
| 12 | import { getPage, llmsFullTxt, llmsTxt } from "../../src/docs/pages.js"; |
| 13 | import { CLOUDFLARE_CLAIM, faqs, homeMarkdown, sitePage, sitePages } from "../../src/web/site.js"; |
| 14 | import { RESERVED_LOGINS } from "../../src/lib/util.js"; |
| 15 | |
| 16 | app.route("/", web); |
| 17 | |
| 18 | const ORIGIN = "https://diffinery.example"; |
| 19 | const stmt = { bind: () => stmt, all: async () => ({ results: [] }), first: async () => null, run: async () => ({ success: true, meta: {} }) }; |
| 20 | const env = { PUBLIC_URL: ORIGIN, DB: { prepare: () => stmt } } as unknown as Env; |
| 21 | const ctx = { waitUntil() {}, passThroughOnException() {}, props: {} } as unknown as ExecutionContext; |
| 22 | const get = (path: string, headers: Record<string, string> = {}) => app.fetch(new Request(`${ORIGIN}${path}`, { headers }), env, ctx); |
| 23 | |
| 24 | const pages = sitePages(); |
| 25 | |
| 26 | /** The JSON-LD graph of an HTML page. */ |
| 27 | function graph(html: string): Array<Record<string, unknown>> { |
| 28 | const m = /<script type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/.exec(html); |
| 29 | expect(m, "structured data").toBeTruthy(); |
| 30 | const ld = JSON.parse(m![1]!) as { "@context": string; "@graph": Array<Record<string, unknown>> }; |
| 31 | expect(ld["@context"]).toBe("https://schema.org"); |
| 32 | return ld["@graph"]; |
| 33 | } |
| 34 | |
| 35 | const types = (g: Array<Record<string, unknown>>) => g.map((x) => x["@type"]); |
| 36 | |
| 37 | /** Markdown without code, so examples aren't checked as links. */ |
| 38 | const prose = (md: string) => md.replace(/^```[\s\S]*?^```/gm, "").replace(/`[^`\n]*`/g, ""); |
| 39 | |
| 40 | /** Paths outside /docs and the site pages that the site may link to. */ |
| 41 | const OTHER_PATHS = [/^\/$/, /^\/llms(-full)?\.txt$/, /^\/early-access$/, /^\/status$/, /^\/security\/report$/, /^\/\.well-known\/security\.txt$/, /^\/docs\/api\/openapi\.json$/]; |
| 42 | |
| 43 | function linkProblems(md: string): string[] { |
| 44 | const problems: string[] = []; |
| 45 | for (const m of prose(md).matchAll(/\]\(([^)\s]+)\)/g)) { |
| 46 | const href = m[1]!; |
| 47 | if (/^https?:\/\//.test(href)) { |
| 48 | if (!href.startsWith("https://")) problems.push(`insecure link: ${href}`); |
| 49 | continue; |
| 50 | } |
| 51 | if (!href.startsWith("/")) { |
| 52 | problems.push(`relative link: ${href}`); |
| 53 | continue; |
| 54 | } |
| 55 | const [path, frag] = href.split("#") as [string, string | undefined]; |
| 56 | if (path === "/docs" || path.startsWith("/docs/")) { |
| 57 | if (OTHER_PATHS.some((re) => re.test(path))) continue; |
| 58 | const doc = getPage(path === "/docs" ? "index" : path.slice(6), ORIGIN); |
| 59 | if (!doc) problems.push(`no docs page ${path}`); |
| 60 | else if (frag && !doc.markdown.split("\n").some((l) => /^#{2,6} /.test(l) && slugify(l.replace(/^#+ /, "")) === frag)) problems.push(`no heading ${href}`); |
| 61 | continue; |
| 62 | } |
| 63 | const site = sitePage(path); |
| 64 | if (site) { |
| 65 | if (frag && !site.markdown.split("\n").some((l) => /^#{2,6} /.test(l) && slugify(l.replace(/^#+ /, "")) === frag)) problems.push(`no heading ${href}`); |
| 66 | continue; |
| 67 | } |
| 68 | if (!OTHER_PATHS.some((re) => re.test(path))) problems.push(`link to ${path} isn't a page`); |
| 69 | } |
| 70 | return problems; |
| 71 | } |
| 72 | |
| 73 | describe("site pages", () => { |
| 74 | it("has the product, comparison and company pages", () => { |
| 75 | expect(pages.map((p) => p.path)).toEqual(expect.arrayContaining(["/features", "/agents", "/performance", "/security", "/built-on-cloudflare", "/about", "/faq", "/blog", "/compare", "/compare/github", "/compare/gitlab", "/compare/blacksmith", "/compare/depot"])); |
| 76 | for (const p of pages) { |
| 77 | expect(p.description.length, p.path).toBeLessThanOrEqual(160); |
| 78 | expect(p.updated, p.path).toMatch(/^\d{4}-\d{2}-\d{2}$/); |
| 79 | // The blog's index only lists its posts. |
| 80 | if (p.path !== "/blog") expect(p.markdown.length, `${p.path} is too short`).toBeGreaterThan(1500); |
| 81 | } |
| 82 | }); |
| 83 | |
| 84 | it("keeps the pages' top-level paths away from account names", () => { |
| 85 | for (const p of pages) expect(RESERVED_LOGINS.has(p.path.split("/")[1]!), p.path).toBe(true); |
| 86 | }); |
| 87 | |
| 88 | it.each(pages.map((p) => [p.path, p] as const))("%s: links resolve", (_path, p) => { |
| 89 | expect(linkProblems(p.markdown)).toEqual([]); |
| 90 | }); |
| 91 | |
| 92 | it("home page links resolve", () => { |
| 93 | expect(linkProblems(homeMarkdown(""))).toEqual([]); |
| 94 | }); |
| 95 | |
| 96 | it("every comparison names its sources and the date they were checked", () => { |
| 97 | for (const p of pages.filter((x) => x.path.startsWith("/compare/"))) { |
| 98 | expect(p.markdown, p.path).toMatch(/\[source\]\(https:\/\//); |
| 99 | // The date the other product's facts were checked, written out ("checked on October 16, 2026"). |
| 100 | expect(p.markdown, p.path).toMatch(/(January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}/); |
| 101 | expect(faqs(p.markdown).length, `${p.path} questions`).toBeGreaterThan(0); |
| 102 | } |
| 103 | }); |
| 104 | |
| 105 | it.each(pages.map((p) => [p.path, p] as const))("%s: renders for visitors with search tags and structured data", async (path, p) => { |
| 106 | const res = await get(path); |
| 107 | expect(res.status).toBe(200); |
| 108 | expect(res.headers.get("link")).toContain(`<${ORIGIN}${path}.md>; rel="alternate"; type="text/markdown"`); |
| 109 | const html = await res.text(); |
| 110 | expect(html).toContain(`<link rel="canonical" href="${ORIGIN}${path}"/>`); |
| 111 | expect(html).not.toContain('name="robots"'); |
| 112 | expect(html.match(/<h1[ >]/g)?.length).toBe(1); |
| 113 | expect(html).toContain(`<meta name="description" content="`); |
| 114 | expect(html).toContain('href="/features"'); |
| 115 | const g = graph(html); |
| 116 | expect(types(g)).toEqual(expect.arrayContaining(["Organization", "WebSite", "SoftwareApplication", "BreadcrumbList"])); |
| 117 | const page = g.find((x) => x["@id"] === `${ORIGIN}${path}#page`)!; |
| 118 | expect(page.dateModified).toBe(p.updated); |
| 119 | const qs = faqs(p.markdown); |
| 120 | const faq = g.find((x) => x["@type"] === "FAQPage") as { mainEntity: Array<{ name: string }> } | undefined; |
| 121 | expect(faq?.mainEntity.length ?? 0).toBe(qs.length); |
| 122 | }); |
| 123 | |
| 124 | it.each(pages.map((p) => [p.path, p] as const))("%s: is served as Markdown", async (path, p) => { |
| 125 | for (const res of [await get(`${path}.md`), await get(path, { accept: "text/markdown" })]) { |
| 126 | expect(res.status).toBe(200); |
| 127 | expect(res.headers.get("content-type")).toBe("text/markdown; charset=utf-8"); |
| 128 | const md = await res.text(); |
| 129 | expect(md.startsWith(`# ${p.title}\n\n> ${p.description}\n\nSource: ${ORIGIN}${path} (last updated ${p.updated})`)).toBe(true); |
| 130 | expect(md).not.toMatch(/\]\(\/(?!\/)/); |
| 131 | } |
| 132 | }); |
| 133 | |
| 134 | it("marks blog posts up as BlogPostings with the date they went out", async () => { |
| 135 | const post = pages.find((p) => p.path.startsWith("/blog/"))!; |
| 136 | const g = graph(await (await get(post.path)).text()); |
| 137 | const page = g.find((x) => x["@id"] === `${ORIGIN}${post.path}#page`)!; |
| 138 | expect(page["@type"]).toBe("BlogPosting"); |
| 139 | expect(page.datePublished).toBe(post.published ?? post.updated); |
| 140 | const crumbs = g.find((x) => x["@type"] === "BreadcrumbList") as { itemListElement: Array<{ name: string }> }; |
| 141 | expect(crumbs.itemListElement.map((c) => c.name)).toEqual(["Home", "Blog", post.title]); |
| 142 | }); |
| 143 | |
| 144 | it("sends a trailing slash to the page", async () => { |
| 145 | const res = await get("/compare/github/"); |
| 146 | expect(res.status).toBe(301); |
| 147 | expect(res.headers.get("location")).toBe("/compare/github"); |
| 148 | }); |
| 149 | }); |
| 150 | |
| 151 | describe("home page", () => { |
| 152 | it("renders for visitors with the product, its questions and a Markdown version", async () => { |
| 153 | const res = await get("/"); |
| 154 | expect(res.status).toBe(200); |
| 155 | expect(res.headers.get("link")).toContain(`<${ORIGIN}/index.md>; rel="alternate"; type="text/markdown"`); |
| 156 | const html = await res.text(); |
| 157 | expect(html).toContain("<title>Diffinery: open-source GitHub alternative with fast CI, built for AI agents</title>"); |
| 158 | expect(html).toContain("Code hosting and CI for the agentic era</h1>"); |
| 159 | const g = graph(html); |
| 160 | expect(types(g)).toEqual(expect.arrayContaining(["Organization", "WebSite", "SoftwareApplication", "SoftwareSourceCode", "WebPage", "FAQPage"])); |
| 161 | // Open source: the license and the repositories, no pricing, and early access to try it on diffinery.com. |
| 162 | const source = g.find((n) => n["@type"] === "SoftwareSourceCode")!; |
| 163 | expect(source).toMatchObject({ codeRepository: "https://diffinery.com/codician/diffinery", license: "https://www.apache.org/licenses/LICENSE-2.0", sameAs: ["https://github.com/codician-team/diffinery"] }); |
| 164 | expect(g.find((n) => n["@type"] === "SoftwareApplication")).toMatchObject({ license: "https://www.apache.org/licenses/LICENSE-2.0", isAccessibleForFree: true, installUrl: `${ORIGIN}/docs/self-hosting/deploy`, downloadUrl: "https://diffinery.com/codician/diffinery/releases" }); |
| 165 | expect(html).toContain('href="https://diffinery.com/early-access"'); |
| 166 | const faq = g.find((n) => n["@type"] === "FAQPage") as { mainEntity: Array<{ name: string; acceptedAnswer: { text: string } }> }; |
| 167 | expect(faq.mainEntity.find((q) => q.name === "Can I try Diffinery without deploying it?")?.acceptedAnswer.text).toContain("Request early access to diffinery.com"); |
| 168 | expect(html).not.toMatch(/not been announced|hasn't been announced|invite-only while/i); |
| 169 | for (const href of ["/compare/github", "/compare/blacksmith", "/performance", "/agents", "/about", "/llms.txt", "/open-source", "/docs/self-hosting", "https://diffinery.com/codician/diffinery", "https://github.com/codician-team/diffinery"]) expect(html, href).toContain(`href="${href}"`); |
| 170 | }); |
| 171 | |
| 172 | it("doesn't load the Kumo bundle, which only pages with islands use", async () => { |
| 173 | expect(await (await get("/")).text()).not.toContain("/-/kumo/"); |
| 174 | }); |
| 175 | |
| 176 | it("is served as Markdown", async () => { |
| 177 | for (const res of [await get("/index.md"), await get("/", { accept: "text/markdown" })]) { |
| 178 | expect(res.status).toBe(200); |
| 179 | expect(res.headers.get("content-type")).toBe("text/markdown; charset=utf-8"); |
| 180 | const md = await res.text(); |
| 181 | expect(md).toContain("## Frequently asked questions"); |
| 182 | expect(md).toContain(`(${ORIGIN}/compare/github)`); |
| 183 | } |
| 184 | }); |
| 185 | }); |
| 186 | |
| 187 | describe("sign-in page", () => { |
| 188 | // The Deploy workflow checks that /login names both bundle files and that they answer. |
| 189 | it("prefetches the Kumo bundle for the pages after it, without loading it", async () => { |
| 190 | const html = await (await get("/login")).text(); |
| 191 | expect(html).toContain("Sign in with a passkey"); |
| 192 | expect(html).toMatch(/<link rel="prefetch" href="\/-\/kumo\/[a-z0-9]+\.css" as="style"\/?>/); |
| 193 | expect(html).toMatch(/<link rel="prefetch" href="\/-\/kumo\/[a-z0-9]+\.js" as="script"\/?>/); |
| 194 | expect(html).not.toMatch(/<(?:script|link rel="stylesheet")[^>]*\/-\/kumo\//); |
| 195 | }); |
| 196 | }); |
| 197 | |
| 198 | describe("crawlers and AI assistants", () => { |
| 199 | it("lists every site page in llms.txt, before the docs", () => { |
| 200 | const txt = llmsTxt(ORIGIN); |
| 201 | expect(txt).toContain(`(${ORIGIN}/index.md)`); |
| 202 | for (const p of pages) expect(txt).toContain(`(${ORIGIN}${p.path}.md)`); |
| 203 | expect(txt.indexOf(`${ORIGIN}/compare/github.md`)).toBeLessThan(txt.indexOf(`${ORIGIN}/docs/quickstart.md`)); |
| 204 | expect(txt).toContain("request early access at https://diffinery.com/early-access"); |
| 205 | }); |
| 206 | |
| 207 | it("states the Cloudflare claim the same way everywhere, backed by the sourced list of other projects", async () => { |
| 208 | expect(llmsTxt(ORIGIN)).toContain(CLOUDFLARE_CLAIM); |
| 209 | const g = graph(await (await get("/")).text()); |
| 210 | expect((g.find((x) => x["@type"] === "SoftwareApplication")!.featureList as string[]).join(" ")).toContain(CLOUDFLARE_CLAIM); |
| 211 | const page = sitePage("/built-on-cloudflare")!; |
| 212 | expect(page.markdown).toContain("## Other Git and CI projects on Cloudflare"); |
| 213 | expect(page.markdown.split("## Other Git and CI projects on Cloudflare")[1]!.split("\n## ")[0]!.match(/\[source\]\(https:\/\//g)!.length).toBeGreaterThan(5); |
| 214 | }); |
| 215 | |
| 216 | it("holds every site page in llms-full.txt", () => { |
| 217 | const txt = llmsFullTxt(ORIGIN); |
| 218 | expect(txt).toContain(`Source: ${ORIGIN}/ (last updated`); |
| 219 | for (const p of pages) expect(txt).toContain(`# ${p.title}\n\n> ${p.description}\n\nSource: ${ORIGIN}${p.path}`); |
| 220 | }); |
| 221 | |
| 222 | it("lists the site in the sitemap with the date each page was checked", async () => { |
| 223 | const xml = await (await get("/sitemap.xml")).text(); |
| 224 | expect(xml.match(new RegExp(`<loc>${ORIGIN}/</loc>`, "g"))?.length).toBe(1); |
| 225 | for (const p of pages) expect(xml).toContain(`<url><loc>${ORIGIN}${p.path}</loc><lastmod>${p.updated}</lastmod></url>`); |
| 226 | }); |
| 227 | |
| 228 | it("welcomes search engines and AI assistants in robots.txt", async () => { |
| 229 | const txt = await (await get("/robots.txt")).text(); |
| 230 | expect(txt).toContain("User-agent: *\nContent-Signal: search=yes, ai-input=yes, ai-train=yes\nAllow: /"); |
| 231 | expect(txt).toContain(`Sitemap: ${ORIGIN}/sitemap.xml`); |
| 232 | for (const p of pages) expect(txt).not.toMatch(new RegExp(`^Disallow: ${p.path}$`, "m")); |
| 233 | }); |
| 234 | }); |