Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

671 lines · 34.8 KBRaw History
1/** @jsxImportSource hono/jsx */
2/**
3 * The public site around the product: the home page for visitors (Landing), and the product,
4 * comparison and company pages written as Markdown in site/ (built into content.generated.ts by
5 * scripts/build-docs.mjs), served at /features, /compare/github and so on.
6 *
7 * Written for people, search engines and AI assistants alike. Every page is also served as Markdown
8 * (/<page>.md, or the page itself with `Accept: text/markdown`; the home page at /index.md), is listed in
9 * /llms.txt, /llms-full.txt and the sitemap, and carries schema.org structured data: the organization,
10 * the product, breadcrumbs, the date its facts were last checked, and its questions and answers.
11 */
12import { raw } from "hono/html";
13import type { FC } from "hono/jsx";
14import type { AppContext } from "../auth/principal.js";
15import { SITE_NAV, SITE_PAGES } from "../docs/content.generated.js";
16import { imageProxy } from "../lib/camo.js";
17import { ICONS } from "./assets.js";
18import { fenceHighlighter } from "./highlight.js";
19import { escapeHtml, fenceLangs, renderMarkdown } from "./markdown.js";
20import { SITE } from "./seo.js";
21import { render, type WebApp } from "./ui.js";
22
23export interface SitePageInfo {
24 /** URL path, e.g. "/compare/github". */
25 path: string;
26 title: string;
27 description: string;
28 markdown: string;
29 /** The date its facts were last checked, YYYY-MM-DD. */
30 updated: string;
31 /** Blog posts: the date the post first went out, YYYY-MM-DD (`updated` when it's left out). */
32 published?: string;
33 seoTitle: string | null;
34 /** The site/nav.json section it belongs to ("Product", "Compare", "Company"). */
35 section: string;
36}
37
38const origin = (c: AppContext) => c.env.PUBLIC_URL.replace(/\/$/, "");
39
40/** Every site page, in the order of site/nav.json. */
41export function sitePages(): SitePageInfo[] {
42 return SITE_NAV.flatMap((s) => s.pages.map((path) => ({ path, ...SITE_PAGES[path]!, section: s.title })));
43}
44
45export function sitePage(path: string): SitePageInfo | null {
46 return sitePages().find((p) => p.path === path) ?? null;
47}
48
49/** Root-relative links (`](/docs/...)`) made absolute, for Markdown read outside the site. */
50const absolute = (md: string, o: string) => md.replace(/\]\(\/(?!\/)/g, `](${o}/`);
51
52const longDate = (d: string) => new Date(`${d}T00:00:00Z`).toLocaleDateString("en-US", { dateStyle: "long", timeZone: "UTC" });
53
54/** A site page as standalone Markdown: what /<page>.md serves. */
55export function sitePageMarkdown(p: SitePageInfo, o: string): string {
56 return `# ${p.title}\n\n> ${p.description}\n\nSource: ${o}${p.path} (last updated ${p.updated})\n\n${absolute(p.markdown, o).trim()}\n`;
57}
58
59// ---------------------------------------------------------------- questions and answers
60
61/** The "## Frequently asked questions" section of a page: each `###` heading and the text under it. */
62export function faqs(markdown: string): Array<{ q: string; a: string }> {
63 const m = /^## Frequently asked questions\s*$([\s\S]*?)(?=^## |(?![\s\S]))/m.exec(markdown);
64 if (!m) return [];
65 return [...m[1]!.matchAll(/^### +(.+?)\s*$([\s\S]*?)(?=^### |(?![\s\S]))/gm)].map((x) => ({ q: x[1]!.trim(), a: x[2]!.trim() })).filter((x) => x.a);
66}
67
68/** Markdown reduced to plain text, for structured data. */
69export const plainText = (md: string) =>
70 md
71 .replace(/`([^`]*)`/g, "$1")
72 .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
73 .replace(/\*\*([^*]+)\*\*/g, "$1")
74 .replace(/\s+/g, " ")
75 .trim();
76
77// ---------------------------------------------------------------- the home page
78
79/** When the home page's facts were last checked. */
80export const HOME_UPDATED = "2026-10-25";
81
82const HOME = {
83 // No SITE here: seo.tsx imports this module, so SITE isn't defined yet while it loads.
84 title: "Diffinery: open-source GitHub alternative with fast CI, built for AI agents",
85 description:
86 "Diffinery is open-source code hosting and CI for the agentic era, running on your own Cloudflare account: Git repositories, pull requests, GitHub Actions–compatible CI on 4-vCPU runners, and AI agents with their own accounts and guardrails.",
87 h1: "Code hosting and CI for the agentic era",
88 lead: "Diffinery is an open-source GitHub alternative for teams of people and AI agents. Git repositories, pull requests, GitHub Actions–compatible CI on fast runners, and agents with their own accounts and guardrails, running entirely on your own Cloudflare account.",
89 stats: [
90 { value: "4 vCPU", label: "per CI job by default. GitHub's standard runner for private repositories has 2." },
91 { value: "3–5 s", label: "from a job being queued to its first step." },
92 { value: "Apache-2.0", label: "Open source, with nothing to buy. You run it on your own Cloudflare account." },
93 { value: "1 person", label: "approves every agent pull request, enforced by the server." },
94 ],
95 features: [
96 { icon: "repo", title: "Git hosting", text: "Clone, fetch and push over HTTPS with Git LFS. Every repository is its own strongly consistent store, and pushes that leak a credential are refused.", href: "/features#git-hosting" },
97 { icon: "pr", title: "Pull requests with intent", text: "Reviews, required approvals and checks, auto-merge and a merge queue. Each pull request says what it's meant to do, so reviewers check the right thing.", href: "/features#pull-requests-reviews-and-issues" },
98 { icon: "play", title: "GitHub Actions–compatible CI", text: "The same triggers, expressions, matrices, reusable workflows, secrets, OIDC and Docker. Actions from GitHub run as they do there.", href: "/features#actions-ci-that-runs-your-github-workflows" },
99 { icon: "zap", title: "Fast, right-sized runners", text: "4 vCPUs and 12 GiB per job by default, started in seconds. Jobs that only wait move to smaller runners on their own, never at the cost of speed.", href: "/performance" },
100 { icon: "bot", title: "AI agents with guardrails", text: "Agents get their own accounts and tokens, an MCP server, sandboxes, tasks and reviews. They can't push to protected branches or reach your secrets.", href: "/agents" },
101 { icon: "shield", title: "Secure by default", text: "Passkeys instead of passwords, invite-only sign-up, encrypted secrets, scoped job tokens and an append-only audit log.", href: "/security" },
102 ],
103 workflow: `name: CI
104on: [push, pull_request]
105jobs:
106 test:
107 runs-on: ubuntu-latest # 4 vCPU, 12 GiB on Diffinery
108 steps:
109 - uses: actions/checkout@v4
110 - uses: actions/setup-node@v4
111 with: { node-version: 22, cache: pnpm }
112 - run: pnpm install --frozen-lockfile
113 - run: pnpm test`,
114 compare: {
115 cols: ["Diffinery", "GitHub", "GitHub with a runner service"],
116 rows: [
117 ["Git hosting, pull requests, issues", "Yes", "Yes", "Yes, on GitHub"],
118 ["Runs GitHub Actions workflows", "Yes, as they are", "Yes", "Yes, with a new runs-on label"],
119 ["Default Linux runner, private repositories", "4 vCPU, 12 GiB", "2 vCPU, 8 GB", "Your choice of size, billed by the vendor"],
120 ["Guardrails for AI agents", "Built in for every agent account", "Rulesets and permissions you configure", "Rulesets and permissions you configure"],
121 ["Built-in MCP server and agent sandboxes", "Yes", "MCP server, Codespaces", "GitHub's"],
122 ["Hosted macOS and Windows runners", "No (self-hosted)", "Yes", "Depends on the vendor"],
123 ["Open source", "Yes, Apache-2.0", "No", "No"],
124 ["Who runs it", "You, on your own Cloudflare account", "GitHub", "GitHub and the runner vendor"],
125 ],
126 },
127 faq: [
128 {
129 q: "What is Diffinery?",
130 a: "Diffinery is a git forge and CI platform for teams of people and AI agents. It hosts Git repositories, pull requests, issues and releases, runs GitHub Actions workflows on its own hosted runners, and gives AI agents their own accounts, tokens and server-enforced guardrails. It runs entirely on Cloudflare, and it's open source under the Apache License 2.0: you deploy it to your own Cloudflare account.",
131 },
132 {
133 q: "Is Diffinery an alternative to GitHub?",
134 a: "Yes, for teams whose work fits it: Diffinery replaces GitHub's code hosting, pull requests and GitHub Actions in one platform. It doesn't have GitHub's hosted macOS and Windows runners, forks, app marketplace, Codespaces, Pages or Discussions. See [Diffinery vs GitHub](/compare/github).",
135 },
136 {
137 q: "Do my GitHub Actions workflows run on Diffinery?",
138 a: "Most run without changes. Diffinery reads `.github/workflows/*.yml` with the same syntax, triggers, expressions and actions. Hosted runners are Linux x64; macOS, Windows and Arm jobs need a self-hosted runner. Every difference is listed in [Compatibility with GitHub Actions](/docs/actions/compatibility).",
139 },
140 {
141 q: "Is Diffinery built on Cloudflare?",
142 a: "Yes, entirely: Git hosting, pull requests with reviews in a web UI and GitHub Actions–compatible CI, marketplace actions included, all run on Cloudflare. One Cloudflare Worker serves the site, the API, Git and the MCP server; each repository is its own Durable Object, with large objects in R2 and accounts in D1; Workflows and Queues drive CI runs, webhooks and email; and every CI job runs in a Cloudflare Container that exists only while the job runs. There are no servers to manage. See [Built on Cloudflare](/built-on-cloudflare).",
143 },
144 {
145 q: "How is Diffinery different from Blacksmith or Depot?",
146 a: "Blacksmith sells faster runners for GitHub Actions, and Depot sells runners, its own CI engine and container builds. With either, your code and pull requests usually stay on GitHub and jobs run on their machines. Diffinery replaces GitHub and GitHub Actions together, with fast runners built in, as one open-source platform you run on your own Cloudflare account. See [Diffinery vs Blacksmith](/compare/blacksmith) and [Diffinery vs Depot](/compare/depot).",
147 },
148 {
149 q: "How does Diffinery work with Claude Code, Codex and other AI agents?",
150 a: "Agents connect over MCP at `/mcp` on your instance with a browser sign-in, or use Git, the GitHub CLI and the REST API with an agent token. Each agent is an account of its own: it can't push to protected branches, its pull requests need a person's approval, and runs of its code get no secrets. See [Diffinery for AI agents](/agents).",
151 },
152 {
153 q: "How do I move from GitHub?",
154 a: "Diffinery's importer copies a repository or a whole organization from GitHub on the server, with history, issues, pull requests, reviews, releases and branch protection, keeping their numbers. Then you enter your secrets' values and push. See [Migrate from GitHub](/docs/migrate-from-github).",
155 },
156 {
157 q: "Is Diffinery open source?",
158 a: "Yes, under the Apache License 2.0, with no paid edition and nothing sold. The source code is at [diffinery.com/codician/diffinery](https://diffinery.com/codician/diffinery) and mirrored to [github.com/codician-team/diffinery](https://github.com/codician-team/diffinery). See [Open source](/open-source).",
159 },
160 {
161 q: "How do I start using Diffinery, and what does it cost?",
162 a: "Deploy your own instance to your Cloudflare account and domain with one command, `scripts/setup.sh`, then sign in as its first administrator and invite your team. See [Deploy your own Diffinery](/docs/self-hosting/deploy). Diffinery is free; you pay Cloudflare for what your instance uses, on the Workers Paid plan (a minimum of $5 USD per month per account, plus usage; [Cloudflare's pricing](https://developers.cloudflare.com/workers/platform/pricing/), checked October 25, 2026). There's no hosted service to sign up for: diffinery.com is the project's own instance.",
163 },
164 ],
165};
166
167/** The home page as Markdown, for agents: /index.md, and / with `Accept: text/markdown`. */
168export function homeMarkdown(o: string): string {
169 const lines = [
170 `# ${SITE.name}`,
171 "",
172 `> ${HOME.description}`,
173 "",
174 `Source: ${o}/ (last updated ${HOME_UPDATED})`,
175 "",
176 `## ${HOME.h1}`,
177 "",
178 HOME.lead,
179 "",
180 ...HOME.stats.map((s) => `- **${s.value}**: ${s.label}`),
181 "",
182 "## What you get",
183 "",
184 ...HOME.features.map((f) => `- **[${f.title}](${f.href})**: ${f.text}`),
185 "",
186 "## Your workflows, as they are",
187 "",
188 "```yaml",
189 HOME.workflow,
190 "```",
191 "",
192 "## How Diffinery compares",
193 "",
194 `| | ${HOME.compare.cols.join(" | ")} |`,
195 `| --- | ${HOME.compare.cols.map(() => "---").join(" | ")} |`,
196 ...HOME.compare.rows.map((r) => `| ${r.join(" | ")} |`),
197 "",
198 "Runner services include Blacksmith and Depot. See [all comparisons](/compare).",
199 "",
200 "## Open source, on your own Cloudflare account",
201 "",
202 `Diffinery is open source under the Apache License 2.0: [source code](${SOURCE.repo}), [mirror on GitHub](${SOURCE.github}). Deploy it with [Deploy your own Diffinery](/docs/self-hosting/deploy); you pay Cloudflare only for what your instance uses. See [Open source](/open-source).`,
203 "",
204 "## Frequently asked questions",
205 "",
206 ...HOME.faq.flatMap((f) => [`### ${f.q}`, "", f.a, ""]),
207 ];
208 return absolute(lines.join("\n"), o);
209}
210
211/** Everything the home page says, for IndexNow (web/indexnow.ts): its Markdown and the landing page's own copy. */
212export async function homeContent(o: string): Promise<string> {
213 return `${homeMarkdown(o)}\n${await (<Landing />).toString()}`;
214}
215
216/**
217 * What sets Diffinery apart on Cloudflare, in the words every page uses (llms.txt, structured data). Checked against
218 * the other Git and CI projects on Cloudflare listed on /built-on-cloudflare; re-check them when this changes.
219 */
220export const CLOUDFLARE_CLAIM =
221 "an open-source platform where Git hosting, pull requests with reviews in a web UI and GitHub Actions–compatible CI, marketplace actions included, all run on Cloudflare (checked October 25, 2026).";
222
223/** Where Diffinery's source lives, whatever instance serves this page, and its license. */
224export const SOURCE = {
225 repo: "https://diffinery.com/codician/diffinery",
226 github: "https://github.com/codician-team/diffinery",
227 license: "https://www.apache.org/licenses/LICENSE-2.0",
228 spdx: "Apache-2.0",
229};
230
231/** schema.org ids for the site's entities, shared by every page's graph. */
232const ids = (o: string) => ({ org: `${o}/#organization`, site: `${o}/#website`, app: `${o}/#software`, source: `${o}/#source` });
233
234function entities(o: string): object[] {
235 const id = ids(o);
236 return [
237 { "@type": "Organization", "@id": id.org, name: SITE.name, url: `${o}/`, logo: { "@type": "ImageObject", url: `${o}/icon-512.png`, width: 512, height: 512 }, description: SITE.description },
238 { "@type": "WebSite", "@id": id.site, name: SITE.name, url: `${o}/`, inLanguage: "en", publisher: { "@id": id.org } },
239 {
240 "@type": "SoftwareApplication",
241 "@id": id.app,
242 name: SITE.name,
243 url: `${o}/`,
244 applicationCategory: "DeveloperApplication",
245 applicationSubCategory: "Git hosting and continuous integration",
246 operatingSystem: "Web",
247 description: HOME.lead,
248 featureList: [...HOME.features.map((f) => `${f.title}: ${f.text}`), `Runs entirely on Cloudflare: ${CLOUDFLARE_CLAIM}`, `Open source under the Apache License 2.0, deployed to your own Cloudflare account`],
249 license: SOURCE.license,
250 isAccessibleForFree: true,
251 offers: { "@type": "Offer", price: "0", priceCurrency: "USD", description: "Free, open-source software. Your instance's Cloudflare usage is billed by Cloudflare." },
252 softwareHelp: { "@type": "CreativeWork", url: `${o}/docs/self-hosting/deploy` },
253 installUrl: `${o}/docs/self-hosting/deploy`,
254 downloadUrl: `${SOURCE.repo}/releases`,
255 publisher: { "@id": id.org },
256 sameAs: [`${o}/docs`, SOURCE.repo, SOURCE.github],
257 },
258 {
259 "@type": "SoftwareSourceCode",
260 "@id": id.source,
261 name: SITE.name,
262 description: `The source code of ${SITE.name}, which you deploy to your own Cloudflare account.`,
263 codeRepository: SOURCE.repo,
264 sameAs: [SOURCE.github],
265 license: SOURCE.license,
266 programmingLanguage: "TypeScript",
267 runtimePlatform: "Cloudflare Workers",
268 isAccessibleForFree: true,
269 targetProduct: { "@id": id.app },
270 publisher: { "@id": id.org },
271 },
272 ];
273}
274
275function faqPage(url: string, list: Array<{ q: string; a: string }>): object[] {
276 if (!list.length) return [];
277 return [{ "@type": "FAQPage", "@id": `${url}#faq`, url, mainEntity: list.map((f) => ({ "@type": "Question", name: f.q, acceptedAnswer: { "@type": "Answer", text: plainText(f.a) } })) }];
278}
279
280/**
281 * Structured data for a docs page (docs/web.tsx): a TechArticle in the site's graph, with breadcrumbs (Home, Docs, the
282 * page), the date it was last reviewed when its frontmatter says, and its questions and answers.
283 */
284export function docsJsonLd(o: string, p: { url: string; title: string; description: string; section: string; markdown: string; updated?: string; home?: boolean }): object[] {
285 const id = ids(o);
286 const trail = [
287 { name: "Home", item: `${o}/` },
288 { name: "Docs", item: `${o}/docs` },
289 ...(p.home ? [] : [{ name: p.title, item: p.url }]),
290 ];
291 return [
292 ...entities(o),
293 {
294 "@type": "TechArticle",
295 "@id": `${p.url}#page`,
296 url: p.url,
297 name: p.title,
298 headline: p.title,
299 description: p.description,
300 articleSection: p.section,
301 inLanguage: "en",
302 ...(p.updated ? { dateModified: p.updated } : {}),
303 author: { "@id": id.org },
304 publisher: { "@id": id.org },
305 isPartOf: { "@id": id.site },
306 about: { "@id": id.app },
307 breadcrumb: { "@id": `${p.url}#breadcrumb` },
308 },
309 { "@type": "BreadcrumbList", "@id": `${p.url}#breadcrumb`, itemListElement: trail.map((t, i) => ({ "@type": "ListItem", position: i + 1, name: t.name, item: t.item })) },
310 ...faqPage(p.url, faqs(p.markdown)),
311 ];
312}
313
314const Cta: FC = () => (
315 <>
316 <a class="btn primary" href="/docs/self-hosting">
317 Deploy your own
318 </a>
319 <a class="btn" href={SOURCE.repo}>
320 View the source
321 </a>
322 <a class="btn" href="/docs">
323 Read the docs
324 </a>
325 </>
326);
327
328/** Inline Markdown (links and code) of a home page answer, as HTML. */
329const inline = (md: string) => renderMarkdown(md, { idPrefix: "" }).replace(/^<p>|<\/p>\s*$/g, "");
330
331/** The home page for visitors. */
332export const Landing: FC = () => (
333 <div class="mk">
334 <style>{raw(MARKETING_CSS)}</style>
335 <section class="hero">
336 <a class="ea-eyebrow" href="/open-source">
337 {raw(ICONS.code)} Open source under Apache-2.0
338 </a>
339 <h1>{HOME.h1}</h1>
340 <p>{HOME.lead}</p>
341 <p class="row" style="justify-content:center">
342 <Cta />
343 </p>
344 <p class="muted small">
345 Free, with nothing to buy: you run Diffinery on your own Cloudflare account. Source on <a href={SOURCE.repo}>Diffinery</a> and <a href={SOURCE.github}>GitHub</a>.
346 </p>
347 </section>
348 <ul class="mk-stats" aria-label="Diffinery in numbers">
349 {HOME.stats.map((s) => (
350 <li>
351 <b>{s.value}</b>
352 <span>{s.label}</span>
353 </li>
354 ))}
355 </ul>
356 <section class="mk-sec">
357 <h2>Everything your team and its agents need, in one place</h2>
358 <div class="features">
359 {HOME.features.map((f) => (
360 <a class="box mk-card" href={f.href}>
361 <h3>
362 {raw(ICONS[f.icon as keyof typeof ICONS])} {f.title}
363 </h3>
364 <p class="muted m0">{f.text}</p>
365 </a>
366 ))}
367 </div>
368 </section>
369 <section class="mk-sec mk-split">
370 <div>
371 <h2>Your workflows, as they are</h2>
372 <p>Diffinery reads <code>.github/workflows</code> with the same triggers, expressions, matrices, reusable workflows, environments, secrets and OIDC as GitHub Actions. Common actions such as checkout, setup-node and cache are built into the runner, so they finish in seconds.</p>
373 <ul>
374 <li>Each job gets a fresh Ubuntu 24.04 container with 4 vCPUs and 12 GiB of memory.</li>
375 <li>Caches restore in parallel and extract while they download.</li>
376 <li>Self-hosted runners cover macOS, Windows, Arm and GPUs.</li>
377 </ul>
378 <p>
379 <a href="/performance">See the measurements</a> · <a href="/docs/actions/compatibility">Compatibility with GitHub Actions</a>
380 </p>
381 </div>
382 {/* One line: whitespace inside <pre> shows. */}
383 {raw(`<pre class="mk-code" aria-label="A GitHub Actions workflow that runs on Diffinery unchanged"><code>${escapeHtml(HOME.workflow)}</code></pre>`)}
384 </section>
385 <section class="mk-sec">
386 <h2>Built for AI coding agents</h2>
387 <p class="mk-wide">Claude Code, Codex and other agents work through Git, the GitHub CLI, the REST API or the built-in MCP server, as accounts of their own. The server keeps a person in charge of what lands.</p>
388 <div class="features">
389 <div class="box mk-card">
390 <h3>{raw(ICONS.bot)} Agent accounts</h3>
391 <p class="muted m0">Their own logins and tokens, owned by a person or organization, with every commit stamped with who they act for.</p>
392 </div>
393 <div class="box mk-card">
394 <h3>{raw(ICONS.shield)} Guardrails</h3>
395 <p class="muted m0">No pushes to protected branches, a person's approval on every agent pull request, and no secrets for agent-written code.</p>
396 </div>
397 <div class="box mk-card">
398 <h3>{raw(ICONS.code)} MCP, sandboxes and tasks</h3>
399 <p class="muted m0">One URL connects any MCP app. Agents get sandboxes to build and test in, and tasks turn a goal into a pull request.</p>
400 </div>
401 </div>
402 <p>
403 <a href="/agents">Diffinery for AI agents</a> · <a href="/docs/connect">Connect Claude, Claude Code or Codex</a>
404 </p>
405 </section>
406 <section class="mk-sec">
407 <h2>How Diffinery compares</h2>
408 <div class="table-wrap">
409 <table class="mk-table">
410 <thead>
411 <tr>
412 <th></th>
413 {HOME.compare.cols.map((h) => (
414 <th scope="col">{h}</th>
415 ))}
416 </tr>
417 </thead>
418 <tbody>
419 {HOME.compare.rows.map((r) => (
420 <tr>
421 <th scope="row">{r[0]}</th>
422 {r.slice(1).map((v) => (
423 <td>{v}</td>
424 ))}
425 </tr>
426 ))}
427 </tbody>
428 </table>
429 </div>
430 <p class="muted small">
431 Runner services include Blacksmith and Depot. GitHub's runner sizes are from its documentation, checked {longDate(HOME_UPDATED)}. Read the full comparisons with{" "}
432 <a href="/compare/github">GitHub</a>, <a href="/compare/gitlab">GitLab</a>, <a href="/compare/blacksmith">Blacksmith</a> and <a href="/compare/depot">Depot</a>.
433 </p>
434 </section>
435 <section class="mk-sec mk-faq" id="faq">
436 <h2>Frequently asked questions</h2>
437 {HOME.faq.map((f) => (
438 <div class="mk-q">
439 <h3>{f.q}</h3>
440 <p>{raw(inline(f.a))}</p>
441 </div>
442 ))}
443 </section>
444 <section class="mk-band">
445 <h2>Run Diffinery on your own Cloudflare account</h2>
446 <p class="muted">Open source under Apache-2.0. Your workflows, your editor and gh keep working, your agents get guardrails, and you pay Cloudflare only for what you use.</p>
447 <p class="row" style="justify-content:center">
448 <Cta />
449 </p>
450 </section>
451 </div>
452);
453
454/** The home page for signed-out visitors (registerAccount calls this for "/"). */
455export function landingPage(c: AppContext): Response | Promise<Response> {
456 const o = origin(c);
457 if (wantsMarkdown(c)) return markdownResponse(c, homeMarkdown(o));
458 c.header("link", `<${o}/index.md>; rel="alternate"; type="text/markdown", <${o}/llms.txt>; rel="describedby"; type="text/plain"`);
459 c.header("vary", "Accept");
460 return render(
461 c,
462 {
463 title: "",
464 home: true,
465 seoTitle: HOME.title,
466 description: HOME.description,
467 jsonLd: [...entities(o), { "@type": "WebPage", "@id": `${o}/#webpage`, url: `${o}/`, name: HOME.title, description: HOME.description, isPartOf: { "@id": ids(o).site }, about: { "@id": ids(o).app }, dateModified: HOME_UPDATED, inLanguage: "en" }, ...faqPage(`${o}/`, HOME.faq)],
468 head: <link rel="alternate" type="text/markdown" href="/index.md" title="Diffinery (Markdown)" />,
469 },
470 <Landing />,
471 );
472}
473
474// ---------------------------------------------------------------- the Markdown pages (site/)
475
476const wantsMarkdown = (c: AppContext) => /\btext\/markdown\b/i.test(c.req.header("accept") ?? "");
477const PUBLIC = { "cache-control": "public, max-age=300", "access-control-allow-origin": "*", "x-content-type-options": "nosniff" };
478const markdownResponse = (c: AppContext, body: string) => c.body(body, 200, { ...PUBLIC, "content-type": "text/markdown; charset=utf-8", vary: "Accept" });
479
480const htmlCache = new Map<string, string>();
481async function pageHtml(c: AppContext, p: SitePageInfo): Promise<string> {
482 let html = htmlCache.get(p.path);
483 if (html === undefined) {
484 html = renderMarkdown(p.markdown, { idPrefix: "", highlight: await fenceHighlighter(fenceLangs(p.markdown)), image: imageProxy(c.env) });
485 htmlCache.set(p.path, html);
486 }
487 return html;
488}
489
490/** Blog posts: /blog/<post>, not the /blog index. */
491const isPost = (p: SitePageInfo) => p.path.startsWith("/blog/");
492
493/** Comparisons, measurements and blog posts are articles: dated, with sources. */
494const isArticle = (p: SitePageInfo) => p.path.startsWith("/compare/") || p.path === "/performance" || p.path === "/built-on-cloudflare" || isPost(p);
495
496function crumbs(p: SitePageInfo): Array<{ name: string; path: string }> {
497 const out = [{ name: "Home", path: "/" }];
498 if (p.path.startsWith("/compare/")) out.push({ name: "Compare", path: "/compare" });
499 if (isPost(p)) out.push({ name: "Blog", path: "/blog" });
500 out.push({ name: p.title, path: p.path });
501 return out;
502}
503
504async function sitePageResponse(c: AppContext, p: SitePageInfo): Promise<Response> {
505 const o = origin(c);
506 const url = `${o}${p.path}`;
507 const html = await pageHtml(c, p);
508 const id = ids(o);
509 const trail = crumbs(p);
510 const related = sitePages().filter((x) => x.section === p.section && x.path !== p.path);
511 c.header("link", `<${o}${p.path}.md>; rel="alternate"; type="text/markdown", <${o}/llms.txt>; rel="describedby"; type="text/plain"`);
512 c.header("vary", "Accept");
513 const page = {
514 "@type": isPost(p) ? "BlogPosting" : isArticle(p) ? "Article" : p.path === "/about" ? "AboutPage" : "WebPage",
515 "@id": `${url}#page`,
516 url,
517 name: p.title,
518 ...(isArticle(p) ? { headline: p.title } : {}),
519 description: p.description,
520 inLanguage: "en",
521 ...(isPost(p) ? { datePublished: p.published ?? p.updated } : {}),
522 dateModified: p.updated,
523 ...(isArticle(p) ? { author: { "@id": id.org } } : {}),
524 publisher: { "@id": id.org },
525 isPartOf: { "@id": id.site },
526 about: { "@id": id.app },
527 breadcrumb: { "@id": `${url}#breadcrumb` },
528 };
529 const breadcrumb = { "@type": "BreadcrumbList", "@id": `${url}#breadcrumb`, itemListElement: trail.map((t, i) => ({ "@type": "ListItem", position: i + 1, name: t.name, item: `${o}${t.path}` })) };
530 return render(
531 c,
532 {
533 title: p.title,
534 seoTitle: p.seoTitle ?? undefined,
535 description: p.description,
536 modified: isArticle(p) ? p.updated : undefined,
537 jsonLd: [...entities(o), page, breadcrumb, ...faqPage(url, faqs(p.markdown))],
538 head: <link rel="alternate" type="text/markdown" href={`${p.path}.md`} title={`${p.title} (Markdown)`} />,
539 },
540 <article class="mk mk-page">
541 <style>{raw(MARKETING_CSS)}</style>
542 <header class="mk-head">
543 <nav class="mk-crumbs" aria-label="Breadcrumb">
544 {trail.slice(0, -1).map((t) => (
545 <>
546 <a href={t.path}>{t.name}</a> <span class="faint">/</span>{" "}
547 </>
548 ))}
549 </nav>
550 <h1>{p.title}</h1>
551 <p class="mk-lead">{p.description}</p>
552 <p class="mk-meta muted small">
553 {isPost(p) && p.published && p.published !== p.updated ? (
554 <>
555 Published <time datetime={p.published}>{longDate(p.published)}</time> ·{" "}
556 </>
557 ) : null}
558 Last updated <time datetime={p.updated}>{longDate(p.updated)}</time> ·{" "}
559 <a href={`${p.path}.md`} title="This page as plain Markdown">
560 View as Markdown
561 </a>
562 </p>
563 </header>
564 <div class="markdown mk-body">{raw(html)}</div>
565 <aside class="mk-band">
566 <h2>Run Diffinery yourself</h2>
567 <p class="muted">Open source under Apache-2.0, deployed to your own Cloudflare account.</p>
568 <p class="row" style="justify-content:center">
569 <Cta />
570 </p>
571 </aside>
572 {related.length ? (
573 <nav class="mk-related" aria-label={`More in ${p.section}`}>
574 <h2>More in {p.section}</h2>
575 <ul>
576 {related.map((r) => (
577 <li>
578 <a href={r.path}>{r.title}</a>
579 <span class="muted small">{r.description}</span>
580 </li>
581 ))}
582 </ul>
583 </nav>
584 ) : null}
585 </article>,
586 );
587}
588
589/** The sitemap's entries for the site: the home page first, then every page with the date it was last checked. */
590export function siteSitemapEntries(): Array<{ path: string; updated: string }> {
591 return [{ path: "/", updated: HOME_UPDATED }, ...sitePages().map((p) => ({ path: p.path, updated: p.updated }))];
592}
593
594/** The site's part of /llms.txt: what Diffinery is, then every site page as Markdown. */
595export function siteLlmsLines(o: string): string[] {
596 const lines = [
597 "## About Diffinery",
598 "",
599 `- [Home page](${o}/index.md): ${HOME.description}`,
600 `- Open source under the Apache License 2.0 (SPDX: ${SOURCE.spdx}). Source code: ${SOURCE.repo}, mirrored to ${SOURCE.github}. Free, with no paid edition and nothing sold.`,
601 `- How to use it: deploy your own instance to your own Cloudflare account and domain (Workers Paid plan); you pay Cloudflare for what it uses. Guide: ${o}/docs/self-hosting/deploy.md. Overview: ${o}/docs/self-hosting.md. There's no hosted service for other teams: diffinery.com is the project's home and its own instance, with accounts by invitation for the people working on Diffinery.`,
602 `- Runs entirely on Cloudflare (Workers, Durable Objects, R2, D1, Queues, Workflows, Containers): code, pull requests, CI runners and the agent features alike. It is ${CLOUDFLARE_CLAIM} See ${o}/built-on-cloudflare.md, which compares the other Git and CI projects on Cloudflare.`,
603 "- Hosted CI runners: Ubuntu 24.04 x64 containers, 4 vCPU and 12 GiB of memory by default, with smaller sizes chosen per job from its measured runs. No hosted macOS, Windows or Arm runners; self-hosted runners cover them.",
604 "- Runs GitHub Actions workflow files from `.github/workflows/` as they are; differences are listed at " + `${o}/docs/actions/compatibility.md`,
605 "",
606 ];
607 for (const s of SITE_NAV) {
608 lines.push(`## ${s.title}`, "");
609 for (const path of s.pages) {
610 const p = sitePage(path)!;
611 lines.push(`- [${p.title}](${o}${path}.md): ${p.description} (updated ${p.updated})`);
612 }
613 lines.push("");
614 }
615 return lines;
616}
617
618/** The site's pages for /llms-full.txt, home page first. */
619export function siteFullMarkdown(o: string): string[] {
620 return [homeMarkdown(o), ...sitePages().map((p) => sitePageMarkdown(p, o))];
621}
622
623export function registerSite(web: WebApp): void {
624 web.get("/index.md", (c) => markdownResponse(c, homeMarkdown(origin(c))));
625 for (const p of sitePages()) {
626 web.get(`${p.path}.md`, (c) => markdownResponse(c, sitePageMarkdown(p, origin(c))));
627 web.get(`${p.path}/`, (c) => c.redirect(p.path, 301));
628 web.get(p.path, (c) => (wantsMarkdown(c) ? markdownResponse(c, sitePageMarkdown(p, origin(c))) : sitePageResponse(c, p)));
629 }
630}
631
632export const MARKETING_CSS = `
633.mk .hero h1{max-width:820px;margin-left:auto;margin-right:auto;font-size:48px;line-height:1.08}
634.mk .hero>p:first-of-type{max-width:720px}
635.mk-stats{list-style:none;margin:8px 0 8px;padding:0;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
636.mk-stats li{border:1px solid var(--border);border-radius:var(--r);background:var(--bg);padding:16px}
637.mk-stats b{display:block;font-size:24px;letter-spacing:-.02em;margin-bottom:4px}
638.mk-stats span{color:var(--fg2);font-size:13.5px;line-height:1.45}
639.mk-sec{margin:56px 0 0}
640.mk-sec>h2,.mk-band h2,.mk-related h2{font-size:26px;letter-spacing:-.02em;margin:0 0 10px}
641.mk-wide{max-width:760px;color:var(--fg2);font-size:16px}
642.mk .features{grid-template-columns:repeat(3,minmax(0,1fr))}
643.mk-card{display:block;color:var(--fg)}a.mk-card:hover{border-color:var(--accent);text-decoration:none}
644.mk-card h3{display:flex;align-items:center;gap:8px}.mk-card h3 svg{flex:none;color:var(--accent)}
645.mk-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:32px;align-items:center}
646.mk-split>*{min-width:0}
647.mk .table-wrap{overflow:auto}
648.mk-split p,.mk-split li{color:var(--fg2);font-size:15px;line-height:1.65}
649.mk-code{margin:0;padding:18px 20px;background:var(--code-bg);border:1px solid var(--border);border-radius:var(--r);overflow:auto;font-size:13px;line-height:1.6}
650.mk-table{width:100%;border-collapse:collapse;font-size:14px}
651.mk-table th,.mk-table td{border-bottom:1px solid var(--border2);padding:10px 12px;text-align:left;vertical-align:top}
652.mk-table thead th{font-weight:600;border-bottom:1px solid var(--border)}.mk-table thead th:nth-child(2){color:var(--accent)}
653.mk-table tbody th{font-weight:500;color:var(--fg2)}
654.mk-faq .mk-q{border-top:1px solid var(--border2);padding:14px 0}.mk-faq h3{font-size:16px;margin:0 0 6px}.mk-faq p{margin:0;color:var(--fg2);line-height:1.65;max-width:860px}
655.mk-band{margin:56px 0 0;padding:36px 20px;text-align:center;border:1px solid var(--border);border-radius:var(--r);background:var(--bg2)}
656.mk-band p{margin:0 auto 14px;max-width:620px}
657.mk-page{max-width:880px;margin:0 auto}
658.mk-head{padding:28px 0 18px;border-bottom:1px solid var(--border2);margin-bottom:8px}
659.mk-head h1{font-size:36px;line-height:1.15;letter-spacing:-.025em;margin:8px 0 10px}
660.mk-crumbs{font-size:13px;min-height:18px}
661.mk-lead{font-size:18px;color:var(--fg2);margin:0 0 10px;line-height:1.55}
662.mk-meta{margin:0}
663.mk-body{font-size:15.5px;line-height:1.7}
664.mk-body h2{font-size:23px;margin-top:38px;scroll-margin-top:64px}.mk-body h3{font-size:17px;margin-top:26px;scroll-margin-top:64px}
665.mk-body table{font-size:14px;width:100%}.mk-body td code{white-space:nowrap}
666.mk-related{margin:40px 0 0}
667.mk-related ul{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px}
668.mk-related li{border:1px solid var(--border);border-radius:var(--r);padding:12px 14px;display:flex;flex-direction:column;gap:4px}
669@media (max-width:900px){.mk .features{grid-template-columns:repeat(2,minmax(0,1fr))}.mk-stats{grid-template-columns:repeat(2,minmax(0,1fr))}.mk-split{grid-template-columns:minmax(0,1fr)}}
670@media (max-width:600px){.mk .features{grid-template-columns:minmax(0,1fr)}.mk .hero{padding-top:40px}.mk .hero h1{font-size:32px}.mk-head h1{font-size:27px}.mk-stats{grid-template-columns:minmax(0,1fr)}.mk-sec>h2,.mk-band h2{font-size:22px}}
671`;