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
24 changes: 24 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,29 @@ jobs:
name: Validate llms.txt
command: yarn validate-llms-txt

check-error-docs:
executor:
name: default
steps:
- checkout
- run:
name: Fetch error code registry (ably-common submodule)
command: git submodule update --init ably-common
- run:
name: Regenerate error code pages from the registry
command: node bin/generate-error-pages.ts
- run:
name: Ensure committed error pages match the registry
command: |
CHANGES="$(git status --porcelain src/pages/docs/platform/errors/codes src/pages/docs/platform/errors/codes.mdx)"
if [ -n "$CHANGES" ]; then
echo "❌ Generated error code pages are out of date or were hand-edited:"
echo "$CHANGES"
echo "Run 'yarn generate:errors' and commit the result."
exit 1
fi
echo "✓ Error code pages are up to date"

test-nginx:
docker:
- image: heroku/heroku:24-build
Expand Down Expand Up @@ -174,6 +197,7 @@ workflows:
unless: << pipeline.parameters.content-update >>
jobs:
- install-dependencies
- check-error-docs
- security-audit:
requires:
- install-dependencies
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "ably-common"]
path = ably-common
url = https://github.com/ably/ably-common.git
1 change: 1 addition & 0 deletions ably-common
Submodule ably-common added at fcc793
160 changes: 160 additions & 0 deletions bin/generate-error-pages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import fs from 'fs';
import path from 'path';

// Generates the docs pages for Ably error codes from the ably-common registry
// (the pinned `ably-common` git submodule). Each `ably-common/errors/codes/<code>.md`
// becomes a docs page at `/docs/platform/errors/codes/<code>`, plus an index at
// `/docs/platform/errors/codes`.
//
// The generated pages are committed to the repo, so the site build consumes them
// without needing the submodule checked out. Run `yarn generate:errors` after
// bumping the submodule; CI regenerates and fails on any diff, so the committed
// output can't drift from the registry or be hand-edited (see .circleci/config.yml).

const REGISTRY_DIR = path.resolve('ably-common/errors/codes');
const DETAIL_OUT_DIR = path.resolve('src/pages/docs/platform/errors/codes');
const INDEX_OUT = path.resolve('src/pages/docs/platform/errors/codes.mdx');
const DETAIL_BASE_URL = '/docs/platform/errors/codes';

interface ErrorEntry {
code: number;
identifier: string;
title: string;
summary: string;
body: string;
}

// Mirrors ably-common's `errors/scripts/frontmatter.js`: a `---` fence, then one
// `key: value` per line (split on the first `: ` only, so values may contain
// colons), optional surrounding double quotes, then a closing `---`. We parse it
// the same way rather than with a strict YAML parser, which rejects the colons
// that appear in some summaries.
const parseFrontmatter = (content: string): { fields: Record<string, string>; body: string } | null => {
if (!content.startsWith('---\n')) {
return null;
}
const rest = content.slice(4);
const end = rest.indexOf('\n---');
if (end === -1) {
return null;
}
const body = rest.slice(end + 4).replace(/^\n+/, '');
const fields: Record<string, string> = {};

for (const raw of rest.slice(0, end).split('\n')) {
const line = raw.trimEnd();
if (line === '') {
continue;
}
const sep = line.indexOf(': ');
const key = sep === -1 ? line.replace(/:$/, '') : line.slice(0, sep);
let value = sep === -1 ? '' : line.slice(sep + 2);
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1).replace(/\\"/g, '"');
}
fields[key.trim()] = value;
}

return { fields, body };
};

