diff --git a/apps/docs/package.json b/apps/docs/package.json new file mode 100644 index 00000000..d6850898 --- /dev/null +++ b/apps/docs/package.json @@ -0,0 +1,14 @@ +{ + "name": "@cloudhal/docs", + "version": "0.0.1", + "private": true, + "description": "cloudhal.dev: the developer site (docs, JSON Schemas, the fetcher page). ADR-0037.", + "type": "module", + "scripts": { + "build": "node scripts/build.ts", + "deploy": "node scripts/build.ts && wrangler deploy" + }, + "devDependencies": { + "wrangler": "catalog:" + } +} diff --git a/apps/docs/scripts/build.ts b/apps/docs/scripts/build.ts new file mode 100644 index 00000000..4920c8da --- /dev/null +++ b/apps/docs/scripts/build.ts @@ -0,0 +1,89 @@ +/** + * Builds cloudhal.dev into `dist/` (ADR-0037): + * + * - `static/` as is (home page, /fetch, 404, `_headers`, `_redirects`, robots.txt); + * - `/schemas/v1/.schema.json` from `packages/core-contracts/schemas`, the permanent `$id` + * URLs, plus `/schemas/v1/index.json` listing them; + * - `/llms.txt`, an index for agents (llmstxt.org); + * - one `/docs/guides/` redirect per guide, prepended to `_redirects` (static rules win over + * the `/docs/*` catch-all). + * + * Every schema's `$id` must already be its URL here; the build fails otherwise. + */ +import { cp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const APP = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const SCHEMAS = path.resolve(APP, "../../packages/core-contracts/schemas"); +const GUIDES = path.resolve(APP, "../../docs/guides"); +const DIST = path.join(APP, "dist"); +const ORIGIN = "https://cloudhal.dev"; + +interface SchemaEntry { + readonly name: string; + readonly url: string; + readonly title: string; +} + +async function copySchemas(): Promise { + const out = path.join(DIST, "schemas/v1"); + await mkdir(out, { recursive: true }); + const entries: SchemaEntry[] = []; + for (const file of (await readdir(SCHEMAS)).filter((f) => f.endsWith(".schema.json")).sort()) { + const text = await readFile(path.join(SCHEMAS, file), "utf8"); + const schema = JSON.parse(text) as { $id?: string; title?: string }; + const url = `${ORIGIN}/schemas/v1/${file}`; + if (schema.$id !== url) throw new Error(`${file}: $id is ${schema.$id}, expected ${url}`); + await writeFile(path.join(out, file), text); + entries.push({ name: file, url, title: schema.title ?? file }); + } + await writeFile( + path.join(out, "index.json"), + `${JSON.stringify({ version: 1, schemas: entries }, null, 2)}\n`, + ); + return entries; +} + +function llmsTxt(schemas: readonly SchemaEntry[]): string { + return [ + "# Cloudhal", + "", + "> Agent-native site platform: sites are typed graphs, agents change them through an MCP server behind a policy engine, and every change is a snapshot you can diff, preview and roll back. Runs on Cloudflare or self-hosted. Open source, TypeScript.", + "", + "## Docs", + "", + "- [README](https://raw.githubusercontent.com/cloudhal-dev/cloudhal/main/README.md): install, first site, connect an agent", + "- [Guides](https://github.com/cloudhal-dev/cloudhal/tree/main/docs/guides): deploy button, search, schedules, comments, multi-site, translations", + "- [Architecture decisions](https://github.com/cloudhal-dev/cloudhal/tree/main/docs/adr)", + "- [AGENTS.md](https://raw.githubusercontent.com/cloudhal-dev/cloudhal/main/AGENTS.md): conventions for coding agents working on Cloudhal", + "", + "## JSON Schemas (v1)", + "", + ...schemas.map((s) => `- [${s.title}](${s.url})`), + "", + "## Optional", + "", + "- [Fetcher](https://cloudhal.dev/fetch): what the Cloudhal-Fetch/1 user agent requests", + "- [Playground](https://try.cloudhal.com): a private Cloudhal site for one hour, no account", + "", + ].join("\n"); +} + +await rm(DIST, { recursive: true, force: true }); +await cp(path.join(APP, "static"), DIST, { recursive: true }); +const schemas = await copySchemas(); +await writeFile(path.join(DIST, "llms.txt"), llmsTxt(schemas)); +const guides = (await readdir(GUIDES)).filter((f) => f.endsWith(".md")).map((f) => f.slice(0, -3)); +const redirects = path.join(DIST, "_redirects"); +await writeFile( + redirects, + guides + .map( + (g) => + `/docs/guides/${g} https://github.com/cloudhal-dev/cloudhal/blob/main/docs/guides/${g}.md 302`, + ) + .join("\n") + .concat("\n", await readFile(redirects, "utf8")), +); +console.log(`built cloudhal.dev: ${schemas.length} schemas`); diff --git a/apps/docs/static/404.html b/apps/docs/static/404.html new file mode 100644 index 00000000..53eb48be --- /dev/null +++ b/apps/docs/static/404.html @@ -0,0 +1,16 @@ + + + + + + Not found + + + +
+
Cloudhal
+

Not found

+

There is nothing at this address. Try the home page, the docs or the JSON Schemas.

+
+ + diff --git a/apps/docs/static/_headers b/apps/docs/static/_headers new file mode 100644 index 00000000..0ef1b82b --- /dev/null +++ b/apps/docs/static/_headers @@ -0,0 +1,15 @@ +/* + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + Strict-Transport-Security: max-age=31536000; includeSubDomains + +/schemas/*.schema.json + Content-Type: application/schema+json; charset=utf-8 + +/schemas/* + Access-Control-Allow-Origin: * + Cache-Control: public, max-age=3600 + +/llms.txt + Content-Type: text/markdown; charset=utf-8 + Access-Control-Allow-Origin: * diff --git a/apps/docs/static/_redirects b/apps/docs/static/_redirects new file mode 100644 index 00000000..588bac5b --- /dev/null +++ b/apps/docs/static/_redirects @@ -0,0 +1,5 @@ +# Until the docs site lands, /docs points at the docs on GitHub (302: these links will change). +/docs/deploy https://github.com/cloudhal-dev/cloudhal/blob/main/docs/guides/deploy-button.md 302 +/docs https://github.com/cloudhal-dev/cloudhal#readme 302 +/docs/* https://github.com/cloudhal-dev/cloudhal/tree/main/docs 302 +/github https://github.com/cloudhal-dev/cloudhal 302 diff --git a/apps/docs/static/fetch.html b/apps/docs/static/fetch.html new file mode 100644 index 00000000..e2f98c92 --- /dev/null +++ b/apps/docs/static/fetch.html @@ -0,0 +1,41 @@ + + + + + + Cloudhal-Fetch + + + + +
+
+ Cloudhal + +
+ +

Cloudhal-Fetch

+

You found Cloudhal-Fetch/1 (+https://cloudhal.dev/fetch) in your logs. It is the media fetcher of Cloudhal, an open-source site platform.

+ +

Who sends it

+

Cloudhal is self-hosted: every site runs on its owner's own Cloudflare account or server. A request with this user agent comes from one such site, when its owner or an agent they approved imports an image or file by URL. The Cloudhal project does not crawl the web and does not run these requests itself.

+ +

What it requests

+
    +
  • A single GET for the exact URL someone gave it. It does not follow links or crawl.
  • +
  • No cookies, no Authorization header, no headers forwarded from the person's browser.
  • +
  • HTTPS on port 443 only; at most 3 redirects, each checked again.
  • +
  • Media types only (images, video, audio, documents). Web pages are refused.
  • +
  • At most 25 MB, within 30 seconds.
  • +
+ +

Blocking it

+

Because each request comes from a different site's infrastructure, block by user agent: refuse requests whose User-Agent starts with Cloudhal-Fetch/. The fetcher treats any status other than 200 as a failure and tells the person the import did not work.

+ +

Contact

+

If a Cloudhal site is misusing the fetcher, write to security@cloudhal.com with the request time, URL and source address. The rules above are specified in RFC-0004 §5.7.

+ + +
+ + diff --git a/apps/docs/static/index.html b/apps/docs/static/index.html new file mode 100644 index 00000000..5b761d62 --- /dev/null +++ b/apps/docs/static/index.html @@ -0,0 +1,46 @@ + + + + + + Cloudhal for developers + + + + + +
+
+ Cloudhal + +
+ +

Sites your agents can build, safely.

+

Cloudhal is an open-source site platform. Sites are typed graphs; agents change them through an MCP server behind a policy engine; every change is a snapshot you can diff, preview and roll back. It runs on Cloudflare or on your own machine.

+ +

Start

+
npm create cloudhal@latest my-site
+cd my-site
+npx cloudhal dev
+

Then connect your agent: claude mcp add cloudhal -- npx cloudhal mcp --mode edits. Any MCP client works.

+

Or deploy a blog to your Cloudflare account in one click with the Deploy to Cloudflare button, or try a private site for an hour at try.cloudhal.com.

+ +

Reference

+
    +
  • Docs: guides and architecture decisions (on GitHub until the docs site is ready).
  • +
  • JSON Schemas v1: the contracts every Cloudhal implementation shares. Their URLs are permanent.
  • +
  • llms.txt: an index of this site for agents.
  • +
  • Cloudhal-Fetch: what the fetcher requests from your server, and how to reach us.
  • +
+ + +
+ + diff --git a/apps/docs/static/robots.txt b/apps/docs/static/robots.txt new file mode 100644 index 00000000..c2a49f4f --- /dev/null +++ b/apps/docs/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/apps/docs/static/site.css b/apps/docs/static/site.css new file mode 100644 index 00000000..89dd6dcc --- /dev/null +++ b/apps/docs/static/site.css @@ -0,0 +1,112 @@ +:root { + --bg: #fbfbfa; + --fg: #1c1c1a; + --muted: #5f5f5a; + --line: #e4e4e0; + --accent: #b4441f; + --code: #f1f1ee; + color-scheme: light; +} +@media (prefers-color-scheme: dark) { + :root { + --bg: #141413; + --fg: #ececea; + --muted: #a3a39d; + --line: #2c2c2a; + --accent: #ee8a64; + --code: #1f1f1d; + color-scheme: dark; + } +} +* { + box-sizing: border-box; +} +body { + margin: 0; + background: var(--bg); + color: var(--fg); + font: + 16px / 1.6 ui-sans-serif, + system-ui, + -apple-system, + "Segoe UI", + sans-serif; +} +main { + max-width: 44rem; + margin: 0 auto; + padding: 4rem 1rem 5rem; +} +header { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 1rem; + flex-wrap: wrap; +} +.brand { + font-weight: 600; + font-size: 1.125rem; + color: var(--fg); + text-decoration: none; +} +nav { + display: flex; + gap: 1rem; + flex-wrap: wrap; + font-size: 0.9375rem; +} +h1 { + font-size: 2rem; + line-height: 1.2; + font-weight: 600; + margin: 3rem 0 0.75rem; +} +h2 { + font-size: 1.125rem; + font-weight: 600; + margin: 2.5rem 0 0.5rem; +} +p, +li { + color: var(--muted); +} +strong { + color: var(--fg); + font-weight: 600; +} +a { + color: var(--accent); +} +pre, +code { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.875em; +} +pre { + background: var(--code); + border: 1px solid var(--line); + border-radius: 8px; + padding: 0.75rem 1rem; + overflow-x: auto; + color: var(--fg); +} +code { + background: var(--code); + border-radius: 4px; + padding: 0.1em 0.3em; +} +pre code { + background: none; + padding: 0; +} +ul { + padding-inline-start: 1.25rem; +} +footer { + margin-top: 4rem; + padding-top: 1.5rem; + border-top: 1px solid var(--line); + font-size: 0.875rem; + color: var(--muted); +} diff --git a/apps/docs/wrangler.jsonc b/apps/docs/wrangler.jsonc new file mode 100644 index 00000000..f811d945 --- /dev/null +++ b/apps/docs/wrangler.jsonc @@ -0,0 +1,15 @@ +// cloudhal.dev (ADR-0037): static assets only, no Worker code. +// Until the Starlight site lands (RFC pending), /docs redirects to the docs on GitHub. +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "cloudhal-dev", + "compatibility_date": "2026-10-01", + "assets": { + "directory": "./dist", + "not_found_handling": "404-page", + "html_handling": "drop-trailing-slash" + }, + "routes": [{ "pattern": "cloudhal.dev", "custom_domain": true }], + "workers_dev": false, + "preview_urls": false +} diff --git a/docs/adr/0037-domain-strategy.md b/docs/adr/0037-domain-strategy.md index 51114e53..c8b6a5e6 100644 --- a/docs/adr/0037-domain-strategy.md +++ b/docs/adr/0037-domain-strategy.md @@ -6,7 +6,7 @@ - Sources: [Cloudflare: create a self-managed OAuth client](https://developers.cloudflare.com/fundamentals/oauth/create-an-oauth-client/) · [HSTS preload list (all `.dev` names are preloaded)](https://hstspreload.org) · [EmDash playground](https://github.com/emdash-cms/emdash/tree/main/demos/playground) ## Context -Cloudhal owns exactly two domains, `cloudhal.com` and `cloudhal.dev`, and will not buy a third. The repo already uses `https://cloudhal.dev/schemas/v1/*` as JSON Schema `$id`, `https://cloudhal.dev/docs`, the bot User-Agent page `https://cloudhal.dev/fetch`, and `security@`, `legal@` and `conduct@cloudhal.com` for mail. As of 2026-10-20 neither domain has DNS records. +Cloudhal owns exactly two domains, `cloudhal.com` and `cloudhal.dev`, and will not buy a third. The repo already uses `https://cloudhal.dev/schemas/v1/*` as JSON Schema `$id`, `https://cloudhal.dev/docs`, the bot User-Agent page `https://cloudhal.dev/fetch`, and `security@`, `legal@` and `conduct@cloudhal.com` for mail. As of 2026-10-20 neither domain had DNS records; cloudhal.dev went live on 2026-10-21 from `apps/docs`. Planned: a future hosted control plane that installs and manages Cloudhal in the customer's own Cloudflare account, and a public playground modelled on EmDash's (a per-visitor SQLite Durable Object, one-hour TTL, no login) in a separate repository, `cloudhal-dev/try`. The question is which domain carries what, and where customer sites live. @@ -49,6 +49,6 @@ Planned: a future hosted control plane that installs and manages Cloudhal in the ## Operator checklist (not done by this change; done in Cloudflare) - [ ] DNS for both zones. -- [ ] Serve `packages/core-contracts/schemas` at `https://cloudhal.dev/schemas/v1/` with `Content-Type: application/schema+json`. +- [x] Serve `packages/core-contracts/schemas` at `https://cloudhal.dev/schemas/v1/` with `Content-Type: application/schema+json`. - [ ] SPF, DKIM and DMARC for `cloudhal.com`. - [ ] `cloudhal.dev` sends no mail: `v=spf1 -all`, DMARC `p=reject`, null MX. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 269ea4e0..4f223692 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -414,6 +414,12 @@ importers: specifier: 'catalog:' version: 5.0.3(@types/node@26.6.5)(happy-dom@21.0.3)(vite@8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) + apps/docs: + devDependencies: + wrangler: + specifier: 'catalog:' + version: 4.148.0(@cloudflare/workers-types@5.20261007.1)(@types/node@26.6.5) + apps/studio: dependencies: '@cloudhal/core-contracts':