Skip to content
Open
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
2 changes: 1 addition & 1 deletion .cloudcannon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The documentation is organized into clear sections so you can find what you need
* **User Glossary** (`user/glossary/`) — Definitions of key terms used across the platform.
* **Developer Articles** (`developer/articles/`) — In-depth explanations for developers integrating with CloudCannon.
* **Developer Guides** (`developer/guides/`) — Hands-on walkthroughs for developer workflows.
* **Developer Reference** (`developer/reference/`) — Technical reference material for configuration and APIs.
* **Developer Reference** (`developer/reference/`) — Technical reference material, split into Site configuration (config files you add to your repo) and Platform (the CLI, SDK, API, and Permissions).
* **Changelog** (`changelogs/`) — Release notes organized by year.

Content follows a modified [Diátaxis framework](https://diataxis.fr/) — separating explanations, instructions, guides, and reference material so readers always land in the right place.
Expand Down
49 changes: 49 additions & 0 deletions .cloudcannon/download-openapi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Downloads the CloudCannon OpenAPI spec for the API reference docs.
//
// Mirrors download-permissions.js, with one difference: if the download fails
// or returns an unhealthy document, we keep the committed _data/openapi.json
// (the cached copy) and continue the build rather than failing it.
//
// Override the source with OPENAPI_URL, e.g. for staging or dev:
// staging: https://staging-app.cloudcannon.io/api/v0/openapi.json
// dev: https://dev-app.cloudcannon.com/api/v0/openapi.json

const filepath = "_data/openapi.json";
const specUrl = Deno.env.get("OPENAPI_URL") ??
"https://app.cloudcannon.com/api/v0/openapi.json";

const useCached = (reason) => {
console.warn(`${reason}`);
console.warn(`Falling back to the cached spec at ${filepath}.`);
};

const pullSpec = async () => {
try {
const req = await fetch(specUrl);
if (!req.ok) {
useCached(`OpenAPI spec at ${specUrl} returned ${req.status}.`);
return;
}

const spec = await req.json();

// Check for a healthy OpenAPI document before overwriting the cache.
if (!spec?.openapi || !spec?.paths || !Object.keys(spec.paths).length) {
useCached(
`OpenAPI spec provided by CloudCannon at ${specUrl} has changed or errored ` +
`(expected "openapi" and a non-empty "paths").`,
);
return;
}

Deno.writeTextFileSync(filepath, JSON.stringify(spec, null, 2));
console.log(
`Downloaded OpenAPI spec from ${specUrl} -> ${filepath} ` +
`(${Object.keys(spec.paths).length} paths).`,
);
} catch (e) {
useCached(`Failed to download OpenAPI spec from ${specUrl}: ${e}`);
}
};

pullSpec();
1 change: 1 addition & 0 deletions .cloudcannon/prebuild
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ fi
./.cloudcannon/generate-versions > _data/systemversions.json
deno run -R -W ./.cloudcannon/tidy-versions.js
deno run -R -W --allow-net ./.cloudcannon/download-permissions.js
deno run -R -W --allow-net --allow-env ./.cloudcannon/download-openapi.js
deno run -R -W ./.cloudcannon/copy-docshot-source.js
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The documentation is organized into clear sections so you can find what you need
* **User Glossary** (`user/glossary/`) — Definitions of key terms used across the platform.
* **Developer Articles** (`developer/articles/`) — In-depth explanations for developers integrating with CloudCannon.
* **Developer Guides** (`developer/guides/`) — Hands-on walkthroughs for developer workflows.
* **Developer Reference** (`developer/reference/`) — Technical reference material for configuration and APIs.
* **Developer Reference** (`developer/reference/`) — Technical reference material, split into Site configuration (config files you add to your repo) and Platform (the CLI, SDK, API, and Permissions).
* **Changelog** (`changelogs/`) — Release notes organized by year.

Content follows a modified [Diátaxis framework](https://diataxis.fr/) — separating explanations, instructions, guides, and reference material so readers always land in the right place.
Expand Down
212 changes: 212 additions & 0 deletions _components/Api/ApiOperation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import type {
OperationView,
ParameterView,
SchemaTypeRef,
} from "../../developer/reference/api/_shared/openapi.ts";
import type { Comp, Helpers } from "../../_types.d.ts";

const MAX_ENUM_VALUES = 30;

// Renders a response/request body as a link to its named schema page.
function SchemaReference({ reference }: { reference: SchemaTypeRef }) {
return (
<p className="c-api-schema-ref">
{reference.isArray && "Array of "}
<a href={reference.url}>
<code className="code-no-box">{reference.name}</code>
</a>
</p>
);
}

// Renders a labelled group of parameters as a dt/dd pair, matching the
// configuration reference's c-data-reference layout.
function Parameters(
{ title, params, helpers }: {
title: string;
params: ParameterView[];
helpers?: Helpers;
},
) {
if (!params.length) return null;
return (
<>
<dt>{title}:</dt>
<dd className="c-data-reference">
{params.map((param) => {
const enumValues = (param.enumValues ?? []).slice(0, MAX_ENUM_VALUES);
const enumMore = (param.enumValues?.length ?? 0) - enumValues.length;
return (
<div className="c-data-reference__item" key={`${param.in}-${param.name}`}>
<div className="c-data-reference__header">
<span className="c-data-reference__key">
<code className="code-no-box">{param.name}</code>
</span>
<code>{param.typeLabel}</code>
{param.required && <small className="pill pill--red">Required</small>}
</div>
<div className="c-data-reference__description">
{param.description && (
helpers
? (
<div
dangerouslySetInnerHTML={{
__html: helpers.md(param.description),
}}
/>
)
: <div>{param.description}</div>
)}
{param.defaultValue !== undefined && (
<p>
<em>Defaults to:</em> <code>{param.defaultValue}</code>
</p>
)}
{enumValues.length > 0 && (
<p>
<em>Allowed values:</em>{" "}
{enumValues.map((val, i) => (
<span key={i}>
{i > 0 && " "}
<code>{val}</code>
</span>
))}
{enumMore > 0 && ` and ${enumMore} more.`}
</p>
)}
</div>
</div>
);
})}
</dd>
</>
);
}

interface ApiOperationProps {
key?: string | number;
operation: OperationView;
helpers?: Helpers;
comp: Comp;
}

export default function ApiOperation(
{ operation, helpers, comp }: ApiOperationProps,
) {
const hasResponses = operation.responses.length > 0;

return (
<section className="c-api-operation" id={operation.id}>
<h2 className="c-api-operation__title exclude-from-toc">
{operation.title}
</h2>
<div className="c-api-operation__signature">
<comp.Api.HttpMethod method={operation.method} />
<code className="c-api-operation__path">{operation.path}</code>
</div>

{operation.deprecated && (
<p>
<small className="pill pill--warn">Deprecated</small>
</p>
)}

{operation.description && (
<div className="c-api-operation__description">
{helpers
? (
<div
dangerouslySetInnerHTML={{
__html: helpers.md(operation.description),
}}
/>
)
: <div>{operation.description}</div>}
</div>
)}

<dl>
<Parameters
title="Path parameters"
params={operation.pathParams}
helpers={helpers}
/>
<Parameters
title="Filters"
params={operation.filterParams}
helpers={helpers}
/>
<Parameters
title="Sorting"
params={operation.sortParams}
helpers={helpers}
/>
<Parameters
title="Pagination"
params={operation.paginationParams}
helpers={helpers}
/>
<Parameters
title="Query parameters"
params={operation.queryParams}
helpers={helpers}
/>
<Parameters
title="Header parameters"
params={operation.headerParams}
helpers={helpers}
/>

{(operation.requestSchemaRef || operation.requestRows.length > 0) && (
<>
<dt>Request body:</dt>
<dd>
{operation.requestSchemaRef
? <SchemaReference reference={operation.requestSchemaRef} />
: (
<comp.Api.ApiSchema
rows={operation.requestRows}
helpers={helpers}
/>
)}
</dd>
</>
)}

<dt>Example request:</dt>
<dd>
<comp.CodeBlock language="bash" source="Terminal">
<pre><code className="language-bash">{operation.curl}</code></pre>
</comp.CodeBlock>
</dd>

{hasResponses && (
<>
<dt>Responses:</dt>
<dd>
{operation.responses.map((response) => (
<div className="c-api-response" key={response.status}>
<div className="c-api-response__header">
<span className="c-api-response__status">
{response.status}
</span>
{response.description && (
<span className="c-api-response__description">
{response.description}
</span>
)}
</div>
{response.schemaRef
? <SchemaReference reference={response.schemaRef} />
: response.rows.length > 0 && (
<comp.Api.ApiSchema rows={response.rows} helpers={helpers} />
)}
</div>
))}
</dd>
</>
)}
</dl>
</section>
);
}
27 changes: 27 additions & 0 deletions _components/Api/ApiResource.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { ApiResource as ApiResourceData } from "../../developer/reference/api/_shared/openapi.ts";
import type { Comp, Helpers } from "../../_types.d.ts";

interface ApiResourceProps {
resource: ApiResourceData;
helpers?: Helpers;
comp: Comp;
}

export default function ApiResource(
{ resource, helpers, comp }: ApiResourceProps,
) {
return (
<div className="c-api-resource">
{resource.description && (
<p className="c-api-resource__description">{resource.description}</p>
)}
{resource.operations.map((operation) => (
<comp.Api.ApiOperation
key={operation.id}
operation={operation}
helpers={helpers}
/>
))}
</div>
);
}
34 changes: 34 additions & 0 deletions _components/Api/ApiResourceIndex.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {
API_BASE_PATH,
getApiResources,
} from "../../developer/reference/api/_shared/openapi.ts";
import type { Comp, Helpers } from "../../_types.d.ts";

interface ApiResourceIndexProps {
comp: Comp;
helpers?: Helpers;
}

// Lists every API resource group as a card, for the API landing page.
// Built from the OpenAPI spec, so it stays in sync as endpoints change.
export default function ApiResourceIndex(
{ comp, helpers }: ApiResourceIndexProps,
) {
const resources = getApiResources();
return (
<div className="c-card-container--related">
{resources.map((resource) => {
const count = resource.operations.length;
return (
<comp.Card.Card
key={resource.slug}
href={`${API_BASE_PATH}${resource.slug}/`}
title={resource.title}
description={`${count} ${count === 1 ? "endpoint" : "endpoints"}`}
helpers={helpers}
/>
);
})}
</div>
);
}
Loading
Loading