Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,29 @@ Assume every commit and every line of history will be public.
an encyclopedia of alternatives.
- Do not add navigation to a section until that section has a real page or
generated source.
- The sidebar in `blume.config.ts` is the public route manifest.
`bun run verify:routes` checks every sidebar route against the built output.
- The routes written as string literals in the `blume.config.ts` sidebar are
the public route manifest. `bun run verify:routes` checks every one against
the built output, and `scripts/build-llms-index.mjs` keeps exactly those
entries in `llms.txt`. Both read the config source between `sidebar:` and
`openapi:`, so the generated sections — API operations, CLI commands — are
computed above the config object and spread into the sidebar. Writing one of
those routes as a literal inside the sidebar would claim a manifest entry it
cannot keep, and break both.
- Header tabs are the top-level sections: Platform (`/`), Zero, CLI, API, and
Platform API. A tab's `path` is a URL prefix, and the sidebar group rooted at
that same path is the section the tab scopes the sidebar to — a new tab means
both, or it renders an empty sidebar. Frozen generated prefixes are what make
tabs work here: they are already stable section roots, so no route moves.
- Reference navigation is derived, never hand-listed. The API sections read the
OpenAPI snapshot through Blume's own extractor, and the CLI section reads the
command headings out of the generated CLI page, so an endpoint or command
that ships appears in the sidebar without an edit here.
- Route policy: one page per task, not one page per toggle; a page lives in one
primary place and is cross-linked elsewhere. Authored URLs may move freely
before launch, without compatibility redirects. Generated routes are frozen
public contracts and can never move: `/api/reference`,
`/platforms/api/reference`, `/cli`, `/errors` (RFC 9457 error type URIs point
at `/errors/<code>`), and `/changelog`. Because those prefixes can never be
unified, the site uses a single sidebar with no tabs.
at `/errors/<code>`), and `/changelog`.
- Test behavior and built output. Do not read implementation source and assert
that a string exists as a substitute for exercising the result.

Expand Down
154 changes: 142 additions & 12 deletions blume.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { readFileSync } from "node:fs";

import { defineConfig } from "blume";
import { componentSlug } from "blume/components/slug.ts";
import { extractOperations } from "blume/openapi/model.ts";

const deploymentBase = "/docs";
const haskoy = {
Expand All @@ -12,6 +16,89 @@ const haskoy = {
],
};

/**
* Sidebar groups for one OpenAPI spec: a collapsible group per tag, holding
* that tag's operation pages in spec order.
*
* Blume generates those pages and their routes from the spec, so the sidebar
* reads the same spec through Blume's own extractor rather than a hand-kept
* list that drifts every time an endpoint lands.
*/
const referenceGroups = (spec: string, route: string) => {
const { operations, tags } = extractOperations(
JSON.parse(readFileSync(spec, "utf8")),
route
);
return tags.map((tag) => ({
collapsed: true,
display: "group" as const,
items: operations
.filter((operation) => operation.tagSlug === tag.slug)
.map((operation) => operation.route),
label: tag.name,
}));
};

/**
* Sidebar entries for the CLI: one group per top-level command, holding that
* command's subcommands.
*
* The CLI reference is a single generated page with a heading per command, so
* the entries link to heading anchors and are read back out of that page —
* a command that ships is in the sidebar without anyone editing this file.
* Anchors are slugged the way Blume slugs headings, so they always agree.
*/
const cliCommands = () => {
let page: string;
try {
page = readFileSync("./content/cli/index.md", "utf8");
} catch {
// The page is an ignored build overlay, so a bare `blume` run without the
// prepare step would otherwise fail here as a bare ENOENT.
throw new Error(
"content/cli/index.md is missing. Run scripts/prepare-generated.mjs before Blume; the CLI sidebar is built from that page."
);
}
const commands = [...page.matchAll(/^## `sf ([^`]+)`$/gmu)].map((heading) => ({
href: `/cli#${componentSlug(`sf ${heading[1]}`)}`,
// Argument placeholders (`BUILD`, `[PATH]`) name the call, not the
// command, so they are dropped from the label and the grouping.
words: heading[1].split(/\s+/u).filter((word) => !/^[A-Z[]/u.test(word)),
}));

const groups = new Map<string, typeof commands>();
for (const command of commands) {
const name = command.words[0];
groups.set(name, [...(groups.get(name) ?? []), command]);
}

return [...groups].map(([name, entries]) => {
const [first] = entries;
if (entries.length === 1) {
return { href: first.href, label: `sf ${first.words.join(" ")}` };
}
return {
collapsed: true,
display: "group" as const,
items: entries.map((entry) => ({
href: entry.href,
label:
entry.words.length === 1 ? "Overview" : entry.words.slice(1).join(" "),
})),
label: `sf ${name}`,
};
});
};

const restReference = referenceGroups(
"./generated/openapi/api.json",
"/api/reference"
);
const platformReference = referenceGroups(
"./generated/openapi/platform.json",
"/platforms/api/reference"
);