// Frontmatter values are single-line; a double-quoted YAML scalar with `\` and
// `"` escaped is safe for the titles, summaries, identifiers and URLs we emit.
const yamlQuote = (value: string): string => `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;

// An MDX comment (`{/* … */}`, not an HTML `<!-- -->` comment, which MDX rejects)
// that renders to nothing. Marks the file as generated so it isn't hand-edited.
const generatedBanner = (source: string): string =>
`{/* AUTOGENERATED — DO NOT EDIT. Generated from ${source} by bin/generate-error-pages.ts; run \`yarn generate:errors\` to update. */}`;

const readRegistry = (): ErrorEntry[] => {
const entries: ErrorEntry[] = [];

for (const file of fs.readdirSync(REGISTRY_DIR)) {
if (!file.endsWith('.md')) {
continue;
}
const parsed = parseFrontmatter(fs.readFileSync(path.join(REGISTRY_DIR, file), 'utf8'));
if (!parsed) {
console.warn(`[error-docs] skipping ${file}: could not parse frontmatter`);
continue;
}
const { identifier, title, summary } = parsed.fields;
const code = Number(parsed.fields.code);

if (!Number.isFinite(code) || !identifier || !title || !summary) {
console.warn(`[error-docs] skipping ${file}: missing required frontmatter (code, identifier, title, summary)`);
continue;
}
entries.push({ code, identifier, title, summary, body: parsed.body.trim() });
}

return entries.sort((a, b) => a.code - b.code);
};

const writeDetailPage = (entry: ErrorEntry): void => {
const frontmatter = [
'---',
`title: ${yamlQuote(`${entry.code}: ${entry.title}`)}`,
`meta_description: ${yamlQuote(entry.summary)}`,
`identifier: ${yamlQuote(entry.identifier)}`,
'---',
'',
].join('\n');
const banner = generatedBanner(`ably-common/errors/codes/${entry.code}.md`);
// The summary is the error's primary description, so it leads the body as
// normal prose (not the muted PageHeader `intro`). Any detail body follows.
const content = [entry.summary, entry.body].filter(Boolean).join('\n\n');
fs.writeFileSync(path.join(DETAIL_OUT_DIR, `${entry.code}.mdx`), `${frontmatter}\n${banner}\n\n${content}\n`);
};

const writeIndexPage = (entries: ErrorEntry[]): void => {
const frontmatter = [
'---',
`title: ${yamlQuote('Error codes')}`,
`meta_description: ${yamlQuote('Understand Ably error codes and their causes, to resolve them efficiently.')}`,
'redirect_from:',
` - ${yamlQuote('/docs/errors/codes')}`,
'---',
'',
].join('\n');
const intro =
'\nEvery Ably error code, its identifier, and a short description. ' +
'Select a code for details on what it means, why it happens, and how to resolve it.\n\n';
// One section per code: a linked `code: title` heading (with a numeric anchor
// so old `…/codes#<code>` deep links still resolve), then the identifier and
// the summary.
const sections = entries
.map(
(e) =>
`## [${e.code}: ${e.title}](${DETAIL_BASE_URL}/${e.code}) <a id="${e.code}"/>\n\n` +
`\`${e.identifier}\`\n\n` +
`${e.summary}\n`,
)
.join('\n');

const banner = generatedBanner('the ably-common errors registry');
fs.writeFileSync(INDEX_OUT, `${frontmatter}\n${banner}\n${intro}${sections}`);
};

const generate = (): void => {
if (!fs.existsSync(REGISTRY_DIR)) {
console.error(
`[error-docs] registry not found at ${REGISTRY_DIR}. Run \`git submodule update --init ably-common\` first.`,
);
process.exit(1);
}

// Rebuild the output directory from scratch so codes removed from the registry
// don't leave stale pages behind (a stale deletion then shows up in the CI diff).
fs.rmSync(DETAIL_OUT_DIR, { recursive: true, force: true });
fs.mkdirSync(DETAIL_OUT_DIR, { recursive: true });

const entries = readRegistry();
entries.forEach(writeDetailPage);
writeIndexPage(entries);

console.log(`[error-docs] generated ${entries.length} error code pages and the index`);
};

