From 55a7a1e57c5cbe88d061e8f3fe45d6d8f3efc768 Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Thu, 29 Oct 2026 21:32:44 -0700 Subject: [PATCH] docs: move documentation under /docs with standard section names Docs move from the onebox.run root to /docs, so the root can hold the landing page. Sections use the names most common across developer-tool docs: get-started, guides, reference, concepts. Generic guide slugs become the common nouns (databases, backups, secrets, rollback, migrations, cron-jobs, environment-variables), the capability matrix moves from status/ into reference/, and /docs gets a documentation home. The site builds with format "preserve", so Starlight's links carry no .html; the route middleware still corrects the canonical, and check-docs-urls fails the build on a .html link or a docs page without its Markdown export or llms.txt entry. ob-docgen writes the reference pages to the new paths, and the CLI's install remedy links to /docs/get-started/install. Old URLs are not redirected. --- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .gitignore | 2 +- CONTRIBUTING.md | 4 +- README.md | 42 +++++------ cmd/ob-docgen/main.go | 8 +- cmd/ob-docgen/main_test.go | 4 +- cmd/ob/docs_test.go | 4 +- context7.json | 4 +- docs/README.md | 8 +- docs/product.md | 4 +- internal/app/jsonschema_test.go | 4 +- internal/app/prerequisites.go | 2 +- site/astro.config.mjs | 67 ++++++++--------- site/package.json | 2 +- site/scripts/check-canonical.mjs | 16 +++- site/scripts/check-docs-urls.mjs | 43 +++++++++++ site/scripts/markdown.test.mjs | 24 ++++++ site/src/components/SocialIcons.astro | 13 ++++ .../concepts}/evidence-not-declaration.mdx | 0 .../concepts}/generated-compose.mdx | 4 +- .../concepts}/ownership-boundary.mdx | 6 +- .../concepts}/safety-envelope.mdx | 2 +- .../concepts}/what-onebox-refuses.mdx | 6 +- .../get-started}/first-deploy.mdx | 4 +- .../{start => docs/get-started}/install.mdx | 2 +- .../get-started/overview.mdx} | 6 +- .../get-started}/reading-it-back.mdx | 4 +- .../guides/backups.mdx} | 4 +- .../guides/compose.mdx} | 4 +- .../guides/cron-jobs.mdx} | 2 +- .../guides/databases.mdx} | 4 +- .../content/docs/{ => docs}/guides/eject.mdx | 0 .../guides/environment-variables.mdx | 2 +- .../guides/jump-host.mdx} | 2 +- .../guides/migrations.mdx} | 0 .../guides/rollback.mdx} | 0 .../guides/secrets.mdx} | 0 .../guides/upgrade-to-onebox-names.mdx | 0 site/src/content/docs/docs/index.mdx | 75 +++++++++++++++++++ .../reference}/capabilities.mdx | 10 +-- .../content/docs/{ => docs}/reference/cli.mdx | 0 .../docs/{ => docs}/reference/drivers.mdx | 4 +- .../docs/{ => docs}/reference/errors.mdx | 0 .../reference/fields/backup-targets.mdx | 0 .../{ => docs}/reference/fields/checks.mdx | 0 .../reference/fields/deployment.mdx | 0 .../reference/fields/environments.mdx | 0 .../reference/fields/external-services.mdx | 0 .../{ => docs}/reference/fields/hooks.mdx | 0 .../reference/fields/notifications.mdx | 0 .../{ => docs}/reference/fields/proxy.mdx | 0 .../reference/fields/registries.mdx | 0 .../{ => docs}/reference/fields/runtime.mdx | 0 .../{ => docs}/reference/fields/services.mdx | 0 .../{ => docs}/reference/fields/top-level.mdx | 0 .../{ => docs}/reference/fields/workloads.mdx | 0 .../docs/{ => docs}/reference/naming.mdx | 0 .../docs/{ => docs}/reference/policies.mdx | 0 .../{ => docs}/reference/project-file.mdx | 32 ++++---- site/src/content/docs/index.mdx | 14 ++-- site/src/lib/markdown.ts | 9 ++- site/src/pages/llms-full.txt.ts | 18 +++-- site/src/pages/llms.txt.ts | 27 ++++--- site/src/starlight-route-data.ts | 15 ++-- 65 files changed, 343 insertions(+), 168 deletions(-) create mode 100644 site/scripts/check-docs-urls.mjs create mode 100644 site/scripts/markdown.test.mjs create mode 100644 site/src/components/SocialIcons.astro rename site/src/content/docs/{explanation => docs/concepts}/evidence-not-declaration.mdx (100%) rename site/src/content/docs/{explanation => docs/concepts}/generated-compose.mdx (97%) rename site/src/content/docs/{explanation => docs/concepts}/ownership-boundary.mdx (96%) rename site/src/content/docs/{explanation => docs/concepts}/safety-envelope.mdx (98%) rename site/src/content/docs/{explanation => docs/concepts}/what-onebox-refuses.mdx (96%) rename site/src/content/docs/{start => docs/get-started}/first-deploy.mdx (98%) rename site/src/content/docs/{start => docs/get-started}/install.mdx (99%) rename site/src/content/docs/{start/what-onebox-is.mdx => docs/get-started/overview.mdx} (93%) rename site/src/content/docs/{start => docs/get-started}/reading-it-back.mdx (95%) rename site/src/content/docs/{guides/back-up-a-database.mdx => docs/guides/backups.mdx} (98%) rename site/src/content/docs/{guides/adopt-compose.mdx => docs/guides/compose.mdx} (95%) rename site/src/content/docs/{guides/schedule-a-job.mdx => docs/guides/cron-jobs.mdx} (99%) rename site/src/content/docs/{guides/add-a-database.mdx => docs/guides/databases.mdx} (98%) rename site/src/content/docs/{ => docs}/guides/eject.mdx (100%) rename site/src/content/docs/{ => docs}/guides/environment-variables.mdx (97%) rename site/src/content/docs/{guides/deploy-through-a-jump-host.mdx => docs/guides/jump-host.mdx} (98%) rename site/src/content/docs/{guides/run-migrations.mdx => docs/guides/migrations.mdx} (100%) rename site/src/content/docs/{guides/roll-back.mdx => docs/guides/rollback.mdx} (100%) rename site/src/content/docs/{guides/handle-secrets.mdx => docs/guides/secrets.mdx} (100%) rename site/src/content/docs/{ => docs}/guides/upgrade-to-onebox-names.mdx (100%) create mode 100644 site/src/content/docs/docs/index.mdx rename site/src/content/docs/{status => docs/reference}/capabilities.mdx (91%) rename site/src/content/docs/{ => docs}/reference/cli.mdx (100%) rename site/src/content/docs/{ => docs}/reference/drivers.mdx (98%) rename site/src/content/docs/{ => docs}/reference/errors.mdx (100%) rename site/src/content/docs/{ => docs}/reference/fields/backup-targets.mdx (100%) rename site/src/content/docs/{ => docs}/reference/fields/checks.mdx (100%) rename site/src/content/docs/{ => docs}/reference/fields/deployment.mdx (100%) rename site/src/content/docs/{ => docs}/reference/fields/environments.mdx (100%) rename site/src/content/docs/{ => docs}/reference/fields/external-services.mdx (100%) rename site/src/content/docs/{ => docs}/reference/fields/hooks.mdx (100%) rename site/src/content/docs/{ => docs}/reference/fields/notifications.mdx (100%) rename site/src/content/docs/{ => docs}/reference/fields/proxy.mdx (100%) rename site/src/content/docs/{ => docs}/reference/fields/registries.mdx (100%) rename site/src/content/docs/{ => docs}/reference/fields/runtime.mdx (100%) rename site/src/content/docs/{ => docs}/reference/fields/services.mdx (100%) rename site/src/content/docs/{ => docs}/reference/fields/top-level.mdx (100%) rename site/src/content/docs/{ => docs}/reference/fields/workloads.mdx (100%) rename site/src/content/docs/{ => docs}/reference/naming.mdx (100%) rename site/src/content/docs/{ => docs}/reference/policies.mdx (100%) rename site/src/content/docs/{ => docs}/reference/project-file.mdx (92%) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index e41b17d3..5e22bcc2 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -12,7 +12,7 @@ body: [`docs/product.md`](https://github.com/labstack/onebox/blob/main/docs/product.md) records the direction, and - [`/status/capabilities`](https://onebox.run/status/capabilities) records + [`/docs/reference/capabilities`](https://onebox.run/docs/reference/capabilities) records what the binary does today versus what the schema merely accepts. - type: textarea diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 834111b5..39adbd2a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -18,7 +18,7 @@ output you compared, or the invariant it preserves. diff --git a/.gitignore b/.gitignore index 68afe174..55198d40 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,7 @@ .playwright-mcp/ # Documentation site build output and dependencies. The generated reference -# pages under site/src/content/docs/reference/ are NOT ignored: they are +# pages under site/src/content/docs/docs/reference/ are NOT ignored: they are # committed so a reader of the repository sees the same reference a reader of # the site does, and so `just docs-generate-check` can prove they are current. /site/node_modules/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab783c1e..060adde8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ product decision, not a code review question, and it is far cheaper to settle before the code exists. [`docs/product.md`](docs/product.md) records the direction, and -`/status/capabilities` records what the binary does today versus what the schema +`/docs/reference/capabilities` records what the binary does today versus what the schema merely accepts. A change that moves something between those two states should say so in its description. @@ -73,7 +73,7 @@ just e2e ## Generated documentation The field reference, CLI reference, and error code pages under -`site/src/content/docs/reference/` are **generated from the binary** by +`site/src/content/docs/docs/reference/` are **generated from the binary** by `cmd/ob-docgen`. Do not hand-edit them; they are committed so that a reader of the repository sees the same reference a reader of the website does. diff --git a/README.md b/README.md index cc4c558b..51ef551d 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ server. [![CI](https://img.shields.io/github/actions/workflow/status/labstack/onebox/ci.yml?branch=main&label=ci&color=4f9a3c&labelColor=22291f)](https://github.com/labstack/onebox/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/labstack/onebox?label=release&color=4f9a3c&labelColor=22291f)](https://github.com/labstack/onebox/releases) [![Licence](https://img.shields.io/badge/licence-Apache--2.0-4f9a3c?labelColor=22291f)](LICENSE) -[Documentation](https://onebox.run) · [Install](https://onebox.run/start/install) · [First deploy](https://onebox.run/start/first-deploy) · [Capabilities](https://onebox.run/status/capabilities) · [Releases](https://github.com/labstack/onebox/releases) +[Documentation](https://onebox.run/docs) · [Install](https://onebox.run/docs/get-started/install) · [First deploy](https://onebox.run/docs/get-started/first-deploy) · [Capabilities](https://onebox.run/docs/reference/capabilities) · [Releases](https://github.com/labstack/onebox/releases) @@ -60,7 +60,7 @@ owns the generated application runtime inside that boundary. ### 1. Install the CLI Homebrew, Scoop, release archives, and Debian/RPM packages are available. The -[installation guide](https://onebox.run/start/install) includes checksum +[installation guide](https://onebox.run/docs/get-started/install) includes checksum verification and source builds. ```sh @@ -112,7 +112,7 @@ expires after 15 minutes; drift or a changed local payload requires a new one. Approval is a short-lived, digest-bound local confirmation. It is tamper-evident ceremony, not authenticated identity or an independently issued -capability. See [Your first deploy](https://onebox.run/start/first-deploy) for +capability. See [Your first deploy](https://onebox.run/docs/get-started/first-deploy) for the complete walkthrough and expected output. ## Refusing is part of the contract @@ -127,8 +127,8 @@ Onebox refuses configurations it cannot operate safely, including: There is no generic `--force`. Each exceptional path grants one named capability, such as breaking a stale lock or accepting a destructive volume -change. [What Onebox refuses](https://onebox.run/explanation/what-onebox-refuses) -and [the safety envelope](https://onebox.run/explanation/safety-envelope) give +change. [What Onebox refuses](https://onebox.run/docs/concepts/what-onebox-refuses) +and [the safety envelope](https://onebox.run/docs/concepts/safety-envelope) give the full rules. ## Boundaries that matter @@ -143,7 +143,7 @@ the full rules. - **MongoDB is standalone.** Applications requiring change streams or multi-document transactions need a replica set, which Onebox does not manage. -[Shipped vs proposed](https://onebox.run/status/capabilities) is the complete +[Shipped vs proposed](https://onebox.run/docs/reference/capabilities) is the complete account of what the binary executes today and what remains direction. ## How it compares @@ -172,7 +172,7 @@ are typed: branch on the code, never the sentence. There is deliberately no MCP mutation surface. Point an agent at `ob` the way you would point it at `gh`; every lifecycle decision still passes through the same canonical service and safety checks. See the -[structured-output policies](https://onebox.run/reference/policies#structured-output-contracts). +[structured-output policies](https://onebox.run/docs/reference/policies#structured-output-contracts). ## Documentation @@ -180,20 +180,20 @@ Every page on **[onebox.run](https://onebox.run)** is also available as clean Markdown at `.md`; [llms.txt](https://onebox.run/llms.txt) maps the site for agents. -- **Start:** [installation](https://onebox.run/start/install) and - [your first deploy](https://onebox.run/start/first-deploy) -- **Operate:** [databases](https://onebox.run/guides/add-a-database), - [backups](https://onebox.run/guides/back-up-a-database), - [migrations](https://onebox.run/guides/run-migrations), - [secrets](https://onebox.run/guides/handle-secrets), and - [rollback](https://onebox.run/guides/roll-back) -- **Reference:** [project file](https://onebox.run/reference/project-file), - [CLI](https://onebox.run/reference/cli), - [errors](https://onebox.run/reference/errors), and - [policies](https://onebox.run/reference/policies) -- **Understand:** [ownership boundary](https://onebox.run/explanation/ownership-boundary), - [evidence, not declaration](https://onebox.run/explanation/evidence-not-declaration), - and [generated Compose](https://onebox.run/explanation/generated-compose) +- **Start:** [installation](https://onebox.run/docs/get-started/install) and + [your first deploy](https://onebox.run/docs/get-started/first-deploy) +- **Operate:** [databases](https://onebox.run/docs/guides/databases), + [backups](https://onebox.run/docs/guides/backups), + [migrations](https://onebox.run/docs/guides/migrations), + [secrets](https://onebox.run/docs/guides/secrets), and + [rollback](https://onebox.run/docs/guides/rollback) +- **Reference:** [project file](https://onebox.run/docs/reference/project-file), + [CLI](https://onebox.run/docs/reference/cli), + [errors](https://onebox.run/docs/reference/errors), and + [policies](https://onebox.run/docs/reference/policies) +- **Understand:** [ownership boundary](https://onebox.run/docs/concepts/ownership-boundary), + [evidence, not declaration](https://onebox.run/docs/concepts/evidence-not-declaration), + and [generated Compose](https://onebox.run/docs/concepts/generated-compose) The field, CLI, and error references are generated from the binary by `cmd/ob-docgen`, so documentation cannot silently drift from the accepted diff --git a/cmd/ob-docgen/main.go b/cmd/ob-docgen/main.go index 211570f4..4c82bdb9 100644 --- a/cmd/ob-docgen/main.go +++ b/cmd/ob-docgen/main.go @@ -7,7 +7,7 @@ // contract that disagrees with the loader is worse than none — it teaches the // author something untrue and they learn to ignore it. // -// So this program is the only writer of `site/src/content/docs/reference/` +// So this program is the only writer of `site/src/content/docs/docs/reference/` // — the field pages, `drivers.mdx`, `errors.mdx` and `cli.mdx` — and of the // schema published below `site/public/schemas/application/v1alpha2/`. Those pages carry // a generated marker, @@ -45,7 +45,7 @@ func main() { obBin string check bool ) - flag.StringVar(&out, "out", "site/src/content/docs/reference", "directory to write generated reference pages into") + flag.StringVar(&out, "out", "site/src/content/docs/docs/reference", "directory to write generated reference pages into") flag.StringVar(&public, "public", "site/public", "directory to publish machine-readable artifacts into") flag.StringVar(&obBin, "ob", "", "path to a built ob binary, for the CLI reference (default: look on PATH)") flag.BoolVar(&check, "check", false, "fail if the files on disk differ from what would be generated") @@ -1009,14 +1009,14 @@ func renderDriverPage() string { fmt.Fprintln(&buf, "rather than replacing the container and leaving the data intact and unreachable.") fmt.Fprintln(&buf) fmt.Fprintln(&buf, "For `postgres`, declaring `backup` changes this — see") - fmt.Fprintln(&buf, "[Shipped vs proposed](/status/capabilities).") + fmt.Fprintln(&buf, "[Shipped vs proposed](/docs/reference/capabilities).") fmt.Fprintln(&buf, ":::") fmt.Fprintln(&buf) fmt.Fprintln(&buf, "## Anything else") fmt.Fprintln(&buf) fmt.Fprintln(&buf, "Run it as a `daemon` workload and you own it: the image, the credential, the") - fmt.Fprintln(&buf, "volumes, the backup. See [Add a database](/guides/add-a-database) for the") + fmt.Fprintln(&buf, "volumes, the backup. See [Add a database](/docs/guides/databases) for the") fmt.Fprintln(&buf, "comparison.") return buf.String() diff --git a/cmd/ob-docgen/main_test.go b/cmd/ob-docgen/main_test.go index 9cbbe153..d20ffe73 100644 --- a/cmd/ob-docgen/main_test.go +++ b/cmd/ob-docgen/main_test.go @@ -26,7 +26,7 @@ func TestCLIReferenceMatchesBinary(t *testing.T) { if err != nil { t.Fatal(err) } - path := filepath.Join("..", "..", "site", "src", "content", "docs", "reference", "cli.mdx") + path := filepath.Join("..", "..", "site", "src", "content", "docs", "docs", "reference", "cli.mdx") committed, err := os.ReadFile(path) if err != nil { t.Fatal(err) @@ -360,7 +360,7 @@ func TestCommittedPagesMatchTheGenerator(t *testing.T) { want["errors.mdx"] = renderErrorPage() want["drivers.mdx"] = renderDriverPage() - root := filepath.Join("..", "..", "site", "src", "content", "docs", "reference") + root := filepath.Join("..", "..", "site", "src", "content", "docs", "docs", "reference") for name, body := range want { onDisk, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(name))) if err != nil { diff --git a/cmd/ob/docs_test.go b/cmd/ob/docs_test.go index 0c8b897f..49fc745d 100644 --- a/cmd/ob/docs_test.go +++ b/cmd/ob/docs_test.go @@ -11,9 +11,9 @@ import ( "github.com/spf13/pflag" ) -const cliReferencePage = "site/src/content/docs/reference/cli.mdx" +const cliReferencePage = "site/src/content/docs/docs/reference/cli.mdx" -const policiesPage = "site/src/content/docs/reference/policies.mdx" +const policiesPage = "site/src/content/docs/docs/reference/policies.mdx" // readDocsPage reads a documentation page from the site. The remediation hint // belongs at the call site: only some of these pages are generated, and telling diff --git a/context7.json b/context7.json index f96b4187..f79db146 100644 --- a/context7.json +++ b/context7.json @@ -3,13 +3,13 @@ "projectTitle": "Onebox", "description": "Plan-before-apply deploys for one application on one Linux server. Zero downtime, no agent on the host.", "branch": "main", - "folders": ["site/src/content/docs", "README.md"], + "folders": ["site/src/content/docs/docs", "README.md"], "excludeFolders": ["docs/plans", "docs/superpowers", "docs/media"], "rules": [ "The CLI is `ob`. The application is declared in `ob.yml` (or `ob.yaml`), from which Onebox derives the Compose runtime, stable names, routing, supporting services, and release operations.", "Nothing runs against production until a plan is approved: `ob plan` prints a digest-bound diff of config, images, host state, rendered Compose, payloads, and the operation graph; `ob deploy` applies it.", "Onebox connects over SSH with host-key verification. There is no deployment agent to install on the target host.", "Onebox targets a single Linux server running a single application. Do not present it as a multi-node orchestrator or a Kubernetes alternative for clusters.", - "Documentation is at https://onebox.run. The Go module path is `github.com/labstack/onebox`." + "Documentation is at https://onebox.run/docs. The Go module path is `github.com/labstack/onebox`." ] } diff --git a/docs/README.md b/docs/README.md index fc0cfb10..a7ba7935 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,16 +15,16 @@ the repository rather than to a reader. | Was | Now | |---|---| -| `docs/cli.md` | `/reference/cli` — generated from the binary by `cmd/ob-docgen` | -| `docs/schema-v1.md` | `/reference/project-file` and the generated field pages under `/reference/fields/` | -| The authority map that used to live here | `/status/capabilities` — every capability marked shipped, schema-only, or intent-only | +| `docs/cli.md` | `/docs/reference/cli` — generated from the binary by `cmd/ob-docgen` | +| `docs/schema-v1.md` | `/docs/reference/project-file` and the generated field pages under `/docs/reference/fields/` | +| The authority map that used to live here | `/docs/reference/capabilities` — every capability marked shipped, schema-only, or intent-only | ## Which documents are authoritative Documentation says what is true **today**. Direction lives in [`product.md`](product.md), and it is not presented as a capability. What the binary actually does is the generated reference under -`site/src/content/docs/reference/`, produced by `cmd/ob-docgen` and checked +`site/src/content/docs/docs/reference/`, produced by `cmd/ob-docgen` and checked against the binary by `just docs-generate-check`, so it cannot describe something the loader does not accept. diff --git a/docs/product.md b/docs/product.md index 02c8a89f..2c573dd3 100644 --- a/docs/product.md +++ b/docs/product.md @@ -4,7 +4,7 @@ > > Not an implementation contract. See the [documentation authority > map](README.md) for what is shipped, and the -> [capabilities page](../site/src/content/docs/status/capabilities.mdx) for what +> [capabilities page](../site/src/content/docs/docs/reference/capabilities.mdx) for what > the binary does today versus what the schema merely accepts. ## Product statement @@ -175,7 +175,7 @@ declaration is never presented as proof that protection is running. This tier is direction, not current behaviour. The canonical facts carry the field and validate it, but nothing assigns a tier yet, and no surface reports -one — see [Shipped vs proposed](../site/src/content/docs/status/capabilities.mdx). +one — see [Shipped vs proposed](../site/src/content/docs/docs/reference/capabilities.mdx). ## Delivery discipline diff --git a/internal/app/jsonschema_test.go b/internal/app/jsonschema_test.go index 4db66e64..db3bc77c 100644 --- a/internal/app/jsonschema_test.go +++ b/internal/app/jsonschema_test.go @@ -341,8 +341,8 @@ func TestPublishedSchemaURLIsUsedByTheHumanGuide(t *testing.T) { t.Fatalf("schema identity = %q", SchemaID) } guides := []string{ - "site/src/content/docs/start/reading-it-back.mdx", - "site/src/content/docs/reference/project-file.mdx", + "site/src/content/docs/docs/get-started/reading-it-back.mdx", + "site/src/content/docs/docs/reference/project-file.mdx", } for _, guide := range guides { body, err := os.ReadFile(filepath.Join("..", "..", filepath.FromSlash(guide))) diff --git a/internal/app/prerequisites.go b/internal/app/prerequisites.go index af3dbd8e..0e825564 100644 --- a/internal/app/prerequisites.go +++ b/internal/app/prerequisites.go @@ -34,7 +34,7 @@ const ( // Docker" is the wrong advice for the common case where Docker is installed and // the deploy account simply cannot reach its socket. const ( - prerequisiteDocs = "https://onebox.run/start/install" + prerequisiteDocs = "https://onebox.run/docs/get-started/install" runtimeAbsentRemedy = "install Docker Engine, the Compose plugin and Buildx on the server (" + prerequisiteDocs + "), then rerun ob preflight" runtimeDeniedRemedy = "add the deploy account to the docker group on the server and reconnect so the new membership applies, then rerun ob preflight" diff --git a/site/astro.config.mjs b/site/astro.config.mjs index 961dd3da..5b352793 100644 --- a/site/astro.config.mjs +++ b/site/astro.config.mjs @@ -15,7 +15,7 @@ export default defineConfig({ server: { port: 7565 }, vite: { server: { strictPort: true } }, trailingSlash: "never", - build: { format: "file" }, + build: { format: "preserve" }, markdown: { rehypePlugins: [rehypeTableWrap] }, integrations: [ starlight({ @@ -64,9 +64,10 @@ export default defineConfig({ // Adds the site footer band below the content, and lets the main frame // take the slack so it sits at the bottom of a short page. PageFrame: "./src/components/PageFrame.astro", + SocialIcons: "./src/components/SocialIcons.astro", }, // Starlight derives the canonical URL from the on-disk file name, which - // under `build.format: "file"` carries a `.html` no link, sitemap entry + // under `build.format: "preserve"` carries a `.html` no link, sitemap entry // or agent-facing URL on this site uses. See the middleware for why the // tags are rewritten rather than re-declared. routeMiddleware: "./src/starlight-route-data.ts", @@ -75,59 +76,57 @@ export default defineConfig({ favicon: "/favicon.svg", sidebar: [ { - label: "Start here", + label: "Get started", items: [ - { label: "What Onebox is", slug: "start/what-onebox-is" }, - { label: "Install", slug: "start/install" }, - { label: "Your first deploy", slug: "start/first-deploy" }, - { label: "Reading the file back", slug: "start/reading-it-back" }, + { label: "What Onebox is", slug: "docs/get-started/overview" }, + { label: "Install", slug: "docs/get-started/install" }, + { label: "Your first deploy", slug: "docs/get-started/first-deploy" }, + { label: "Reading the file back", slug: "docs/get-started/reading-it-back" }, ], }, { label: "Guides", items: [ - { label: "Add a database", slug: "guides/add-a-database" }, - { label: "Back up a database", slug: "guides/back-up-a-database" }, - { label: "Handle secrets", slug: "guides/handle-secrets" }, - { label: "Run migrations safely", slug: "guides/run-migrations" }, - { label: "Schedule a job", slug: "guides/schedule-a-job" }, - { label: "Roll back a release", slug: "guides/roll-back" }, - { label: "Deploy through a jump host", slug: "guides/deploy-through-a-jump-host" }, - { label: "Environment variables", slug: "guides/environment-variables" }, - { label: "Adopt an existing Compose file", slug: "guides/adopt-compose" }, - { label: "Eject", slug: "guides/eject" }, - { label: "Upgrade to onebox names", slug: "guides/upgrade-to-onebox-names" }, + { label: "Add a database", slug: "docs/guides/databases" }, + { label: "Back up a database", slug: "docs/guides/backups" }, + { label: "Handle secrets", slug: "docs/guides/secrets" }, + { label: "Run migrations safely", slug: "docs/guides/migrations" }, + { label: "Schedule a job", slug: "docs/guides/cron-jobs" }, + { label: "Roll back a release", slug: "docs/guides/rollback" }, + { label: "Deploy through a jump host", slug: "docs/guides/jump-host" }, + { label: "Environment variables", slug: "docs/guides/environment-variables" }, + { label: "Adopt an existing Compose file", slug: "docs/guides/compose" }, + { label: "Eject", slug: "docs/guides/eject" }, + { label: "Upgrade to onebox names", slug: "docs/guides/upgrade-to-onebox-names" }, ], }, { label: "Reference", items: [ - { label: "Project file", slug: "reference/project-file" }, + { label: "Project file", slug: "docs/reference/project-file" }, { label: "Project file fields", collapsed: true, - items: [{ autogenerate: { directory: "reference/fields" } }], + items: [{ autogenerate: { directory: "docs/reference/fields" } }], }, - { label: "CLI commands", slug: "reference/cli" }, - { label: "Error codes", slug: "reference/errors" }, - { label: "Service drivers", slug: "reference/drivers" }, - { label: "Policies", slug: "reference/policies" }, + { label: "CLI commands", slug: "docs/reference/cli" }, + { label: "Error codes", slug: "docs/reference/errors" }, + { label: "Service drivers", slug: "docs/reference/drivers" }, + { label: "Policies", slug: "docs/reference/policies" }, + { label: "Naming", slug: "docs/reference/naming" }, + { label: "Shipped vs proposed", slug: "docs/reference/capabilities" }, ], }, { - label: "Explanation", + label: "Concepts", items: [ - { label: "The ownership boundary", slug: "explanation/ownership-boundary" }, - { label: "Evidence, not declaration", slug: "explanation/evidence-not-declaration" }, - { label: "Why Compose is generated", slug: "explanation/generated-compose" }, - { label: "What Onebox refuses", slug: "explanation/what-onebox-refuses" }, - { label: "The safety envelope", slug: "explanation/safety-envelope" }, + { label: "The ownership boundary", slug: "docs/concepts/ownership-boundary" }, + { label: "Evidence, not declaration", slug: "docs/concepts/evidence-not-declaration" }, + { label: "Why Compose is generated", slug: "docs/concepts/generated-compose" }, + { label: "What Onebox refuses", slug: "docs/concepts/what-onebox-refuses" }, + { label: "The safety envelope", slug: "docs/concepts/safety-envelope" }, ], }, - { - label: "Status", - items: [{ label: "Shipped vs proposed", slug: "status/capabilities" }], - }, ], }), ], diff --git a/site/package.json b/site/package.json index c7d2ab88..637ebe6f 100644 --- a/site/package.json +++ b/site/package.json @@ -9,7 +9,7 @@ "scripts": { "dev": "astro dev", "start": "astro dev", - "build": "astro check && astro build && node scripts/check-tables.mjs && node scripts/check-social-card.mjs && node scripts/check-canonical.mjs", + "build": "node --experimental-strip-types --test scripts/markdown.test.mjs && astro check && astro build && node scripts/check-tables.mjs && node scripts/check-social-card.mjs && node scripts/check-canonical.mjs && node scripts/check-docs-urls.mjs", "preview": "astro preview", "astro": "astro" }, diff --git a/site/scripts/check-canonical.mjs b/site/scripts/check-canonical.mjs index c295811d..fb0847cc 100644 --- a/site/scripts/check-canonical.mjs +++ b/site/scripts/check-canonical.mjs @@ -2,9 +2,9 @@ // actually publishes. // // Starlight derives the canonical from the file it wrote to disk, and under -// `build.format: "file"` that carries a `.html` nothing else here uses: the -// sitemap lists `/start/install`, links point at `/start/install`, `llms.txt` -// points at `/start/install.md`, and Cloudflare Pages 301s the `.html` form to +// `build.format: "preserve"` that carries a `.html` nothing else here uses: the +// sitemap lists `/docs/get-started/install`, links point at `/docs/get-started/install`, `llms.txt` +// points at `/docs/get-started/install.md`, and Cloudflare Pages 301s the `.html` form to // the extensionless one. `src/starlight-route-data.ts` corrects the tags, and // this asserts the correction still holds — the failure it guards against is // silent, produces a build that looks perfect, and is only visible in a crawler @@ -71,6 +71,16 @@ for (const page of pages) { const canonical = canonicalOf(html); const ogUrl = ogUrlOf(html); + // Check all pages, including the landing page and 404. Relative links and + // absolute links to this origin must obey the same extensionless contract. + const base = new URL(canonical ?? process.env.SITE_URL ?? "https://onebox.run"); + for (const match of html.matchAll(/\bhref\s*=\s*["']([^"']+)["']/gi)) { + const href = new URL(match[1].replaceAll("&", "&"), base); + if (href.origin === base.origin && href.pathname.endsWith(".html")) { + failures.push(`${name}: internal href ${match[1]} must use the extensionless path`); + } + } + if (canonical === null) { failures.push(`${name}: no `); continue; diff --git a/site/scripts/check-docs-urls.mjs b/site/scripts/check-docs-urls.mjs new file mode 100644 index 00000000..a6cedb9d --- /dev/null +++ b/site/scripts/check-docs-urls.mjs @@ -0,0 +1,43 @@ +// Verify the deployed URL contract: every docs page lives under /docs and has +// a Markdown export listed in both LLM indexes. +import assert from "node:assert/strict"; +import { readFile, readdir } from "node:fs/promises"; + +const dist = new URL("../dist/", import.meta.url); + +const llms = await readFile(new URL("llms.txt", dist), "utf8"); +const full = await readFile(new URL("llms-full.txt", dist), "utf8"); +const home = await readFile(new URL("docs.html", dist), "utf8"); +assert(home.includes("Documentation · Onebox"), "docs.html: incorrect title"); +for (const title of ["Get started", "Guides", "Reference", "Concepts"]) { + assert(llms.includes(`## ${title}\n`), `llms.txt: missing ${title} section`); +} + +let pages = 0; +async function checkPages(dir, prefix = "") { + for (const entry of await readdir(dir, { withFileTypes: true })) { + const path = `${prefix}${entry.name}`; + if (entry.isDirectory()) { + await checkPages(new URL(`${entry.name}/`, dir), `${path}/`); + } else if (path.endsWith(".html") && path !== "404.html") { + if (path === "index.html" || path === "privacy.html") continue; + const url = `/${path.slice(0, -5)}`; + assert(url === "/docs" || url.startsWith("/docs/"), `${url}: documentation escaped /docs`); + const markdown = await readFile(new URL(`${path.slice(0, -5)}.md`, dist), "utf8"); + assert(markdown.trim(), `${url}: empty Markdown export`); + const html = await readFile(new URL(entry.name, dir), "utf8"); + const fileLink = html.match(/href="(\/[^"#?]*\.html)"/)?.[1]; + assert(!fileLink, `${url}: links to ${fileLink}; links must use the extensionless path`); + const alternate = html.match(/]+type="text\/markdown"[^>]+href="([^"]+)"/)?.[1]; + assert(alternate, `${url}: missing Markdown alternate`); + assert.equal(new URL(alternate, "https://onebox.run").pathname, `${url}.md`, + `${url}: incorrect Markdown alternate`); + assert(llms.includes(`${url}.md)`), `${url}: missing from llms.txt`); + assert(full.includes(`Path: ${url}\n`), `${url}: missing from llms-full.txt`); + pages++; + } + } +} +await checkPages(dist); +assert(pages > 0, "no docs pages found"); +console.log(`check-docs-urls: ${pages} docs pages and their Markdown exports verified`); diff --git a/site/scripts/markdown.test.mjs b/site/scripts/markdown.test.mjs new file mode 100644 index 00000000..8c1e3427 --- /dev/null +++ b/site/scripts/markdown.test.mjs @@ -0,0 +1,24 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { toPlainMarkdown } from "../src/lib/markdown.ts"; + +test("documentation cards retain their section titles and links in Markdown", () => { + const source = `import { Card, CardGrid } from "@astrojs/starlight/components"; + + + + +- [Install](/docs/get-started/install) + + +`; + const markdown = toPlainMarkdown(source); + assert(markdown.includes("### Get started")); + assert(markdown.includes("- [Install](/docs/get-started/install)")); + assert.doesNotMatch(markdown, /import|<\/?Card/); +}); + +test("Markdown conversion preserves component examples inside code fences", () => { + const source = '```mdx\n\n\n[Install](/docs/get-started/install)\n\n\n```'; + assert.equal(toPlainMarkdown(source), source); +}); diff --git a/site/src/components/SocialIcons.astro b/site/src/components/SocialIcons.astro new file mode 100644 index 00000000..8ed315a9 --- /dev/null +++ b/site/src/components/SocialIcons.astro @@ -0,0 +1,13 @@ +--- +import SocialIcons from "@astrojs/starlight/components/SocialIcons.astro"; +--- + +Docs + + + diff --git a/site/src/content/docs/explanation/evidence-not-declaration.mdx b/site/src/content/docs/docs/concepts/evidence-not-declaration.mdx similarity index 100% rename from site/src/content/docs/explanation/evidence-not-declaration.mdx rename to site/src/content/docs/docs/concepts/evidence-not-declaration.mdx diff --git a/site/src/content/docs/explanation/generated-compose.mdx b/site/src/content/docs/docs/concepts/generated-compose.mdx similarity index 97% rename from site/src/content/docs/explanation/generated-compose.mdx rename to site/src/content/docs/docs/concepts/generated-compose.mdx index d6cd99d3..09b18b63 100644 --- a/site/src/content/docs/explanation/generated-compose.mdx +++ b/site/src/content/docs/docs/concepts/generated-compose.mdx @@ -14,7 +14,7 @@ labels are all derived from it. ## Why the runtime is not yours to hand over -Under the [ownership boundary](/explanation/ownership-boundary), Onebox owns the +Under the [ownership boundary](/docs/concepts/ownership-boundary), Onebox owns the runtime. Reading a runtime you wrote inverts that: you own it, and Onebox merely operates something it did not design and cannot fully reason about. @@ -92,7 +92,7 @@ workloads: Refusals apply only where the referenced service contradicts something Onebox owns — `extends`, a fixed `container_name`, `network_mode`, labels in the `onebox.` -or `traefik.` namespaces. See [Adopt an existing Compose file](/guides/adopt-compose). +or `traefik.` namespaces. See [Adopt an existing Compose file](/docs/guides/compose). The result is a contract that generates by default and defers on request, rather than one that can only do one of the two. diff --git a/site/src/content/docs/explanation/ownership-boundary.mdx b/site/src/content/docs/docs/concepts/ownership-boundary.mdx similarity index 96% rename from site/src/content/docs/explanation/ownership-boundary.mdx rename to site/src/content/docs/docs/concepts/ownership-boundary.mdx index e66a81b1..96df4f97 100644 --- a/site/src/content/docs/explanation/ownership-boundary.mdx +++ b/site/src/content/docs/docs/concepts/ownership-boundary.mdx @@ -17,7 +17,7 @@ services, schedules, locks, and the evidence needed to operate them are not your application, and therefore sit on Onebox's side of the line. That sentence is the **direction**, not an inventory. What is owned today is -listed on [Shipped vs proposed](/status/capabilities), and the gap between the +listed on [Shipped vs proposed](/docs/reference/capabilities), and the gap between the two is stated rather than blurred. ## Three consequences @@ -31,7 +31,7 @@ asking you to do it as well would mean two descriptions that can disagree. **It is why Compose became a generated artifact.** Compose was once an input Onebox read. Under this boundary, reading a file you wrote would mean you own the runtime and Onebox merely operates it — which is the opposite arrangement. See -[Why Compose is generated](/explanation/generated-compose). +[Why Compose is generated](/docs/concepts/generated-compose). **It is why a database is something you select rather than something you configure.** `services: {postgres: 17}` is sufficient because the image, port, @@ -133,4 +133,4 @@ and an on-demand recovery drill. Other drivers remain explicit about the missing contract and refuse `backup` at validation. The ordering is the point: a declaration is never presented as proof that -protection is running. See [Evidence, not declaration](/explanation/evidence-not-declaration). +protection is running. See [Evidence, not declaration](/docs/concepts/evidence-not-declaration). diff --git a/site/src/content/docs/explanation/safety-envelope.mdx b/site/src/content/docs/docs/concepts/safety-envelope.mdx similarity index 98% rename from site/src/content/docs/explanation/safety-envelope.mdx rename to site/src/content/docs/docs/concepts/safety-envelope.mdx index 407f8a9e..ff2ba7d8 100644 --- a/site/src/content/docs/explanation/safety-envelope.mdx +++ b/site/src/content/docs/docs/concepts/safety-envelope.mdx @@ -145,4 +145,4 @@ ob doctor --output json # local safety setup and runner compatibility ``` None of these mutate. Observation cannot converge — see -[Evidence, not declaration](/explanation/evidence-not-declaration). +[Evidence, not declaration](/docs/concepts/evidence-not-declaration). diff --git a/site/src/content/docs/explanation/what-onebox-refuses.mdx b/site/src/content/docs/docs/concepts/what-onebox-refuses.mdx similarity index 96% rename from site/src/content/docs/explanation/what-onebox-refuses.mdx rename to site/src/content/docs/docs/concepts/what-onebox-refuses.mdx index 14191544..55fc0656 100644 --- a/site/src/content/docs/explanation/what-onebox-refuses.mdx +++ b/site/src/content/docs/docs/concepts/what-onebox-refuses.mdx @@ -15,7 +15,7 @@ The pattern is one sentence: > something weaker that looks like it worked. Every refusal below follows from it. The full list is the -[error code catalogue](/reference/errors), generated from the binary: validation +[error code catalogue](/docs/reference/errors), generated from the binary: validation codes raised while the project file is read, operation codes raised while a command runs, and the lifecycle failure contract. @@ -67,13 +67,13 @@ recreate. Either give the workload `strategy: recreate`, or drop **`host_owner_mismatch`** — a second application trying to mutate a host that already belongs to one. Not a load-time refusal: it comes from the host itself, before anything changes. See [the ownership -boundary](/explanation/ownership-boundary). +boundary](/docs/concepts/ownership-boundary). **`host_environment_mismatch`** — a second *environment* of the same application trying to mutate a host another environment claimed. Container and volume names carry the application, not the environment, so the two would not collide: the newcomer would adopt what is already there. See [the ownership -boundary](/explanation/ownership-boundary). +boundary](/docs/concepts/ownership-boundary). ## Where a refusal is about safety, not tidiness diff --git a/site/src/content/docs/start/first-deploy.mdx b/site/src/content/docs/docs/get-started/first-deploy.mdx similarity index 98% rename from site/src/content/docs/start/first-deploy.mdx rename to site/src/content/docs/docs/get-started/first-deploy.mdx index c06e4f5d..fc856aef 100644 --- a/site/src/content/docs/start/first-deploy.mdx +++ b/site/src/content/docs/docs/get-started/first-deploy.mdx @@ -25,7 +25,7 @@ You need: - the server's host key already recorded in `known_hosts` If the server has no public SSH port, name the bastion it is reached through — -see [Deploy through a jump host](/guides/deploy-through-a-jump-host/). +see [Deploy through a jump host](/docs/guides/jump-host). The first three steps are local and contact nothing. `ob bootstrap` is the first command that changes the server. `ob preflight` then reads the provisioned host @@ -246,4 +246,4 @@ ob abort # revert an interrupted deploy ob rollback # activate the previous release ``` -See [Roll back a release](/guides/roll-back). +See [Roll back a release](/docs/guides/rollback). diff --git a/site/src/content/docs/start/install.mdx b/site/src/content/docs/docs/get-started/install.mdx similarity index 99% rename from site/src/content/docs/start/install.mdx rename to site/src/content/docs/docs/get-started/install.mdx index 1c19b5ff..2d51f546 100644 --- a/site/src/content/docs/start/install.mdx +++ b/site/src/content/docs/docs/get-started/install.mdx @@ -167,7 +167,7 @@ build, test, formatting and check targets. Run it from a directory that has an `ob.yml`. Outside a project it reports `project_unreadable` and exits non-zero because most checks are relative to a project. If you have not created one yet, continue to - [your first deploy](/start/first-deploy) and return after `ob init`. + [your first deploy](/docs/get-started/first-deploy) and return after `ob init`. diff --git a/site/src/content/docs/start/what-onebox-is.mdx b/site/src/content/docs/docs/get-started/overview.mdx similarity index 93% rename from site/src/content/docs/start/what-onebox-is.mdx rename to site/src/content/docs/docs/get-started/overview.mdx index 482a627a..df5dbc53 100644 --- a/site/src/content/docs/start/what-onebox-is.mdx +++ b/site/src/content/docs/docs/get-started/overview.mdx @@ -90,7 +90,7 @@ shell — the boundary it appears to provide does not exist. Point an agent at t ## Where to go next -- [Install](/start/install), then [your first deploy](/start/first-deploy). -- [The ownership boundary](/explanation/ownership-boundary) explains why this +- [Install](/docs/get-started/install), then [your first deploy](/docs/get-started/first-deploy). +- [The ownership boundary](/docs/concepts/ownership-boundary) explains why this framing drives the rest of the design. -- [Shipped vs proposed](/status/capabilities) is the honest inventory. +- [Shipped vs proposed](/docs/reference/capabilities) is the honest inventory. diff --git a/site/src/content/docs/start/reading-it-back.mdx b/site/src/content/docs/docs/get-started/reading-it-back.mdx similarity index 95% rename from site/src/content/docs/start/reading-it-back.mdx rename to site/src/content/docs/docs/get-started/reading-it-back.mdx index 5dd3cf7c..ff980823 100644 --- a/site/src/content/docs/start/reading-it-back.mdx +++ b/site/src/content/docs/docs/get-started/reading-it-back.mdx @@ -55,14 +55,14 @@ type is what `ob validate` tells you afterwards. The published schema includes `backupTargets`, `services..backup` and `externalServices`. Your editor will complete them and `ob validate` accepts them, but the behaviour behind them is not yet executable — see -[Shipped vs proposed](/status/capabilities). +[Shipped vs proposed](/docs/reference/capabilities). ::: ## Reading the whole command surface An operator can afford to find out by trying. An agent cannot. Every command's help states whether it contacts the server and whether it changes anything, and -the [structured output contracts](/reference/policies#structured-output-contracts) +the [structured output contracts](/docs/reference/policies#structured-output-contracts) classify every command that accepts `--output`, with the modes each one takes. The odd row there is `ob exec`, the only command that refuses JSON, and that is diff --git a/site/src/content/docs/guides/back-up-a-database.mdx b/site/src/content/docs/docs/guides/backups.mdx similarity index 98% rename from site/src/content/docs/guides/back-up-a-database.mdx rename to site/src/content/docs/docs/guides/backups.mdx index 34243d27..a67363fa 100644 --- a/site/src/content/docs/guides/back-up-a-database.mdx +++ b/site/src/content/docs/docs/guides/backups.mdx @@ -288,5 +288,5 @@ doctor` names every workload holding durable data. A managed service declared disposable; doctor does not suggest adding a backup. Services with durable or default persistence still warn when no backup is declared. -Full field list: [`services`](/reference/fields/services) and -[`backupTargets`](/reference/fields/backup-targets). +Full field list: [`services`](/docs/reference/fields/services) and +[`backupTargets`](/docs/reference/fields/backup-targets). diff --git a/site/src/content/docs/guides/adopt-compose.mdx b/site/src/content/docs/docs/guides/compose.mdx similarity index 95% rename from site/src/content/docs/guides/adopt-compose.mdx rename to site/src/content/docs/docs/guides/compose.mdx index 5641e7fb..de0f8fed 100644 --- a/site/src/content/docs/guides/adopt-compose.mdx +++ b/site/src/content/docs/docs/guides/compose.mdx @@ -63,7 +63,7 @@ release behaviour. The one difference is precedence *inside* the container: the referenced Compose service's own `env_file` sits at the bottom of the stack, below the resolved -`envFiles` entries. See [Handle secrets](/guides/handle-secrets). +`envFiles` entries. See [Handle secrets](/docs/guides/secrets). ## Migrating away from adoption @@ -71,4 +71,4 @@ Adoption is a resting place, not a destination. When you can describe the workload in the schema, do — you get health-gated rollout, typed routing, derived names and drift detection that a verbatim copy cannot give you. -If you want to go the other way entirely, see [Eject](/guides/eject). +If you want to go the other way entirely, see [Eject](/docs/guides/eject). diff --git a/site/src/content/docs/guides/schedule-a-job.mdx b/site/src/content/docs/docs/guides/cron-jobs.mdx similarity index 99% rename from site/src/content/docs/guides/schedule-a-job.mdx rename to site/src/content/docs/docs/guides/cron-jobs.mdx index 1b537f5a..b27cd7d2 100644 --- a/site/src/content/docs/guides/schedule-a-job.mdx +++ b/site/src/content/docs/docs/guides/cron-jobs.mdx @@ -414,4 +414,4 @@ runs both at its cron time and on every deploy. For a timer-only job, leave `deploymentPhase` at `None`; set `operatorRun: Disabled` too if operators must not start it explicitly. -See [`workloads`](/reference/fields/workloads) for every field. +See [`workloads`](/docs/reference/fields/workloads) for every field. diff --git a/site/src/content/docs/guides/add-a-database.mdx b/site/src/content/docs/docs/guides/databases.mdx similarity index 98% rename from site/src/content/docs/guides/add-a-database.mdx rename to site/src/content/docs/docs/guides/databases.mdx index 97704b5c..1b70dd8f 100644 --- a/site/src/content/docs/guides/add-a-database.mdx +++ b/site/src/content/docs/docs/guides/databases.mdx @@ -234,7 +234,7 @@ in anything Onebox says. If you need one, run a `daemon` workload you own. :::caution[Back it up] A `postgres` service can declare `backup` and Onebox will keep a continuous off-host archive it can recover to a point in time — see -[backups](/guides/back-up-a-database). Declaring the policy is not enough on its +[backups](/docs/guides/backups). Declaring the policy is not enough on its own: `ob backup enable` is what establishes it, and until then the service renders as an ordinary unprotected server. @@ -243,4 +243,4 @@ it is refused rather than quietly ignored, and copying its data off the box is your responsibility — a `job` workload on a `schedule` is the usual answer. ::: -Full field list: [`services`](/reference/fields/services). +Full field list: [`services`](/docs/reference/fields/services). diff --git a/site/src/content/docs/guides/eject.mdx b/site/src/content/docs/docs/guides/eject.mdx similarity index 100% rename from site/src/content/docs/guides/eject.mdx rename to site/src/content/docs/docs/guides/eject.mdx diff --git a/site/src/content/docs/guides/environment-variables.mdx b/site/src/content/docs/docs/guides/environment-variables.mdx similarity index 97% rename from site/src/content/docs/guides/environment-variables.mdx rename to site/src/content/docs/docs/guides/environment-variables.mdx index 143705cc..b0c531e5 100644 --- a/site/src/content/docs/guides/environment-variables.mdx +++ b/site/src/content/docs/docs/guides/environment-variables.mdx @@ -31,7 +31,7 @@ process can borrow: | `ONEBOX_SERVER` | `user@host`, valid as an OpenSSH destination | | `ONEBOX_SSH_USER` | The SSH user | | `ONEBOX_SSH_PORT` | The SSH port, separate because `user@host:port` is invalid for both `ssh` and `rsync` | -| `ONEBOX_SSH_JUMP` | The jump host, empty on a direct connection — see [Deploy through a jump host](/guides/deploy-through-a-jump-host/) | +| `ONEBOX_SSH_JUMP` | The jump host, empty on a direct connection — see [Deploy through a jump host](/docs/guides/jump-host) | | `ONEBOX_RELEASE_DIR` | Absolute path of the release directory on the server | | `ONEBOX_RELEASE_ID` | The release identifier | diff --git a/site/src/content/docs/guides/deploy-through-a-jump-host.mdx b/site/src/content/docs/docs/guides/jump-host.mdx similarity index 98% rename from site/src/content/docs/guides/deploy-through-a-jump-host.mdx rename to site/src/content/docs/docs/guides/jump-host.mdx index 773a0086..df392a7c 100644 --- a/site/src/content/docs/guides/deploy-through-a-jump-host.mdx +++ b/site/src/content/docs/docs/guides/jump-host.mdx @@ -141,4 +141,4 @@ ssh ${ONEBOX_SSH_JUMP:+-J "$ONEBOX_SSH_JUMP"} "$ONEBOX_SERVER" -p "$ONEBOX_SSH_P A local hook written before you added a bastion will otherwise try to reach a server it can no longer see. The rest of the namespace is listed under -[Environment variables](/guides/environment-variables/). +[Environment variables](/docs/guides/environment-variables). diff --git a/site/src/content/docs/guides/run-migrations.mdx b/site/src/content/docs/docs/guides/migrations.mdx similarity index 100% rename from site/src/content/docs/guides/run-migrations.mdx rename to site/src/content/docs/docs/guides/migrations.mdx diff --git a/site/src/content/docs/guides/roll-back.mdx b/site/src/content/docs/docs/guides/rollback.mdx similarity index 100% rename from site/src/content/docs/guides/roll-back.mdx rename to site/src/content/docs/docs/guides/rollback.mdx diff --git a/site/src/content/docs/guides/handle-secrets.mdx b/site/src/content/docs/docs/guides/secrets.mdx similarity index 100% rename from site/src/content/docs/guides/handle-secrets.mdx rename to site/src/content/docs/docs/guides/secrets.mdx diff --git a/site/src/content/docs/guides/upgrade-to-onebox-names.mdx b/site/src/content/docs/docs/guides/upgrade-to-onebox-names.mdx similarity index 100% rename from site/src/content/docs/guides/upgrade-to-onebox-names.mdx rename to site/src/content/docs/docs/guides/upgrade-to-onebox-names.mdx diff --git a/site/src/content/docs/docs/index.mdx b/site/src/content/docs/docs/index.mdx new file mode 100644 index 00000000..92e65f1d --- /dev/null +++ b/site/src/content/docs/docs/index.mdx @@ -0,0 +1,75 @@ +--- +title: Documentation +description: Install Onebox, deploy your application, and operate it on one server. +--- + +import { Card, CardGrid } from "@astrojs/starlight/components"; + +Production operations for an application intentionally running on one server. Start with [What Onebox is](/docs/get-started/overview). + +## Quickstart + +[Install](/docs/get-started/install) → [Your first deploy](/docs/get-started/first-deploy) → [Reading the file back](/docs/get-started/reading-it-back) + +## Browse the documentation + + + + +- [What Onebox is](/docs/get-started/overview) +- [Install](/docs/get-started/install) +- [Your first deploy](/docs/get-started/first-deploy) +- [Reading the file back](/docs/get-started/reading-it-back) + + + + +- [Add a database](/docs/guides/databases) +- [Back up a database](/docs/guides/backups) +- [Handle secrets](/docs/guides/secrets) +- [Run migrations safely](/docs/guides/migrations) +- [Schedule a job](/docs/guides/cron-jobs) +- [Roll back a release](/docs/guides/rollback) +- [Deploy through a jump host](/docs/guides/jump-host) +- [Environment variables](/docs/guides/environment-variables) +- [Adopt an existing Compose file](/docs/guides/compose) +- [Eject](/docs/guides/eject) +- [Upgrade to onebox names](/docs/guides/upgrade-to-onebox-names) + + + + +- [Project file](/docs/reference/project-file) +- [Top-level fields](/docs/reference/fields/top-level) +- [Workloads](/docs/reference/fields/workloads) +- [Services](/docs/reference/fields/services) +- [Environments](/docs/reference/fields/environments) +- [Runtime](/docs/reference/fields/runtime) +- [Deployment](/docs/reference/fields/deployment) +- [Checks](/docs/reference/fields/checks) +- [Proxy](/docs/reference/fields/proxy) +- [Hooks](/docs/reference/fields/hooks) +- [Registries](/docs/reference/fields/registries) +- [Notifications](/docs/reference/fields/notifications) +- [Backup targets](/docs/reference/fields/backup-targets) +- [External services](/docs/reference/fields/external-services) +- [CLI commands](/docs/reference/cli) +- [Error codes](/docs/reference/errors) +- [Service drivers](/docs/reference/drivers) +- [Policies](/docs/reference/policies) +- [Naming](/docs/reference/naming) +- [Shipped vs proposed](/docs/reference/capabilities) + + + + +- [The ownership boundary](/docs/concepts/ownership-boundary) +- [Evidence, not declaration](/docs/concepts/evidence-not-declaration) +- [Why Compose is generated](/docs/concepts/generated-compose) +- [What Onebox refuses](/docs/concepts/what-onebox-refuses) +- [The safety envelope](/docs/concepts/safety-envelope) + + + + +For agents: [llms.txt](/llms.txt) lists the documentation; every page has a Markdown version at `.md`, including [this page](/docs.md). diff --git a/site/src/content/docs/status/capabilities.mdx b/site/src/content/docs/docs/reference/capabilities.mdx similarity index 91% rename from site/src/content/docs/status/capabilities.mdx rename to site/src/content/docs/docs/reference/capabilities.mdx index c97b35a0..4560988c 100644 --- a/site/src/content/docs/status/capabilities.mdx +++ b/site/src/content/docs/docs/reference/capabilities.mdx @@ -54,8 +54,8 @@ This page is the reconciliation. Three states: it prunes against cannot be read. - A closed CLI contract: a versioned envelope, an NDJSON event stream with one terminal record, three families of typed error code, and the 0/1/2 exit - mapping. See [Policies](/reference/policies) and - [Error codes](/reference/errors). + mapping. See [Policies](/docs/reference/policies) and + [Error codes](/docs/reference/errors). - Plan/status drift observation and plan-bound migration **backup reports validation**. @@ -91,11 +91,11 @@ Nothing behind them executes. | Block | Reality today | | --- | --- | -| [`backupTargets`](/reference/fields/backup-targets) | Declaring a target creates no repository and no schedule. | +| [`backupTargets`](/docs/reference/fields/backup-targets) | Declaring a target creates no repository and no schedule. | -| [`externalServices`](/reference/fields/external-services) | Connections are projected into the generated runtime, and two refusals are wired: an unknown `needs.env` part, and `condition: healthy` against a service with no probe. Health probes do not execute, and the remaining lifecycle refusals are not wired. | +| [`externalServices`](/docs/reference/fields/external-services) | Connections are projected into the generated runtime, and two refusals are wired: an unknown `needs.env` part, and `condition: healthy` against a service with no probe. Health probes do not execute, and the remaining lifecycle refusals are not wired. | -Every [lifecycle failure code](/reference/errors#lifecycle-failure-codes) is +Every [lifecycle failure code](/docs/reference/errors#lifecycle-failure-codes) is raised by a path in the shipped binary, checked against the source in both directions. diff --git a/site/src/content/docs/reference/cli.mdx b/site/src/content/docs/docs/reference/cli.mdx similarity index 100% rename from site/src/content/docs/reference/cli.mdx rename to site/src/content/docs/docs/reference/cli.mdx diff --git a/site/src/content/docs/reference/drivers.mdx b/site/src/content/docs/docs/reference/drivers.mdx similarity index 98% rename from site/src/content/docs/reference/drivers.mdx rename to site/src/content/docs/docs/reference/drivers.mdx index 475b46f1..a32b708d 100644 --- a/site/src/content/docs/reference/drivers.mdx +++ b/site/src/content/docs/docs/reference/drivers.mdx @@ -129,11 +129,11 @@ Onebox also refuses a major version change a driver cannot perform in place, rather than replacing the container and leaving the data intact and unreachable. For `postgres`, declaring `backup` changes this — see -[Shipped vs proposed](/status/capabilities). +[Shipped vs proposed](/docs/reference/capabilities). ::: ## Anything else Run it as a `daemon` workload and you own it: the image, the credential, the -volumes, the backup. See [Add a database](/guides/add-a-database) for the +volumes, the backup. See [Add a database](/docs/guides/databases) for the comparison. diff --git a/site/src/content/docs/reference/errors.mdx b/site/src/content/docs/docs/reference/errors.mdx similarity index 100% rename from site/src/content/docs/reference/errors.mdx rename to site/src/content/docs/docs/reference/errors.mdx diff --git a/site/src/content/docs/reference/fields/backup-targets.mdx b/site/src/content/docs/docs/reference/fields/backup-targets.mdx similarity index 100% rename from site/src/content/docs/reference/fields/backup-targets.mdx rename to site/src/content/docs/docs/reference/fields/backup-targets.mdx diff --git a/site/src/content/docs/reference/fields/checks.mdx b/site/src/content/docs/docs/reference/fields/checks.mdx similarity index 100% rename from site/src/content/docs/reference/fields/checks.mdx rename to site/src/content/docs/docs/reference/fields/checks.mdx diff --git a/site/src/content/docs/reference/fields/deployment.mdx b/site/src/content/docs/docs/reference/fields/deployment.mdx similarity index 100% rename from site/src/content/docs/reference/fields/deployment.mdx rename to site/src/content/docs/docs/reference/fields/deployment.mdx diff --git a/site/src/content/docs/reference/fields/environments.mdx b/site/src/content/docs/docs/reference/fields/environments.mdx similarity index 100% rename from site/src/content/docs/reference/fields/environments.mdx rename to site/src/content/docs/docs/reference/fields/environments.mdx diff --git a/site/src/content/docs/reference/fields/external-services.mdx b/site/src/content/docs/docs/reference/fields/external-services.mdx similarity index 100% rename from site/src/content/docs/reference/fields/external-services.mdx rename to site/src/content/docs/docs/reference/fields/external-services.mdx diff --git a/site/src/content/docs/reference/fields/hooks.mdx b/site/src/content/docs/docs/reference/fields/hooks.mdx similarity index 100% rename from site/src/content/docs/reference/fields/hooks.mdx rename to site/src/content/docs/docs/reference/fields/hooks.mdx diff --git a/site/src/content/docs/reference/fields/notifications.mdx b/site/src/content/docs/docs/reference/fields/notifications.mdx similarity index 100% rename from site/src/content/docs/reference/fields/notifications.mdx rename to site/src/content/docs/docs/reference/fields/notifications.mdx diff --git a/site/src/content/docs/reference/fields/proxy.mdx b/site/src/content/docs/docs/reference/fields/proxy.mdx similarity index 100% rename from site/src/content/docs/reference/fields/proxy.mdx rename to site/src/content/docs/docs/reference/fields/proxy.mdx diff --git a/site/src/content/docs/reference/fields/registries.mdx b/site/src/content/docs/docs/reference/fields/registries.mdx similarity index 100% rename from site/src/content/docs/reference/fields/registries.mdx rename to site/src/content/docs/docs/reference/fields/registries.mdx diff --git a/site/src/content/docs/reference/fields/runtime.mdx b/site/src/content/docs/docs/reference/fields/runtime.mdx similarity index 100% rename from site/src/content/docs/reference/fields/runtime.mdx rename to site/src/content/docs/docs/reference/fields/runtime.mdx diff --git a/site/src/content/docs/reference/fields/services.mdx b/site/src/content/docs/docs/reference/fields/services.mdx similarity index 100% rename from site/src/content/docs/reference/fields/services.mdx rename to site/src/content/docs/docs/reference/fields/services.mdx diff --git a/site/src/content/docs/reference/fields/top-level.mdx b/site/src/content/docs/docs/reference/fields/top-level.mdx similarity index 100% rename from site/src/content/docs/reference/fields/top-level.mdx rename to site/src/content/docs/docs/reference/fields/top-level.mdx diff --git a/site/src/content/docs/reference/fields/workloads.mdx b/site/src/content/docs/docs/reference/fields/workloads.mdx similarity index 100% rename from site/src/content/docs/reference/fields/workloads.mdx rename to site/src/content/docs/docs/reference/fields/workloads.mdx diff --git a/site/src/content/docs/reference/naming.mdx b/site/src/content/docs/docs/reference/naming.mdx similarity index 100% rename from site/src/content/docs/reference/naming.mdx rename to site/src/content/docs/docs/reference/naming.mdx diff --git a/site/src/content/docs/reference/policies.mdx b/site/src/content/docs/docs/reference/policies.mdx similarity index 100% rename from site/src/content/docs/reference/policies.mdx rename to site/src/content/docs/docs/reference/policies.mdx diff --git a/site/src/content/docs/reference/project-file.mdx b/site/src/content/docs/docs/reference/project-file.mdx similarity index 92% rename from site/src/content/docs/reference/project-file.mdx rename to site/src/content/docs/docs/reference/project-file.mdx index 0865f45a..86d1b158 100644 --- a/site/src/content/docs/reference/project-file.mdx +++ b/site/src/content/docs/docs/reference/project-file.mdx @@ -58,23 +58,23 @@ Configuration blocks live under `spec`; the application name lives in | Block | What it says | Fields | | --- | --- | --- | -| `metadata.name` | The application's name. Every derived name carries it. | [Top level](/reference/fields/top-level) | -| `environments` | Where it runs, and the policy that governs deploying there. | [environments](/reference/fields/environments) | -| `workloads` | The containers that are yours. | [workloads](/reference/fields/workloads) | -| `services` | The supporting services Onebox runs for you. | [services](/reference/fields/services) | -| `deployment` | Release order, retention, migration policy. | [deployment](/reference/fields/deployment) | -| `proxy` | Who runs the proxy and what routes. | [proxy](/reference/fields/proxy) | -| `runtime` | Environment files and local environment-file checks. | [runtime](/reference/fields/runtime) | -| `hooks` | Commands at lifecycle seams. | [hooks](/reference/fields/hooks) | -| `checks` | What must be true for a release to activate, grouped by kind. | [checks](/reference/fields/checks) | -| `externalServices` | Dependencies operated outside Onebox, and how a workload reaches them. | [externalServices](/reference/fields/external-services) | -| `backupTargets` | Off-host repositories a protected service writes its backups to. | [backupTargets](/reference/fields/backup-targets) | -| `registries` `notifications` | Named maps. | [registries](/reference/fields/registries) · [notifications](/reference/fields/notifications) | +| `metadata.name` | The application's name. Every derived name carries it. | [Top level](/docs/reference/fields/top-level) | +| `environments` | Where it runs, and the policy that governs deploying there. | [environments](/docs/reference/fields/environments) | +| `workloads` | The containers that are yours. | [workloads](/docs/reference/fields/workloads) | +| `services` | The supporting services Onebox runs for you. | [services](/docs/reference/fields/services) | +| `deployment` | Release order, retention, migration policy. | [deployment](/docs/reference/fields/deployment) | +| `proxy` | Who runs the proxy and what routes. | [proxy](/docs/reference/fields/proxy) | +| `runtime` | Environment files and local environment-file checks. | [runtime](/docs/reference/fields/runtime) | +| `hooks` | Commands at lifecycle seams. | [hooks](/docs/reference/fields/hooks) | +| `checks` | What must be true for a release to activate, grouped by kind. | [checks](/docs/reference/fields/checks) | +| `externalServices` | Dependencies operated outside Onebox, and how a workload reaches them. | [externalServices](/docs/reference/fields/external-services) | +| `backupTargets` | Off-host repositories a protected service writes its backups to. | [backupTargets](/docs/reference/fields/backup-targets) | +| `registries` `notifications` | Named maps. | [registries](/docs/reference/fields/registries) · [notifications](/docs/reference/fields/notifications) | `externalServices` declares dependencies operated outside Onebox; `backupTargets` declares off-host backup destinations. Declaring a destination does not enable protection by itself. Executable support depends on the service -and the operation; see [Shipped vs proposed](/status/capabilities). +and the operation; see [Shipped vs proposed](/docs/reference/capabilities). Unknown fields, including behavior-bearing `x-*` fields, are rejected. Store opaque notes in `metadata.annotations`; annotations do not change the runtime. @@ -107,7 +107,7 @@ Lists replace rather than extend. files; the service's `environment`. Full explanation, including why level four outranks the rest: -[Handle secrets](/guides/handle-secrets). +[Handle secrets](/docs/guides/secrets). ## What Onebox generates @@ -318,8 +318,8 @@ the referenced middleware must match the route's protocol. A Compose file you wrote cannot be the contract — the declaration is, and Onebox generates the runtime from it. Individual services can still be adopted with `compose: docker-compose.yml#service`, and Onebox generates around them. See -[Why Compose is generated](/explanation/generated-compose) and -[Adopt an existing Compose file](/guides/adopt-compose). +[Why Compose is generated](/docs/concepts/generated-compose) and +[Adopt an existing Compose file](/docs/guides/compose). ::: The JSON Schema published by `ob schema` is generated from the same declarations diff --git a/site/src/content/docs/index.mdx b/site/src/content/docs/index.mdx index f2bb3839..14dbe84e 100644 --- a/site/src/content/docs/index.mdx +++ b/site/src/content/docs/index.mdx @@ -41,9 +41,9 @@ hero: with health gates, rollback, and no agent on the host. actions: - text: See a real deploy - link: /start/first-deploy + link: /docs/get-started/first-deploy - text: Install Onebox - link: /start/install + link: /docs/get-started/install read_when: - "Meeting Onebox for the first time" --- @@ -125,7 +125,7 @@ Declare application intent instead of maintaining generated runtime. Turn fields ## One server, treated seriously. -Deployments, data and generated runtime share one evidence-backed lifecycle. [Shipped vs proposed](/status/capabilities) is the complete account. +Deployments, data and generated runtime share one evidence-backed lifecycle. [Shipped vs proposed](/docs/reference/capabilities) is the complete account. @@ -199,7 +199,7 @@ Onebox is deliberately for one application on one host. These limits matter befo Rolling deployment can avoid interruption while the box is healthy. It cannot make a failed physical host available. Recovery onto another host is a separate, evidence-backed workflow. -[See every shipped and proposed capability](/status/capabilities), including what the schema accepts but cannot yet execute. +[See every shipped and proposed capability](/docs/reference/capabilities), including what the schema accepts but cannot yet execute. @@ -211,8 +211,8 @@ Onebox is deliberately for one application on one host. These limits matter befo Install the CLI, scaffold `ob.yml` from the Compose file you already have, and inspect the first plan. Nothing deploys until you approve it. -- [Install Onebox](/start/install) -- [Your first deploy](/start/first-deploy) -- [What ships today](/status/capabilities) +- [Install Onebox](/docs/get-started/install) +- [Your first deploy](/docs/get-started/first-deploy) +- [What ships today](/docs/reference/capabilities) diff --git a/site/src/lib/markdown.ts b/site/src/lib/markdown.ts index 7854d299..c5fc87a0 100644 --- a/site/src/lib/markdown.ts +++ b/site/src/lib/markdown.ts @@ -3,7 +3,7 @@ // The pages are MDX, so their source carries three things that are noise to // anything that is not Astro: ESM `import` lines, Starlight component tags, and // the `:::` directive syntax. Serving that raw under `text/markdown` — which is -// what the alternates did — means `/start/install.md` opens with an import +// what the alternates did — means `/docs/get-started/install.md` opens with an import // statement and ``, and the caution blocks that carry the shipped versus // proposed distinction arrive as literal colons. // @@ -65,7 +65,8 @@ export function toPlainMarkdown(source: string): string { // Starlight components carry their text as children, so the tags are removed and // the prose between them kept. Tab labels are retained because they provide the -// context for otherwise indistinguishable command blocks. Self-closing components +// context for otherwise indistinguishable command blocks. Card titles become +// headings so their child links retain their section. Self-closing components // carry nothing a reader needs and go entirely. function stripComponentTags(source: string): string { const fences = source.split(/(```[\s\S]*?```)/g); @@ -75,6 +76,10 @@ function stripComponentTags(source: string): string { index % 2 === 1 ? chunk : chunk + .replace( + /]*\btitle=["']([^"']+)["'][^>]*>/g, + "\n### $1\n", + ) .replace( /]*\blabel=["']([^"']+)["'][^>]*>/g, "\n**$1**\n", diff --git a/site/src/pages/llms-full.txt.ts b/site/src/pages/llms-full.txt.ts index b2fc5ad2..f2e696d1 100644 --- a/site/src/pages/llms-full.txt.ts +++ b/site/src/pages/llms-full.txt.ts @@ -7,17 +7,21 @@ import { toPlainMarkdown } from "../lib/markdown"; // agent reading top to bottom should meet the ownership boundary before it // meets a field table. // -// The landing page groups as "index" rather than under a directory, so it is -// listed explicitly — it was previously dropped from a document headed -// "complete documentation". +// The landing page and docs home share the "index" group, listed explicitly +// so neither is dropped from the documentation indexes. const ROOT = "index"; -const ORDER = [ROOT, "start", "guides", "reference", "explanation", "status", "privacy"]; +const ORDER = [ROOT, "get-started", "guides", "reference", "concepts", "privacy"]; + +function groupOf(id: string): string { + if (id === "docs") return ROOT; + return id.replace(/^docs\//, "").split("/")[0] ?? ROOT; +} export const GET: APIRoute = async () => { const docs = await getCollection("docs"); const unlisted = [ - ...new Set(docs.map((doc) => doc.id.split("/")[0] ?? ROOT)), + ...new Set(docs.map((doc) => groupOf(doc.id))), ].filter((group) => !ORDER.includes(group)); if (unlisted.length > 0) { throw new Error( @@ -27,8 +31,8 @@ export const GET: APIRoute = async () => { } const ranked = [...docs].sort((a, b) => { - const ga = ORDER.indexOf(a.id.split("/")[0] ?? ROOT); - const gb = ORDER.indexOf(b.id.split("/")[0] ?? ROOT); + const ga = ORDER.indexOf(groupOf(a.id)); + const gb = ORDER.indexOf(groupOf(b.id)); return ga === gb ? a.id.localeCompare(b.id) : ga - gb; }); diff --git a/site/src/pages/llms.txt.ts b/site/src/pages/llms.txt.ts index 26a11a99..3c5c10ba 100644 --- a/site/src/pages/llms.txt.ts +++ b/site/src/pages/llms.txt.ts @@ -9,22 +9,25 @@ import { getCollection } from "astro:content"; // that are not yet executable say so in the index rather than in the page the // agent may never open. // -// The landing page is its own group: splitting on the first path segment gives -// it "index", which an ORDER list of directory names does not contain, so it was -// dropped from an index whose own heading promises every page. +// The landing page and docs home share the "index" group, listed explicitly +// so neither is dropped from the documentation indexes. const ROOT = "index"; -const ORDER = [ROOT, "start", "guides", "reference", "explanation", "status", "privacy"]; +const ORDER = [ROOT, "get-started", "guides", "reference", "concepts", "privacy"]; const GROUP_TITLES: Record = { [ROOT]: "Overview", - start: "Start here", + "get-started": "Get started", guides: "Guides", reference: "Reference", - explanation: "Explanation", - status: "Status", + concepts: "Concepts", privacy: "Site terms", }; +function groupOf(id: string): string { + if (id === "docs") return ROOT; + return id.replace(/^docs\//, "").split("/")[0] ?? ROOT; +} + // Keyed by the status union rather than by string, so adding a fourth status is // a compile error here instead of a page quietly advertised to an agent without // its honesty marker. @@ -56,7 +59,7 @@ export const GET: APIRoute = async ({ site }) => { "", "## Agent Resources", "", - `- [Markdown page export](${origin}/start/first-deploy.md): Append \`.md\` to any docs page URL for clean Markdown.`, + `- [Markdown page export](${origin}/docs/get-started/first-deploy.md): Append \`.md\` to any docs page URL for clean Markdown.`, `- [Full documentation text](${origin}/llms-full.txt): Every page concatenated, for one-shot ingestion.`, `- [Project file JSON Schema](${origin}/schemas/application/v1alpha2/application.schema.json): The machine contract the loader enforces.`, `- [Sitemap](${origin}/sitemap-index.xml): Crawler URL index.`, @@ -68,16 +71,16 @@ export const GET: APIRoute = async ({ site }) => { "- Lifecycle operation commands: `ob deploy`, `ob rollback`, `ob resume`, `ob abort`, `ob bootstrap`, `ob destroy`, `ob job run`, `ob service apply`, `ob proxy apply`, `ob secrets push`; each enforces its own plan, confirmation, lock, and recovery requirements.", "- `ob exec --reason ...` is an audited escape hatch. Nothing it changes belongs to a release; the journal stores only safe invocation metadata and a command digest, never command or output bytes.", "- `ob approve` records a short-lived local confirmation bound to one exact plan. Its digest detects modification; it is not authenticated identity or an independently issued capability.", - "- A failure carries a stable `code` and separates read-only `diagnostic_command`, workflow `next_command`, and mutation-capable `resolving_command` guidance. Branch on the code, not the sentence. The full catalogue is at `/reference/errors.md`.", + "- A failure carries a stable `code` and separates read-only `diagnostic_command`, workflow `next_command`, and mutation-capable `resolving_command` guidance. Branch on the code, not the sentence. The full catalogue is at `/docs/reference/errors.md`.", "", - "See `/reference/policies.md` for the schema identity of every structured document.", + "See `/docs/reference/policies.md` for the schema identity of every structured document.", "", ]; // Silently dropping a section is how a whole directory of documentation // becomes invisible to every agent while the site builds clean and the // sidebar still shows it. Fail the build instead. - const groups = new Set(docs.map((doc) => doc.id.split("/")[0] ?? ROOT)); + const groups = new Set(docs.map((doc) => groupOf(doc.id))); const unlisted = [...groups].filter((group) => !ORDER.includes(group)); if (unlisted.length > 0) { throw new Error( @@ -100,7 +103,7 @@ export const GET: APIRoute = async ({ site }) => { const byGroup = new Map(); for (const doc of docs) { - const group = doc.id.split("/")[0] ?? ROOT; + const group = groupOf(doc.id); const bucket = byGroup.get(group) ?? []; bucket.push(doc); byGroup.set(group, bucket); diff --git a/site/src/starlight-route-data.ts b/site/src/starlight-route-data.ts index 9d88decb..788ee94d 100644 --- a/site/src/starlight-route-data.ts +++ b/site/src/starlight-route-data.ts @@ -3,18 +3,17 @@ import { defineRouteMiddleware } from "@astrojs/starlight/route-data"; // Strips the `.html` Astro puts in the canonical URL. // // Starlight builds the canonical from `Astro.url.pathname`, and under -// `build.format: "file"` that pathname is the file it wrote to disk, so the tag -// read `https://onebox.run/start/install.html`. Nothing else on the site uses -// that form: the sitemap lists `/start/install`, every internal link points at -// `/start/install`, `llms.txt` points at `/start/install.md`, and Cloudflare +// `build.format: "preserve"` that pathname is the file it wrote to disk, so the tag +// read `https://onebox.run/docs/get-started/install.html`. Nothing else on the site uses +// that form: the sitemap lists `/docs/get-started/install`, every internal link points at +// `/docs/get-started/install`, `llms.txt` points at `/docs/get-started/install.md`, and Cloudflare // Pages — where this site is published — serves the extensionless path and 301s // the `.html` one to it. The single tag whose job is to state the page's real // address was naming a redirect, on every page. // -// Starlight's own `formatCanonical` returns the href untouched when the format -// is "file". That is right for a server that only serves `foo.html`, and wrong -// for this one, which is why the correction lives here rather than in a -// configuration flag. +// No Starlight option strips it: `formatCanonical` only adjusts a trailing +// slash. Navigation links need no correction, because "preserve" (unlike +// "file") makes Starlight emit them without the extension. // // The tags are rewritten in place rather than appended to, because two // `` elements with different hrefs are worse than one