export default defineConfig({
title: "Spacefast Docs",
description: "Publish sites and build apps with Spacefast through the CLI, API, SDK, and MCP.",
Expand Down Expand Up @@ -69,6 +156,17 @@ export default defineConfig({
{ label: "Agent setup", href: "/agents", icon: "bot" },
],
repo: true,
// Header sections. A tab's `path` is a URL prefix, and the sidebar group
// rooted at that prefix is the section it scopes to — so reading the CLI
// reference shows CLI navigation, not the whole site. The `/` tab spans
// everything the others don't claim.
tabs: [
{ label: "Platform", path: "/" },
{ label: "Zero", path: "/zero-runtime" },
{ label: "CLI", path: "/cli" },
{ label: "API", path: "/api" },
{ label: "Platform API", path: "/platforms" },
],
sidebar: {
display: "flat",
items: [
Expand Down Expand Up @@ -114,9 +212,8 @@ export default defineConfig({
},
{
label: "Primitives",
root: "/zero-runtime",
root: "/functions",
items: [
"/zero-runtime",
"/functions",
"/functions/php",
"/database",
Expand Down Expand Up @@ -172,11 +269,6 @@ export default defineConfig({
"/guides/local-development",
],
},
{
label: "Platforms",
root: "/platforms",
items: ["/platforms"],
},
{
label: "Account & teams",
root: "/account",
Expand All @@ -192,17 +284,55 @@ export default defineConfig({
root: "/reference",
items: [
"/reference",
"/api",
"/api/versioning",
{ label: "REST API reference", href: "/api/reference" },
{ label: "Platform API reference", href: "/platforms/api/reference" },
"/cli",
"/reference/sdk",
"/errors",
{ label: "Changelog", href: "/changelog" },
"/changelog/packages",
],
},
// Header-tab sections. Each is rooted at the matching tab's path, which
// is what scopes the sidebar to it.
{
label: "Zero",
root: "/zero-runtime",
items: [
"/zero-runtime",
"/zero-runtime/build",
"/zero-runtime/client",
"/zero-runtime/server",
"/zero-runtime/endpoints",
"/zero-runtime/authentication",
"/zero-runtime/styling",
"/zero-runtime/components",
"/zero-runtime/commands",
"/zero-runtime/managed-content",
"/zero-runtime/move-from-lakebed",
],
},
{
label: "CLI",
root: "/cli",
items: ["/cli", ...cliCommands()],
},
{
label: "API",
root: "/api",
items: [
"/api",
"/api/versioning",
{ label: "All endpoints", href: "/api/reference" },
...restReference,
],
},
{
label: "Platform API",
root: "/platforms",
items: [
"/platforms",
{ label: "All endpoints", href: "/platforms/api/reference" },
...platformReference,
],
},
],
},
},
Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion content/database/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ In a Functions worker, opt in through `sf.jsonc`:
The worker then receives `env.DB`. Use `prepare().bind()` with `.all()`,
`.first()`, `.run()`, or `.raw()`. `env.DB.exec()` runs DDL (data
definition language) statements, and `env.DB.batch()` groups statements.
For the typed query contract, see [Zero's database](/zero-runtime#database).
For a complete typed example, [build a Zero to-do app](/zero-runtime/build).

## Migrations

Expand Down
10 changes: 5 additions & 5 deletions content/functions/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ request. Next and OpenNext builds get `nodejs_compat` automatically.

Outbound network access is fail-closed. Spacefast denies it unless the
published version holds the `fetch` capability, so a `fetch()` call in your
code does not silently add network authority. No Functions publish
grants that capability. Outbound `fetch` is available only to
[Zero](/zero-runtime) actions. To check which capabilities a published version
holds, run `sf runtime status`.
code does not add network authority. No Functions publish grants that
capability. Zero server handlers cannot call `fetch` either. Use a Zero
[platform service](/services) when one matches the job. To inspect a published
Functions version, run `sf runtime status`.

## Variables

Expand All @@ -99,7 +99,7 @@ instead.
## Storage

Every worker receives `env.STORAGE`, the same space
[object store](/storage) that [Zero](/zero-runtime#storage) uses. The binding has
[object store](/storage) that Zero uses. The binding has
three methods:

- `upload(file)` takes a `Blob` and returns an object with `id`,
Expand Down
2 changes: 1 addition & 1 deletion content/functions/php.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ suffix removed. A trailing `index` segment also drops:
Routes are literal. A segment that is empty, contains `[` or `]`, or starts
with `:` or `.` is not routed in this lane, so a pattern-named file such
as `functions/[id].php` stays an inert upload. A file you commit at the
same key outranks the PHP route, and redirects and [Zero](/zero-runtime) actions
same key outranks the PHP route, and redirects and [Zero](/zero-runtime) endpoints
win over it. This is the same precedence the
[JavaScript Functions](/functions) lane gets.

Expand Down
4 changes: 1 addition & 3 deletions content/guides/local-development.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,7 @@ sf dev --state-backend sqlite
```

The local store is a development engine, not the database a published space gets. It is enough
to build against, and two differences are worth holding on to. A failed `ctx.transaction()`
does not roll back locally, so writes made inside it survive an error that would undo them in
production. And the local engine has its own ceilings on rows, values, and stored objects,
to build against. The local engine has its own ceilings on rows, values, and stored objects,
which are development guard rails rather than product limits. For those, see
[Plans](/account/plans).

Expand Down
7 changes: 5 additions & 2 deletions content/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ Claim the space afterward to keep it.
Spaces are private by default. Grant access to people, links,
passwords, or the public, and collect comments on previews.
</Card>
<Card title="Primitives" href="/zero-runtime" icon="server">
Zero, Functions, and the space's database, storage, and email.
<Card title="Zero" href="/zero-runtime" icon="server">
Build a full app on a space: server code, endpoints, auth, and styling.
</Card>
<Card title="Primitives" href="/functions" icon="boxes">
Functions and the space's own database, storage, and email.
</Card>
<Card title="Domains" href="/domains" icon="globe-lock">
Attach your own hostname. DNS checks and SSL are automatic.
Expand Down
Loading
Loading