generate();
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"lint-staged": "lint-staged",
"repo-githooks": "git config core.hooksPath .githooks",
"no-githooks": "git config --unset core.hooksPath",
"validate-llms-txt": "node bin/validate-llms.txt.ts"
"validate-llms-txt": "node bin/validate-llms.txt.ts",
"generate:errors": "node bin/generate-error-pages.ts"
},
"dependencies": {
"@ably/ui": "18.3.1",
Expand Down
11 changes: 11 additions & 0 deletions src/components/Layout/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,17 @@ const Footer: React.FC<{ pageContext: PageContextType }> = ({ pageContext }) =>
let path = '#';
const pathName = location.pathname.replace('docs/', '');

// Error-code pages are generated from the ably-common registry, not authored
// in this repo. Point edits at the source file there; hide the link for the
// generated aggregate index (no single source file).
const errorCodeMatch = pathName.match(/^\/platform\/errors\/codes\/(\d+)\/?$/);
if (errorCodeMatch) {
return `https://github.com/ably/ably-common/blob/main/errors/codes/${errorCodeMatch[1]}.md`;
}
if (pathName === '/platform/errors/codes' || pathName === '/platform/errors/codes/') {
return null;
}

if (customGithubPaths[pathName]) {
path = customGithubPaths[pathName];
} else if (activePage.template === 'mdx') {
Expand Down
3 changes: 3 additions & 0 deletions src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export type Frontmatter = {
last_updated?: string;
intro?: string;
json_ld?: Record<string, unknown>;
// Error-code pages (generated from the ably-common registry) carry the stable
// snake_case identifier, rendered as a soft sub-title beneath the title.
identifier?: string;
};

export type PageContextType = {
Expand Down
5 changes: 4 additions & 1 deletion src/components/Layout/MDXWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ const MDXWrapper: React.FC<MDXWrapperProps> = ({ children, pageContext, location
const description = getFrontmatter(frontmatter, 'meta_description', META_DESCRIPTION_FALLBACK) as string;
const intro = getFrontmatter(frontmatter, 'intro') as string;
const keywords = getFrontmatter(frontmatter, 'meta_keywords') as string;
// Error-code pages carry a stable snake_case identifier, shown as a soft
// sub-title beneath the title.
const identifier = getFrontmatter(frontmatter, 'identifier') as string;
const metaTitle = getMetaTitle(title, (activePage.product as ProductName) || META_PRODUCT_FALLBACK) as string;

const { canonicalUrl } = useSiteMetadata();
Expand Down Expand Up @@ -362,7 +365,7 @@ const MDXWrapper: React.FC<MDXWrapperProps> = ({ children, pageContext, location
RequiredBadge,
}}
>
<PageHeader title={title} intro={intro} />
<PageHeader title={title} intro={intro} subtitle={identifier ? <code>{identifier}</code> : undefined} />
{children}
</MarkdownProvider>
</NestedTableProvider>
Expand Down
8 changes: 6 additions & 2 deletions src/components/Layout/mdx/PageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import cn from 'src/utilities/cn';
type PageHeaderProps = {
title: string;
intro: string;
subtitle?: React.ReactNode;
};

export const PageHeader: React.FC<PageHeaderProps> = ({ title, intro }) => {
export const PageHeader: React.FC<PageHeaderProps> = ({ title, intro, subtitle }) => {
return (
<div className="my-8 border-b border-neutral-300 dark:border-neutral-1000 pb-8">
<h1 className={cn('ui-text-h1', intro ? 'mb-4' : 'mb-0')}>{title}</h1>
<h1 className={cn('ui-text-h1', intro || subtitle ? 'mb-4' : 'mb-0')}>{title}</h1>
{subtitle && (
<div className={cn('text-neutral-800 dark:text-neutral-500', intro ? 'mb-4' : 'mb-0')}>{subtitle}</div>
)}
{intro && (
<p className="text-neutral-800 dark:text-neutral-500 font-serif italic tracking-tight text-lg leading-normal mb-0">
{intro}
Expand Down
9 changes: 8 additions & 1 deletion src/contexts/layout-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,14 @@ export const LayoutProvider: React.FC<PropsWithChildren<{ pageContext: PageConte
const location = useLocation();

const activePage = useMemo(() => {
const activePageData = determineActivePage(productData, location.pathname);
const activePageData =
determineActivePage(productData, location.pathname) ??
// Error-code detail pages (/docs/platform/errors/codes/<code>) are generated
// from the ably-common registry and not individually in the nav; resolve them
// to the Error codes index so they inherit the Platform sidebar.
(/^\/docs\/platform\/errors\/codes\/[^/]+\/?$/.test(location.pathname)
? determineActivePage(productData, '/docs/platform/errors/codes')
: null);

let languages: LanguageKey[] = [];
if (activePageData?.page.languages) {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/api/realtime-sdk/types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ An `ErrorInfo` is a type encapsulating error information containing an Ably-spec

ErrorInfo objects can contain nested errors through the `cause` property, allowing you to trace the root cause of failures. When an operation fails due to underlying system errors, the main `ErrorInfo` provides the high-level failure reason while the nested `cause` contains more specific details about what went wrong.

One example of ErrorInfo nesting is [80019: Auth server rejecting request](/docs/platform/errors/codes#80019) where the main error indicates token renewal failed, while the nested `cause` contains the specific HTTP error from the auth server.
One example of ErrorInfo nesting is [80019: Auth server rejecting request](/docs/platform/errors/codes/80019) where the main error indicates token renewal failed, while the nested `cause` contains the specific HTTP error from the auth server.

The following example demonstrates how to handle nested errors:

Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/api/rest-sdk/types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ An `ErrorInfo` is a type encapsulating error information containing an Ably-spec

ErrorInfo objects can contain nested errors through the `cause` property, allowing you to trace the root cause of failures. When an operation fails due to underlying system errors, the main `ErrorInfo` provides the high-level failure reason while the nested `cause` contains more specific details about what went wrong.

One example of ErrorInfo nesting is [80019: Auth server rejecting request](/docs/platform/errors/codes#80019) where the main error indicates token renewal failed, while the nested `cause` contains the specific HTTP error from the auth server.
One example of ErrorInfo nesting is [80019: Auth server rejecting request](/docs/platform/errors/codes/80019) where the main error indicates token renewal failed, while the nested `cause` contains the specific HTTP error from the auth server.

The following example demonstrates how to handle nested errors:

Expand Down
4 changes: 2 additions & 2 deletions src/pages/docs/auth/token/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Ably enforces maximum TTL (time-to-live) limits:
- Revocable tokens: Maximum TTL of 1 hour. A token is revocable if [token revocation](/docs/auth/revocation) has been enabled for the API key used to issue it.

<Aside data-type="important">
Attempting to create a token with a TTL that exceeds these limits will result in an error code [40003](/docs/platform/errors/codes#40003).
Attempting to create a token with a TTL that exceeds these limits will result in an error code [40003](/docs/platform/errors/codes/40003).
</Aside>

## Dynamic channel access control <a id="dynamic-access"/>
Expand All @@ -81,7 +81,7 @@ When you call `client.auth.authorize()`:
### Use cases

You can trigger re-authentication in response to:
- Detect error code [40160](/docs/platform/errors/codes#40160) when a client attempts to attach to a channel.
- Detect error code [40160](/docs/platform/errors/codes/40160) when a client attempts to attach to a channel.
- Instruct clients to re-authenticate via Ably channels or other communication methods.

The following example shows how to re-authenticate with updated capabilities:
Expand Down
Loading
Loading