diff --git a/bin/generate-sidebar-link-lists.js b/bin/generate-sidebar-link-lists.js
new file mode 100644
index 0000000000..64175a2a7f
--- /dev/null
+++ b/bin/generate-sidebar-link-lists.js
@@ -0,0 +1,475 @@
+#!/usr/bin/env node
+
+// Generates the "here's what's in this section" link lists that SDK landing
+// pages and section index pages carry, from sidebars.js — the same source
+// Docusaurus builds the left nav from.
+//
+// Those lists are hand-maintained today, which is why they drift: adding a
+// page wires it into sidebars.js but nothing updates the prose lists that
+// duplicate it. See https://github.com/temporalio/documentation/issues/5144.
+//
+// Only regions wrapped in markers are touched, so a page opts in per list and
+// everything around the markers stays hand-written:
+//
+// {/* SIDEBAR-LIST-START */}
+// - [Workflow basics](/develop/python/workflows/basics)
+// {/* SIDEBAR-LIST-END */}
+//
+// With no attribute the region renders the direct children of whichever
+// sidebar category links to *this* page — the section-index-page case. A
+// landing page carries several lists, one per section, so those name it:
+//
+// {/* SIDEBAR-LIST-START section="Workflows" */}
+//
+// Markers are MDX expression comments, so the LLM markdown pipeline
+// (scripts/mdx-to-md.mjs) already strips them while the generated bullets
+// survive as ordinary Markdown. That's the reason for generating text in
+// place rather than rendering a React component: a component would need a
+// parallel handler in scripts/component-handlers/ to avoid blanking these
+// links in llms-full.txt and the per-page .md files, the way an unwired
+// currently blanks the guides catalog.
+//
+// node bin/generate-sidebar-link-lists.js # rewrite in place
+// node bin/generate-sidebar-link-lists.js --check # CI: fail if stale
+// node bin/generate-sidebar-link-lists.js --json # machine-readable
+// node bin/generate-sidebar-link-lists.js --list # marker inventory
+//
+// Exit codes: 0 clean, 2 stale regions (--check) or an unresolvable marker.
+
+const fs = require('fs');
+const path = require('path');
+const matter = require('gray-matter');
+
+const { walkDir, resolveUrlPath } = require('../plugins/shared/docsRouting');
+const { isExcludedDocPath } = require('./check-orphan-pages');
+
+const DOCS_DIR = path.join(process.cwd(), 'docs');
+const SIDEBARS_FILE = path.join(process.cwd(), 'sidebars.js');
+
+const START = /^(\s*)\{\/\*\s*SIDEBAR-LIST-START(?[^*]*?)\*\/\}\s*$/;
+const END = /^\s*\{\/\*\s*SIDEBAR-LIST-END\s*\*\/\}\s*$/;
+
+// ---------------------------------------------------------------------------
+// Doc index: every routed doc file, keyed by the doc id sidebars.js uses
+// ---------------------------------------------------------------------------
+
+// Mirrors bin/check-orphan-pages.js's computeDocId: an index file's id keeps
+// the literal "index" segment, because that's how sidebars.js addresses it.
+function computeDocId(filePath) {
+ const rel = path.relative(DOCS_DIR, filePath).replace(/\\/g, '/');
+ const withoutExt = rel.replace(/\.(md|mdx)$/i, '');
+ const dir = path.dirname(withoutExt);
+ const base = path.basename(withoutExt);
+ return dir === '.' ? base : `${dir}/${base}`;
+}
+
+function buildDocIndex() {
+ const byId = new Map();
+ for (const filePath of walkDir(DOCS_DIR)) {
+ const rel = path.relative(DOCS_DIR, filePath);
+ if (isExcludedDocPath(rel)) continue;
+
+ const { data } = matter(fs.readFileSync(filePath, 'utf8'));
+ const id = data.id && !data.id.includes('/')
+ // A frontmatter `id` renames only the last segment; sidebars.js still
+ // addresses the file by its directory path.
+ ? [path.dirname(computeDocId(filePath)), data.id].filter((s) => s !== '.').join('/')
+ : computeDocId(filePath);
+
+ byId.set(id, {
+ filePath,
+ url: `/${resolveUrlPath(DOCS_DIR, filePath, data)}`,
+ // Docusaurus falls back title -> sidebar_label for nav text; a `label`
+ // in sidebars.js overrides both and is applied by resolveItem below.
+ label: data.sidebar_label || data.title || path.basename(id),
+ draft: data.draft === true,
+ unlisted: data.unlisted === true,
+ });
+ }
+ return byId;
+}
+
+// ---------------------------------------------------------------------------
+// Sidebar tree, resolved to {label, url} the way the rendered nav resolves it
+// ---------------------------------------------------------------------------
+
+function resolveItem(item, docs) {
+ if (typeof item === 'string') {
+ const doc = docs.get(item);
+ return doc ? { kind: 'doc', id: item, label: doc.label, url: doc.url } : null;
+ }
+ if (!item || typeof item !== 'object') return null;
+
+ if (item.type === 'doc' && item.id) {
+ const doc = docs.get(item.id);
+ if (!doc) return null;
+ return { kind: 'doc', id: item.id, label: item.label || doc.label, url: doc.url };
+ }
+ if (item.type === 'link' && item.href) {
+ return { kind: 'link', label: item.label, url: item.href, external: true };
+ }
+ if (item.type === 'category') {
+ const linkId = item.link && item.link.type === 'doc' ? item.link.id : item.link && item.link.id;
+ const doc = linkId ? docs.get(linkId) : null;
+ return {
+ kind: 'category',
+ label: item.label,
+ url: doc ? doc.url : null,
+ linkId: linkId || null,
+ children: (item.items || []).map((child) => resolveItem(child, docs)).filter(Boolean),
+ };
+ }
+ return null;
+}
+
+function buildSidebarTree(docs) {
+ // sidebars.js is plain CommonJS and already require()s src/constants, so
+ // requiring it gives the real config rather than a regex approximation.
+ const sidebars = require(SIDEBARS_FILE);
+ return Object.values(sidebars)
+ .flat()
+ .map((item) => resolveItem(item, docs))
+ .filter(Boolean);
+}
+
+function eachCategory(nodes, visit) {
+ for (const node of nodes) {
+ if (node.kind !== 'category') continue;
+ visit(node);
+ eachCategory(node.children, visit);
+ }
+}
+
+// The category a page "owns" is the one whose link points at that page.
+function findOwningCategory(tree, docId) {
+ let found = null;
+ eachCategory(tree, (cat) => {
+ if (!found && cat.linkId === docId) found = cat;
+ });
+ return found;
+}
+
+// Matches a sidebar label or the category's own URL. The URL form exists
+// because a landing page's heading text often isn't the sidebar label —
+// "## [Temporal Client](/develop/python/client)" sits above the category
+// labelled "Client" — and the heading already carries the URL, so an author
+// can copy it rather than having to go read sidebars.js.
+function findNamedDescendant(category, nameOrUrl) {
+ const wanted = String(nameOrUrl).replace(/\/$/, '');
+ const byUrl = [];
+ const byLabel = [];
+ eachCategory(category.children, (cat) => {
+ if (cat.url && cat.url.replace(/\/$/, '') === wanted) byUrl.push(cat);
+ else if (cat.label === nameOrUrl) byLabel.push(cat);
+ });
+
+ // A URL is unique, so prefer it and don't let a same-named category elsewhere
+ // in the tree interfere.
+ if (byUrl.length) return byUrl[0];
+
+ // Labels are not unique — "Quickstart" appears under several sections — so
+ // refuse rather than silently picking whichever came first in traversal.
+ if (byLabel.length > 1) {
+ const urls = byLabel.map((c) => c.url || '(no link)').join(', ');
+ throw new Error(`"${nameOrUrl}" matches ${byLabel.length} categories under "${category.label}" (${urls}); use the category URL instead of its label`);
+ }
+ return byLabel[0] || null;
+}
+
+// ---------------------------------------------------------------------------
+// Rendering
+// ---------------------------------------------------------------------------
+
+// A category may have no `link` of its own (Standalone Activities, for one).
+// Docusaurus renders those by falling back to the first descendant that does
+// have a link — mirror that instead of dropping the entry, so a category can
+// never silently vanish from a generated list.
+function firstLink(node) {
+ if (node.url) return node.url;
+ for (const child of node.children || []) {
+ const url = firstLink(child);
+ if (url) return url;
+ }
+ return null;
+}
+
+// One level only, matching what the pages express today: a nested category
+// renders as a single link rather than being flattened, which is what keeps
+// depth-4 pages (serverless-workers/*, data-handling/*) off the landing pages.
+function renderList(category, indent) {
+ return category.children.map((child) => {
+ const url = firstLink(child);
+ if (!url) {
+ // Refusing here rather than omitting: a silently shorter list is the
+ // failure mode this script exists to prevent.
+ throw new Error(`sidebar item "${child.label}" under "${category.label}" has no resolvable link, so it cannot be rendered`);
+ }
+ return `${indent}- [${child.label}](${url})`;
+ });
+}
+
+const BULLET = /^\s*-\s*\[(?[^\]]+)\]\((?[^)]+)\)\s*$/;
+
+// Reconciles a marker region against the sidebar rather than overwriting it.
+//
+// The sidebar decides which pages appear and in what order — that's the drift
+// this script exists to kill. It deliberately does NOT decide the link text:
+// sidebar labels disagree with these lists on 37 links, some of them against
+// the sentence-case rule in readme/STYLE.md ("Feature guide" -> "Feature
+// Guide"), and two SDKs disagree with each other on the same page. Importing
+// that into prose would trade a drift problem for a copy problem.
+//
+// So: an entry already present keeps its hand-written text, a missing entry is
+// added using the sidebar label as a starting point, and a line whose URL
+// isn't in this category at all (an external tutorial link, say) is preserved
+// rather than deleted.
+function reconcileList(category, existingLines, indent) {
+ // Every line is kept, blank ones included. Dropping blanks would make this
+ // silently destructive on a region that had deliberate spacing, which is
+ // exactly what "insert-only" promises not to do.
+ const existing = existingLines.map((line) => {
+ const m = line.match(BULLET);
+ return m ? { line, url: m.groups.url, label: m.groups.label } : { line, url: null, label: null };
+ });
+
+ const generated = renderList(category, indent).map((line) => {
+ const m = line.match(BULLET);
+ return { line, url: m.groups.url, label: m.groups.label };
+ });
+ const sidebarUrls = new Set(generated.map((g) => g.url));
+
+ // Insert-only, deliberately. Reordering and relabelling 52 pages to fix 5
+ // genuine gaps is a bad trade: the sidebar's order and labels are no better
+ // than the prose's (37 label disagreements, some against readme/STYLE.md),
+ // and a purely additive diff is the one a reviewer can actually check. So
+ // existing lines keep their text AND their position; only missing entries
+ // are inserted, each after whichever sidebar sibling precedes it.
+ const existingUrls = new Set(existing.map((e) => e.url).filter(Boolean));
+ const out = existing.map((e) => e.line);
+ const added = [];
+
+ generated.forEach((g, gi) => {
+ if (existingUrls.has(g.url)) return;
+
+ // Anchor to the nearest earlier sidebar sibling that the page already
+ // lists, so a new entry lands next to its neighbours rather than at the
+ // bottom.
+ let at = 0;
+ for (let i = gi - 1; i >= 0; i -= 1) {
+ const idx = out.findIndex((line) => {
+ const m = line.match(BULLET);
+ return m && m.groups.url === generated[i].url;
+ });
+ if (idx !== -1) {
+ at = idx + 1;
+ break;
+ }
+ }
+ out.splice(at, 0, g.line);
+ added.push(g.url);
+ });
+
+ const preserved = existing.filter((e) => e.url && !sidebarUrls.has(e.url)).length;
+ return { lines: out, added, preserved };
+}
+
+function parseAttrs(raw) {
+ const attrs = {};
+ for (const m of String(raw || '').matchAll(/(\w+)="([^"]*)"/g)) attrs[m[1]] = m[2];
+ return attrs;
+}
+
+// Returns {lines, regions:[{start,end,section,expected,actual,stale}]}
+function processFile(filePath, tree, docs) {
+ const original = fs.readFileSync(filePath, 'utf8');
+ const eol = original.includes('\r\n') ? '\r\n' : '\n';
+ const lines = original.split(/\r?\n/);
+ const docId = computeDocId(filePath);
+ const regions = [];
+ const out = [];
+
+ for (let i = 0; i < lines.length; i += 1) {
+ const startMatch = lines[i].match(START);
+ if (!startMatch) {
+ out.push(lines[i]);
+ continue;
+ }
+
+ let close = i + 1;
+ while (close < lines.length && !END.test(lines[close])) close += 1;
+ if (close >= lines.length) {
+ throw new Error(`${path.relative(process.cwd(), filePath)}: SIDEBAR-LIST-START at line ${i + 1} has no matching SIDEBAR-LIST-END`);
+ }
+
+ const indent = startMatch[1] || '';
+ const { section } = parseAttrs(startMatch.groups.attrs);
+ const owning = findOwningCategory(tree, docId);
+ if (!owning) {
+ throw new Error(`${path.relative(process.cwd(), filePath)}: no sidebars.js category links to this page (doc id "${docId}"), so a sidebar list cannot be resolved`);
+ }
+ const target = section ? findNamedDescendant(owning, section) : owning;
+ if (!target) {
+ throw new Error(`${path.relative(process.cwd(), filePath)}: no category matching "${section}" (by label or URL) under "${owning.label}"`);
+ }
+
+ const actual = lines.slice(i + 1, close);
+ const { lines: expected, added, preserved } = reconcileList(target, actual, indent);
+ regions.push({
+ section: section || owning.label,
+ line: i + 1,
+ expected,
+ actual,
+ added,
+ preserved,
+ stale: actual.join('\n') !== expected.join('\n'),
+ });
+
+ out.push(lines[i], ...expected, lines[close]);
+ i = close;
+ }
+
+ return { content: out.join(eol), regions, changed: out.join(eol) !== original };
+}
+
+// ---------------------------------------------------------------------------
+// CLI
+// ---------------------------------------------------------------------------
+
+function filesWithMarkers() {
+ return walkDir(DOCS_DIR).filter((f) => {
+ const rel = path.relative(DOCS_DIR, f);
+ if (isExcludedDocPath(rel)) return false;
+ return /SIDEBAR-LIST-START/.test(fs.readFileSync(f, 'utf8'));
+ });
+}
+
+// Markers are opt-in, so coverage quietly erodes: add a section index page,
+// don't add markers, and this check stays green while that page drifts. So
+// report pages that look like they should be covered but aren't.
+//
+// A warning, not a failure. A page with real prose is allowed to keep a
+// hand-written list, and the absence of a marker is how it says so — the same
+// reason this needs no baseline file.
+function unmarkedCandidates(tree) {
+ const candidates = [];
+ for (const filePath of walkDir(DOCS_DIR)) {
+ const rel = path.relative(DOCS_DIR, filePath);
+ if (isExcludedDocPath(rel)) continue;
+
+ const posix = rel.replace(/\\/g, '/');
+ if (!posix.startsWith('develop/') || !/\/index\.mdx?$/.test(posix)) continue;
+
+ const text = fs.readFileSync(filePath, 'utf8');
+ if (/SIDEBAR-LIST-START/.test(text)) continue;
+
+ // Any page that already hand-maintains internal links qualifies. A single
+ // link counts: a Workers page listing one entry is likelier to be missing
+ // something than one listing ten, and an earlier `< 2` cutoff hid exactly
+ // those pages — four workers/index.mdx among them.
+ const bullets = [...text.matchAll(/^-\s*\[[^\]]+\]\((\/[^)]+)\)\s*$/gm)];
+ if (bullets.length < 1) continue;
+
+ const category = findOwningCategory(tree, computeDocId(filePath));
+ if (!category) continue;
+
+ candidates.push({
+ file: path.relative(process.cwd(), filePath),
+ section: category.label,
+ bullets: bullets.length,
+ });
+ }
+ return candidates;
+}
+
+function main(argv) {
+ const check = argv.includes('--check');
+ const asJson = argv.includes('--json');
+ const listOnly = argv.includes('--list');
+
+ const docs = buildDocIndex();
+ const tree = buildSidebarTree(docs);
+ const files = filesWithMarkers();
+
+ const unmarked = unmarkedCandidates(tree);
+
+ if (listOnly) {
+ console.log(`${files.length} file(s) with sidebar-list markers:`);
+ for (const f of files) console.log(` ${path.relative(process.cwd(), f)}`);
+ console.log(`\n${unmarked.length} page(s) hand-maintaining a list with no markers:`);
+ for (const c of unmarked) console.log(` ${c.file} [${c.section}, ${c.bullets} links]`);
+ return 0;
+ }
+
+ const results = [];
+ for (const filePath of files) {
+ const { content, regions, changed } = processFile(filePath, tree, docs);
+ results.push({ file: path.relative(process.cwd(), filePath), regions, changed });
+ if (!check && changed) fs.writeFileSync(filePath, content);
+ }
+
+ const stale = results.flatMap((r) => r.regions.filter((x) => x.stale).map((x) => ({ ...x, file: r.file })));
+
+ if (asJson) {
+ console.log(JSON.stringify({ files: results.length, stale, unmarked }, null, 2));
+ return stale.length && check ? 2 : 0;
+ }
+
+ if (!files.length) {
+ console.log('No sidebar-list markers found. Nothing to do.');
+ return 0;
+ }
+
+ // Printed on both paths: a green check with low coverage is the misleading
+ // case this warning exists to prevent.
+ const warnUnmarked = () => {
+ if (!unmarked.length) return;
+ console.error(`\nNote: ${unmarked.length} page(s) still hand-maintain a link list with no markers, so this check does not cover them:`);
+ for (const c of unmarked) console.error(` ${c.file} [${c.section}, ${c.bullets} links]`);
+ console.error('Add markers to bring them under the check, or leave them if the list is deliberately hand-written.');
+ };
+
+ if (check) {
+ if (!stale.length) {
+ console.log(`Sidebar link lists are up to date (${results.length} file(s) checked).`);
+ warnUnmarked();
+ return 0;
+ }
+ console.error(`${stale.length} sidebar link list(s) are out of date:\n`);
+ for (const s of stale) {
+ console.error(` ${s.file}:${s.line} [${s.section}]`);
+ for (const line of s.actual.filter((l) => !s.expected.includes(l))) console.error(` - ${line.trim()}`);
+ for (const line of s.expected.filter((l) => !s.actual.includes(l))) console.error(` + ${line.trim()}`);
+ console.error('');
+ }
+ console.error('Run `yarn sidebar-links` to update them.');
+ warnUnmarked();
+ return 2;
+ }
+
+ const rewritten = results.filter((r) => r.changed);
+ console.log(`Generated ${results.reduce((n, r) => n + r.regions.length, 0)} list(s) across ${results.length} file(s); ${rewritten.length} file(s) rewritten.`);
+ for (const r of rewritten) console.log(` updated ${r.file}`);
+ return 0;
+}
+
+if (require.main === module) {
+ try {
+ process.exit(main(process.argv.slice(2)));
+ } catch (err) {
+ console.error(err.message);
+ process.exit(2);
+ }
+}
+
+module.exports = {
+ unmarkedCandidates,
+ buildDocIndex,
+ buildSidebarTree,
+ processFile,
+ findOwningCategory,
+ findNamedDescendant,
+ renderList,
+ reconcileList,
+ firstLink,
+ computeDocId,
+};
diff --git a/bin/generate-sidebar-link-lists.test.js b/bin/generate-sidebar-link-lists.test.js
new file mode 100644
index 0000000000..efe23eaaf3
--- /dev/null
+++ b/bin/generate-sidebar-link-lists.test.js
@@ -0,0 +1,199 @@
+const test = require('node:test');
+const assert = require('node:assert');
+const fs = require('node:fs');
+const path = require('node:path');
+
+const {
+ reconcileList,
+ firstLink,
+ buildDocIndex,
+ buildSidebarTree,
+ findOwningCategory,
+ findNamedDescendant,
+ processFile,
+} = require('./generate-sidebar-link-lists');
+
+// Synthetic categories keep these cases stable as the real docs change.
+const cat = (label, children) => ({ kind: 'category', label, url: null, children });
+const doc = (label, url) => ({ kind: 'doc', label, url });
+const link = (label, url) => ({ kind: 'link', label, url, external: true });
+
+test('adds a missing sidebar entry next to its sibling, not at the end', () => {
+ const category = cat('Workers', [
+ doc('Run a Worker', '/develop/python/workers/run-worker-process'),
+ doc('Serverless Workers', '/develop/python/workers/serverless-workers'),
+ doc('Interceptors', '/develop/python/workers/interceptors'),
+ ]);
+ const existing = [
+ '- [Worker processes](/develop/python/workers/run-worker-process)',
+ '- [Interceptors](/develop/python/workers/interceptors)',
+ ];
+
+ const { lines, added } = reconcileList(category, existing, '');
+
+ assert.deepStrictEqual(added, ['/develop/python/workers/serverless-workers']);
+ assert.deepStrictEqual(lines, [
+ // Hand-written "Worker processes" survives; the sidebar's "Run a Worker" does not win.
+ '- [Worker processes](/develop/python/workers/run-worker-process)',
+ '- [Serverless Workers](/develop/python/workers/serverless-workers)',
+ '- [Interceptors](/develop/python/workers/interceptors)',
+ ]);
+});
+
+test('never rewrites the label of an entry that is already listed', () => {
+ const category = cat('Best practices', [doc('Data handling', '/develop/java/best-practices/data-handling')]);
+ const existing = ['- [Converters and encryption](/develop/java/best-practices/data-handling)'];
+
+ const { lines, added } = reconcileList(category, existing, '');
+
+ assert.deepStrictEqual(added, []);
+ assert.deepStrictEqual(lines, existing, 'sidebar labels disagree with prose on 37 links; prose wins');
+});
+
+test('preserves a link the sidebar does not know about', () => {
+ // docs/develop/java/nexus/index.mdx carries an external learn.temporal.io
+ // tutorial link inside its section list.
+ const category = cat('Nexus', [doc('Quickstart', '/develop/java/nexus/quickstart')]);
+ const existing = [
+ '- [Quickstart](/develop/java/nexus/quickstart)',
+ '- [Nexus sync tutorial](https://learn.temporal.io/tutorials/nexus/nexus-sync-tutorial/)',
+ ];
+
+ const { lines } = reconcileList(category, existing, '');
+
+ assert.ok(
+ lines.includes('- [Nexus sync tutorial](https://learn.temporal.io/tutorials/nexus/nexus-sync-tutorial/)'),
+ 'an external link inside a generated region must survive',
+ );
+ assert.strictEqual(lines.length, 2);
+});
+
+test('is idempotent', () => {
+ const category = cat('Workers', [
+ doc('Run a Worker', '/a'),
+ doc('Serverless Workers', '/b'),
+ ]);
+ const first = reconcileList(category, ['- [Worker processes](/a)'], '').lines;
+ const second = reconcileList(category, first, '').lines;
+ assert.deepStrictEqual(second, first);
+});
+
+test('resolves a category with no link of its own to its first descendant', () => {
+ // "Standalone Activities" has no link:, so Docusaurus falls back to the
+ // first child. Dropping it instead would silently shorten the list.
+ const nested = cat('Standalone Activities', [
+ doc('Quickstart', '/develop/go/activities/standalone-activities-quickstart'),
+ doc('Feature Guide', '/develop/go/activities/standalone-activities'),
+ ]);
+ assert.strictEqual(firstLink(nested), '/develop/go/activities/standalone-activities-quickstart');
+});
+
+test('refuses to render an item with no resolvable link rather than omitting it', () => {
+ const category = cat('Activities', [cat('Empty', [])]);
+ assert.throws(() => reconcileList(category, [], ''), /no resolvable link/);
+});
+
+test('keeps an external sidebar link item', () => {
+ const category = cat('Section', [link('Change log', 'https://temporal.io/change-log')]);
+ const { lines } = reconcileList(category, [], '');
+ assert.deepStrictEqual(lines, ['- [Change log](https://temporal.io/change-log)']);
+});
+
+test('honours the indent of the marker', () => {
+ const category = cat('S', [doc('A', '/a')]);
+ const { lines } = reconcileList(category, [], ' ');
+ assert.deepStrictEqual(lines, [' - [A](/a)']);
+});
+
+// Integration: the real sidebars.js and docs tree must resolve without
+// throwing, so a malformed sidebar entry fails the test rather than the build.
+test('every SDK section index page resolves to a sidebar category', () => {
+ const docs = buildDocIndex();
+ const tree = buildSidebarTree(docs);
+ const missing = [];
+
+ for (const [id, entry] of docs) {
+ if (!/^develop\/[a-z]+\/[a-z-]+\/index$/.test(id)) continue;
+ if (entry.draft || entry.unlisted) continue;
+ if (!findOwningCategory(tree, id)) missing.push(id);
+ }
+
+ assert.deepStrictEqual(missing, [], 'these pages have no owning sidebar category');
+});
+
+// ---------------------------------------------------------------------------
+// processFile: marker parsing and the error paths. These run against a real
+// temporary page under docs/ because the doc id -> sidebar category lookup
+// only means anything against the real sidebars.js.
+// ---------------------------------------------------------------------------
+
+const TMP_DIR = path.join(process.cwd(), 'docs', 'develop', '_sidebar_link_list_fixture');
+
+function withFixture(body, run) {
+ fs.mkdirSync(TMP_DIR, { recursive: true });
+ const file = path.join(TMP_DIR, 'index.mdx');
+ fs.writeFileSync(file, body);
+ try {
+ return run(file);
+ } finally {
+ fs.rmSync(TMP_DIR, { recursive: true, force: true });
+ }
+}
+
+test('processFile rejects a region with no closing marker', () => {
+ const docs = buildDocIndex();
+ const tree = buildSidebarTree(docs);
+ withFixture('---\nid: index\n---\n\n{/* SIDEBAR-LIST-START */}\n- [A](/a)\n', (file) => {
+ assert.throws(() => processFile(file, tree, docs), /no matching SIDEBAR-LIST-END/);
+ });
+});
+
+test('processFile rejects a page no sidebar category links to', () => {
+ const docs = buildDocIndex();
+ const tree = buildSidebarTree(docs);
+ withFixture('---\nid: index\n---\n\n{/* SIDEBAR-LIST-START */}\n{/* SIDEBAR-LIST-END */}\n', (file) => {
+ assert.throws(() => processFile(file, tree, docs), /no sidebars\.js category links to this page/);
+ });
+});
+
+test('processFile leaves everything outside the markers untouched', () => {
+ const docs = buildDocIndex();
+ const tree = buildSidebarTree(docs);
+ const real = path.join(process.cwd(), 'docs', 'develop', 'python', 'index.mdx');
+ const before = fs.readFileSync(real, 'utf8');
+ const { content, regions } = processFile(real, tree, docs);
+
+ assert.ok(regions.length > 0, 'the Python landing page should have marked regions');
+
+ // Compare everything that is not inside a marked region.
+ const strip = (text) => {
+ const out = [];
+ let inside = false;
+ for (const line of text.split('\n')) {
+ if (/SIDEBAR-LIST-START/.test(line)) { inside = true; out.push(line); continue; }
+ if (/SIDEBAR-LIST-END/.test(line)) { inside = false; out.push(line); continue; }
+ if (!inside) out.push(line);
+ }
+ return out.join('\n');
+ };
+ assert.strictEqual(strip(content), strip(before), 'content outside the markers must not change');
+});
+
+test('processFile is a no-op on an already-current page', () => {
+ const docs = buildDocIndex();
+ const tree = buildSidebarTree(docs);
+ const real = path.join(process.cwd(), 'docs', 'develop', 'python', 'index.mdx');
+ const { changed } = processFile(real, tree, docs);
+ assert.strictEqual(changed, false, 'run `yarn sidebar-links` — the committed page is stale');
+});
+
+test('a marker keyed by category URL resolves even when the heading text differs', () => {
+ // "## [Temporal Client](/develop/python/client)" sits above the category
+ // labelled "Client", which is why the URL form exists.
+ const docs = buildDocIndex();
+ const tree = buildSidebarTree(docs);
+ const own = findOwningCategory(tree, 'develop/python/index');
+ assert.ok(own, 'the Python SDK category should link develop/python/index');
+ assert.strictEqual(findNamedDescendant(own, '/develop/python/client').label, 'Client');
+ assert.strictEqual(findNamedDescendant(own, 'Temporal Client'), null, 'heading text is not a sidebar label');
+});
diff --git a/docs/develop/dotnet/index.mdx b/docs/develop/dotnet/index.mdx
index 04a93ce7c7..197204c5ec 100644
--- a/docs/develop/dotnet/index.mdx
+++ b/docs/develop/dotnet/index.mdx
@@ -36,6 +36,7 @@ From there, you can dive deeper into any of the Temporal primitives to start bui
## [Workflows](/develop/dotnet/workflows)
+{/* SIDEBAR-LIST-START section="/develop/dotnet/workflows" */}
- [Workflow basics](/develop/dotnet/workflows/basics)
- [Child Workflows](/develop/dotnet/workflows/child-workflows)
- [Continue-As-New](/develop/dotnet/workflows/continue-as-new)
@@ -46,9 +47,11 @@ From there, you can dive deeper into any of the Temporal primitives to start bui
- [Timers](/develop/dotnet/workflows/timers)
- [Dynamic Workflow](/develop/dotnet/workflows/dynamic-workflow)
- [Versioning](/develop/dotnet/workflows/versioning)
+{/* SIDEBAR-LIST-END */}
## [Activities](/develop/dotnet/activities)
+{/* SIDEBAR-LIST-START section="/develop/dotnet/activities" */}
- [Activity basics](/develop/dotnet/activities/basics)
- [Activity execution](/develop/dotnet/activities/execution)
- [Standalone Activities](/develop/dotnet/activities/standalone-activities-quickstart)
@@ -56,32 +59,45 @@ From there, you can dive deeper into any of the Temporal primitives to start bui
- [Asynchronous Activity completion](/develop/dotnet/activities/asynchronous-activity)
- [Dynamic Activity](/develop/dotnet/activities/dynamic-activity)
- [Benign exceptions](/develop/dotnet/activities/benign-exceptions)
+{/* SIDEBAR-LIST-END */}
## [Workers](/develop/dotnet/workers)
+{/* SIDEBAR-LIST-START section="/develop/dotnet/workers" */}
- [Worker processes](/develop/dotnet/workers/run-worker-process)
- [Interceptors](/develop/dotnet/workers/interceptors)
+- [Serverless Workers](/develop/dotnet/workers/serverless-workers)
+{/* SIDEBAR-LIST-END */}
## [Temporal Client](/develop/dotnet/client)
+{/* SIDEBAR-LIST-START section="/develop/dotnet/client" */}
- [Temporal Client](/develop/dotnet/client/temporal-client)
+{/* SIDEBAR-LIST-END */}
## [Temporal Nexus](/develop/dotnet/nexus)
+{/* SIDEBAR-LIST-START section="/develop/dotnet/nexus" */}
- [Quickstart](/develop/dotnet/nexus/quickstart)
- [Feature guide](/develop/dotnet/nexus/feature-guide)
+- [Standalone Operations](/develop/dotnet/nexus/standalone-operations)
+{/* SIDEBAR-LIST-END */}
## [Platform](/develop/dotnet/platform)
+{/* SIDEBAR-LIST-START section="/develop/dotnet/platform" */}
- [Observability](/develop/dotnet/platform/observability)
- [Enriching the UI](/develop/dotnet/platform/enriching-ui)
+{/* SIDEBAR-LIST-END */}
## [Best practices](/develop/dotnet/best-practices)
+{/* SIDEBAR-LIST-START section="/develop/dotnet/best-practices" */}
- [Error handling](/develop/dotnet/best-practices/error-handling)
- [Testing](/develop/dotnet/best-practices/testing-suite)
- [Debugging](/develop/dotnet/best-practices/debugging)
- [Converters and encryption](/develop/dotnet/best-practices/data-handling)
+{/* SIDEBAR-LIST-END */}
## Temporal .NET technical resources
diff --git a/docs/develop/dotnet/workers/index.mdx b/docs/develop/dotnet/workers/index.mdx
index e16176e1e1..495e55ce43 100644
--- a/docs/develop/dotnet/workers/index.mdx
+++ b/docs/develop/dotnet/workers/index.mdx
@@ -19,5 +19,8 @@ import * as Components from '@site/src/components';
## Workers
+{/* SIDEBAR-LIST-START */}
- [Worker processes](/develop/dotnet/workers/run-worker-process)
- [Interceptors](/develop/dotnet/workers/interceptors)
+- [Serverless Workers](/develop/dotnet/workers/serverless-workers)
+{/* SIDEBAR-LIST-END */}
diff --git a/docs/develop/go/index.mdx b/docs/develop/go/index.mdx
index d7d076003f..5e5348ca91 100644
--- a/docs/develop/go/index.mdx
+++ b/docs/develop/go/index.mdx
@@ -36,6 +36,7 @@ From there, you can dive deeper into any of the Temporal primitives to start bui
## [Workflows](/develop/go/workflows)
+{/* SIDEBAR-LIST-START section="/develop/go/workflows" */}
- [Workflow basics](/develop/go/workflows/basics)
- [Child Workflows](/develop/go/workflows/child-workflows)
- [Continue-As-New](/develop/go/workflows/continue-as-new)
@@ -49,9 +50,11 @@ From there, you can dive deeper into any of the Temporal primitives to start bui
- [Dynamic Workflow](/develop/go/workflows/dynamic-workflow)
- [Versioning](/develop/go/workflows/versioning)
- [Workflow Streams](/develop/go/workflows/workflow-streams)
+{/* SIDEBAR-LIST-END */}
## [Activities](/develop/go/activities)
+{/* SIDEBAR-LIST-START section="/develop/go/activities" */}
- [Activity basics](/develop/go/activities/basics)
- [Activity execution](/develop/go/activities/execution)
- [Standalone Activities](/develop/go/activities/standalone-activities-quickstart)
@@ -59,42 +62,55 @@ From there, you can dive deeper into any of the Temporal primitives to start bui
- [Asynchronous Activity completion](/develop/go/activities/asynchronous-activity)
- [Dynamic Activity](/develop/go/activities/dynamic-activity)
- [Benign exceptions](/develop/go/activities/benign-exceptions)
+{/* SIDEBAR-LIST-END */}
## [Workers](/develop/go/workers)
+{/* SIDEBAR-LIST-START section="/develop/go/workers" */}
- [Run a Worker](/develop/go/workers/run-worker-process)
- [Sessions](/develop/go/workers/sessions)
- [Serverless Workers](/develop/go/workers/serverless-workers)
+{/* SIDEBAR-LIST-END */}
## [Temporal Client](/develop/go/client)
+{/* SIDEBAR-LIST-START section="/develop/go/client" */}
- [Temporal Client](/develop/go/client/temporal-client)
- [Namespaces](/develop/go/client/namespaces)
+{/* SIDEBAR-LIST-END */}
## [Temporal Nexus](/develop/go/nexus)
+{/* SIDEBAR-LIST-START section="/develop/go/nexus" */}
- [Quickstart](/develop/go/nexus/quickstart)
- [Feature guide](/develop/go/nexus/feature-guide)
- [Standalone Operations](/develop/go/nexus/standalone-operations)
+{/* SIDEBAR-LIST-END */}
## [Platform](/develop/go/platform)
+{/* SIDEBAR-LIST-START section="/develop/go/platform" */}
- [Observability](/develop/go/platform/observability)
- [Enriching the UI](/develop/go/platform/enriching-ui)
+{/* SIDEBAR-LIST-END */}
## [Best practices](/develop/go/best-practices)
+{/* SIDEBAR-LIST-START section="/develop/go/best-practices" */}
- [Multithreading](/develop/go/best-practices/multithreading)
- [Context propagation](/develop/go/best-practices/context-propagation)
- [Error handling](/develop/go/best-practices/error-handling)
- [Debugging](/develop/go/best-practices/debugging)
- [Testing](/develop/go/best-practices/testing-suite)
- [Data handling](/develop/go/data-handling)
+{/* SIDEBAR-LIST-END */}
## [Integrations](/develop/go/integrations)
+{/* SIDEBAR-LIST-START section="/develop/go/integrations" */}
- [Google ADK integration](/develop/go/integrations/google-adk)
- [OpenTelemetry v2 integration](/develop/go/integrations/opentelemetry-v2)
+{/* SIDEBAR-LIST-END */}
## Temporal Go technical resources
diff --git a/docs/develop/java/index.mdx b/docs/develop/java/index.mdx
index c4ad311a11..035665029d 100644
--- a/docs/develop/java/index.mdx
+++ b/docs/develop/java/index.mdx
@@ -34,6 +34,7 @@ From there, you can dive deeper into any of the Temporal primitives to start bui
## [Workflows](/develop/java/workflows)
+{/* SIDEBAR-LIST-START section="/develop/java/workflows" */}
- [Workflow basics](/develop/java/workflows/basics)
- [Child Workflows](/develop/java/workflows/child-workflows)
- [Continue-As-New](/develop/java/workflows/continue-as-new)
@@ -45,48 +46,65 @@ From there, you can dive deeper into any of the Temporal primitives to start bui
- [Side effects](/develop/java/workflows/side-effects)
- [Versioning](/develop/java/workflows/versioning)
- [Workflow Streams](/develop/java/workflows/workflow-streams)
+{/* SIDEBAR-LIST-END */}
## [Activities](/develop/java/activities)
+{/* SIDEBAR-LIST-START section="/develop/java/activities" */}
- [Activity basics](/develop/java/activities/basics)
- [Activity execution](/develop/java/activities/execution)
- [Standalone Activities](/develop/java/activities/standalone-activities-quickstart)
- [Timeouts](/develop/java/activities/timeouts)
- [Asynchronous Activity Completion](/develop/java/activities/asynchronous-activity)
- [Benign exceptions](/develop/java/activities/benign-exceptions)
+{/* SIDEBAR-LIST-END */}
## [Workers](/develop/java/workers)
+{/* SIDEBAR-LIST-START section="/develop/java/workers" */}
- [Worker processes](/develop/java/workers/run-worker-process)
+- [Serverless Workers](/develop/java/workers/serverless-workers)
- [Observability](/develop/java/platform/observability)
+{/* SIDEBAR-LIST-END */}
## [Temporal Client](/develop/java/client)
+{/* SIDEBAR-LIST-START section="/develop/java/client" */}
- [Temporal Client](/develop/java/client/temporal-client)
- [Namespaces](/develop/java/client/namespaces)
+{/* SIDEBAR-LIST-END */}
## [Temporal Nexus](/develop/java/nexus)
+{/* SIDEBAR-LIST-START section="/develop/java/nexus" */}
- [Quickstart](/develop/java/nexus/quickstart)
- [Feature guide](/develop/java/nexus/feature-guide)
- [Standalone Operations](/develop/java/nexus/standalone-operations)
+{/* SIDEBAR-LIST-END */}
## [Platform](/develop/java/platform)
+{/* SIDEBAR-LIST-START section="/develop/java/platform" */}
- [Observability](/develop/java/platform/observability)
- [Enriching the UI](/develop/java/platform/enriching-ui)
+{/* SIDEBAR-LIST-END */}
## [Best practices](/develop/java/best-practices)
+{/* SIDEBAR-LIST-START section="/develop/java/best-practices" */}
+- [Error handling](/develop/java/best-practices/error-handling)
- [Testing](/develop/java/best-practices/testing-suite)
- [Debugging](/develop/java/best-practices/debugging)
- [Converters and encryption](/develop/java/best-practices/data-handling)
+{/* SIDEBAR-LIST-END */}
## [Integrations](/develop/java/integrations)
+{/* SIDEBAR-LIST-START section="/develop/java/integrations" */}
- [Parseable integration](https://github.com/parseablehq/temporal-plugin-java/blob/main/INTEGRATION.md)
- [Spring AI integration](/develop/java/integrations/spring-ai)
- [Spring Boot integration](/develop/java/integrations/spring-boot-integration)
+{/* SIDEBAR-LIST-END */}
## Temporal Java technical resources
diff --git a/docs/develop/java/workers/index.mdx b/docs/develop/java/workers/index.mdx
index 24f6e9f90d..a43ca8f9c9 100644
--- a/docs/develop/java/workers/index.mdx
+++ b/docs/develop/java/workers/index.mdx
@@ -19,4 +19,7 @@ import * as Components from '@site/src/components';
## Workers
+{/* SIDEBAR-LIST-START */}
- [Run Worker processes](/develop/java/workers/run-worker-process)
+- [Serverless Workers](/develop/java/workers/serverless-workers)
+{/* SIDEBAR-LIST-END */}
diff --git a/docs/develop/php/index.mdx b/docs/develop/php/index.mdx
index 90d4e1794d..27cfa0c41a 100644
--- a/docs/develop/php/index.mdx
+++ b/docs/develop/php/index.mdx
@@ -34,6 +34,7 @@ From there, you can dive deeper into any of the Temporal primitives to start bui
## [Workflows](/develop/php/workflows)
+{/* SIDEBAR-LIST-START section="/develop/php/workflows" */}
- [Workflow Basics](/develop/php/workflows/basics)
- [Child Workflows](/develop/php/workflows/child-workflows)
- [Continue-As-New](/develop/php/workflows/continue-as-new)
@@ -44,31 +45,42 @@ From there, you can dive deeper into any of the Temporal primitives to start bui
- [Timers](/develop/php/workflows/timers)
- [Side effects](/develop/php/workflows/side-effects)
- [Versioning](/develop/php/workflows/versioning)
+{/* SIDEBAR-LIST-END */}
## [Activities](/develop/php/activities)
+{/* SIDEBAR-LIST-START section="/develop/php/activities" */}
- [Activity Basics](/develop/php/activities/basics)
- [Activity Execution](/develop/php/activities/execution)
- [Timeouts](/develop/php/activities/timeouts)
- [Asynchronous Activity Completion](/develop/php/activities/asynchronous-activity)
+{/* SIDEBAR-LIST-END */}
## [Workers](/develop/php/workers)
+{/* SIDEBAR-LIST-START section="/develop/php/workers" */}
- [Run Worker processes](/develop/php/workers/run-worker-process)
+{/* SIDEBAR-LIST-END */}
## [Temporal Client](/develop/php/client)
+{/* SIDEBAR-LIST-START section="/develop/php/client" */}
- [Temporal Client](/develop/php/client/temporal-client)
+{/* SIDEBAR-LIST-END */}
## [Platform](/develop/php/platform)
+{/* SIDEBAR-LIST-START section="/develop/php/platform" */}
- [Observability](/develop/php/platform/observability)
- [Enriching the UI](/develop/php/platform/enriching-ui)
+{/* SIDEBAR-LIST-END */}
## [Best practices](/develop/php/best-practices)
+{/* SIDEBAR-LIST-START section="/develop/php/best-practices" */}
- [Testing](/develop/php/best-practices/testing-suite)
- [Debugging](/develop/php/best-practices/debugging)
+{/* SIDEBAR-LIST-END */}
## Temporal PHP technical resources
diff --git a/docs/develop/python/index.mdx b/docs/develop/python/index.mdx
index 0095fd8719..bbbe37959b 100644
--- a/docs/develop/python/index.mdx
+++ b/docs/develop/python/index.mdx
@@ -34,6 +34,7 @@ From there, you can dive deeper into any of the Temporal primitives to start bui
## [Workflows](/develop/python/workflows)
+{/* SIDEBAR-LIST-START section="/develop/python/workflows" */}
- [Workflow basics](/develop/python/workflows/basics)
- [Child Workflows](/develop/python/workflows/child-workflows)
- [Continue-As-New](/develop/python/workflows/continue-as-new)
@@ -44,45 +45,62 @@ From there, you can dive deeper into any of the Temporal primitives to start bui
- [Timers](/develop/python/workflows/timers)
- [Versioning](/develop/python/workflows/versioning)
- [Workflow Streams](/develop/python/workflows/workflow-streams)
+{/* SIDEBAR-LIST-END */}
## [Activities](/develop/python/activities)
+{/* SIDEBAR-LIST-START section="/develop/python/activities" */}
- [Activity basics](/develop/python/activities/basics)
- [Activity execution](/develop/python/activities/execution)
- [Standalone Activities](/develop/python/activities/standalone-activities-quickstart)
- [Timeouts](/develop/python/activities/timeouts)
- [Asynchronous Activity completion](/develop/python/activities/asynchronous-activity)
- [Benign exceptions](/develop/python/activities/benign-exceptions)
+{/* SIDEBAR-LIST-END */}
## [Workers](/develop/python/workers)
+{/* SIDEBAR-LIST-START section="/develop/python/workers" */}
- [Worker processes](/develop/python/workers/run-worker-process)
+- [Interceptors](/develop/python/workers/interceptors)
+- [Serverless Workers](/develop/python/workers/serverless-workers)
+{/* SIDEBAR-LIST-END */}
## [Temporal Client](/develop/python/client)
+{/* SIDEBAR-LIST-START section="/develop/python/client" */}
- [Temporal Client](/develop/python/client/temporal-client)
+{/* SIDEBAR-LIST-END */}
## [Temporal Nexus](/develop/python/nexus)
+{/* SIDEBAR-LIST-START section="/develop/python/nexus" */}
- [Quickstart](/develop/python/nexus/quickstart)
- [Feature guide](/develop/python/nexus/feature-guide)
- [Standalone Operations](/develop/python/nexus/standalone-operations)
+{/* SIDEBAR-LIST-END */}
## [Platform](/develop/python/platform)
+{/* SIDEBAR-LIST-START section="/develop/python/platform" */}
- [Observability](/develop/python/platform/observability)
- [Enriching the UI](/develop/python/platform/enriching-ui)
+{/* SIDEBAR-LIST-END */}
## [Best practices](/develop/python/best-practices)
+{/* SIDEBAR-LIST-START section="/develop/python/best-practices" */}
+- [Error handling](/develop/python/best-practices/error-handling)
- [Testing](/develop/python/best-practices/testing-suite)
- [Python SDK sandbox](/develop/python/best-practices/python-sdk-sandbox)
- [Debugging](/develop/python/best-practices/debugging)
- [Data handling](/develop/python/data-handling)
- [Sync vs async](/develop/python/best-practices/python-sdk-sync-vs-async)
+{/* SIDEBAR-LIST-END */}
## [Integrations](/develop/python/integrations)
+{/* SIDEBAR-LIST-START section="/develop/python/integrations" */}
- [Braintrust integration](/develop/python/integrations/braintrust)
- [Deep Agents integration](/develop/python/integrations/deepagents)
- [Google ADK integration](/develop/python/integrations/google-adk)
@@ -96,6 +114,7 @@ From there, you can dive deeper into any of the Temporal primitives to start bui
- [Pydantic AI integration](https://ai.pydantic.dev/durable_execution/temporal/)
- [Strands Agents integration](/develop/python/integrations/strands-agents)
- [Tenuo integration](https://tenuo.ai/temporal)
+{/* SIDEBAR-LIST-END */}
## Temporal Python technical resources
diff --git a/docs/develop/python/workers/index.mdx b/docs/develop/python/workers/index.mdx
index 97cf989250..c0fcc07be9 100644
--- a/docs/develop/python/workers/index.mdx
+++ b/docs/develop/python/workers/index.mdx
@@ -19,5 +19,8 @@ import * as Components from '@site/src/components';
## Workers
+{/* SIDEBAR-LIST-START */}
- [Worker processes](/develop/python/workers/run-worker-process)
- [Interceptors](/develop/python/workers/interceptors)
+- [Serverless Workers](/develop/python/workers/serverless-workers)
+{/* SIDEBAR-LIST-END */}
diff --git a/docs/develop/ruby/index.mdx b/docs/develop/ruby/index.mdx
index ad50a4e762..3158e313ba 100644
--- a/docs/develop/ruby/index.mdx
+++ b/docs/develop/ruby/index.mdx
@@ -38,6 +38,7 @@ From there, you can dive deeper into any of the Temporal primitives to start bui
## [Workflows](/develop/ruby/workflows)
+{/* SIDEBAR-LIST-START section="/develop/ruby/workflows" */}
- [Workflow basics](/develop/ruby/workflows/basics)
- [Child Workflows](/develop/ruby/workflows/child-workflows)
- [Continue-As-New](/develop/ruby/workflows/continue-as-new)
@@ -49,9 +50,11 @@ From there, you can dive deeper into any of the Temporal primitives to start bui
- [Futures](/develop/ruby/workflows/futures)
- [Dynamic Workflow](/develop/ruby/workflows/dynamic-workflow)
- [Versioning](/develop/ruby/workflows/versioning)
+{/* SIDEBAR-LIST-END */}
## [Activities](/develop/ruby/activities)
+{/* SIDEBAR-LIST-START section="/develop/ruby/activities" */}
- [Activity basics](/develop/ruby/activities/basics)
- [Activity execution](/develop/ruby/activities/execution)
- [Standalone Activities](/develop/ruby/activities/standalone-activities-quickstart)
@@ -59,31 +62,43 @@ From there, you can dive deeper into any of the Temporal primitives to start bui
- [Asynchronous Activity completion](/develop/ruby/activities/asynchronous-activity)
- [Dynamic Activity](/develop/ruby/activities/dynamic-activity)
- [Benign exceptions](/develop/ruby/activities/benign-exceptions)
+{/* SIDEBAR-LIST-END */}
## [Workers](/develop/ruby/workers)
+{/* SIDEBAR-LIST-START section="/develop/ruby/workers" */}
- [Worker processes](/develop/ruby/workers/run-worker-process)
+- [Serverless Workers](/develop/ruby/workers/serverless-workers)
- [Observability](/develop/ruby/platform/observability)
+{/* SIDEBAR-LIST-END */}
## [Temporal Client](/develop/ruby/client)
+{/* SIDEBAR-LIST-START section="/develop/ruby/client" */}
- [Temporal Client](/develop/ruby/client/temporal-client)
+{/* SIDEBAR-LIST-END */}
## [Platform](/develop/ruby/platform)
+{/* SIDEBAR-LIST-START section="/develop/ruby/platform" */}
- [Observability](/develop/ruby/platform/observability)
- [Enriching the UI](/develop/ruby/platform/enriching-ui)
+{/* SIDEBAR-LIST-END */}
## [Integrations](/develop/ruby/integrations)
+{/* SIDEBAR-LIST-START section="/develop/ruby/integrations" */}
- [Rails integration](/develop/ruby/integrations/rails-integration)
+{/* SIDEBAR-LIST-END */}
## [Best practices](/develop/ruby/best-practices)
+{/* SIDEBAR-LIST-START section="/develop/ruby/best-practices" */}
- [Error handling](/develop/ruby/best-practices/error-handling)
- [Testing](/develop/ruby/best-practices/testing-suite)
- [Debugging](/develop/ruby/best-practices/debugging)
- [Converters and encryption](/develop/ruby/best-practices/data-handling)
+{/* SIDEBAR-LIST-END */}
## Temporal Ruby technical resources
diff --git a/docs/develop/ruby/workers/index.mdx b/docs/develop/ruby/workers/index.mdx
index b345b79db4..767cb9966a 100644
--- a/docs/develop/ruby/workers/index.mdx
+++ b/docs/develop/ruby/workers/index.mdx
@@ -19,4 +19,7 @@ import * as Components from '@site/src/components';
## Workers
+{/* SIDEBAR-LIST-START */}
- [Worker processes](/develop/ruby/workers/run-worker-process)
+- [Serverless Workers](/develop/ruby/workers/serverless-workers)
+{/* SIDEBAR-LIST-END */}
diff --git a/docs/develop/rust/index.mdx b/docs/develop/rust/index.mdx
index 61c03ba94b..31c42a5707 100644
--- a/docs/develop/rust/index.mdx
+++ b/docs/develop/rust/index.mdx
@@ -31,6 +31,7 @@ Once your local Temporal Service is set up, continue building with the following
## [Workflows](/develop/rust/workflows)
+{/* SIDEBAR-LIST-START section="/develop/rust/workflows" */}
- [Workflow basics](/develop/rust/workflows/basics)
- [Child Workflows](/develop/rust/workflows/child-workflows)
- [Continue-As-New](/develop/rust/workflows/continue-as-new)
@@ -38,24 +39,34 @@ Once your local Temporal Service is set up, continue building with the following
- [Cancellation](/develop/rust/workflows/cancellation)
- [Timers](/develop/rust/workflows/timers)
- [Timeouts](/develop/rust/workflows/timeouts)
+{/* SIDEBAR-LIST-END */}
## [Activities](/develop/rust/activities)
+{/* SIDEBAR-LIST-START section="/develop/rust/activities" */}
- [Activity basics](/develop/rust/activities/basics)
- [Activity execution](/develop/rust/activities/execution)
- [Timeouts](/develop/rust/activities/timeouts)
+{/* SIDEBAR-LIST-END */}
## [Workers](/develop/rust/workers)
+{/* SIDEBAR-LIST-START section="/develop/rust/workers" */}
- [Worker processes](/develop/rust/workers/worker-process)
+- [Serverless Workers](/develop/rust/workers/serverless-workers)
+{/* SIDEBAR-LIST-END */}
## [Temporal Client](/develop/rust/client)
+{/* SIDEBAR-LIST-START section="/develop/rust/client" */}
- [Temporal Client](/develop/rust/client/temporal-client)
+{/* SIDEBAR-LIST-END */}
## [Temporal Nexus](/develop/rust/nexus)
+{/* SIDEBAR-LIST-START section="/develop/rust/nexus" */}
- [Feature guide](/develop/rust/nexus/feature-guide)
+{/* SIDEBAR-LIST-END */}
## Temporal Rust technical resources
diff --git a/docs/develop/rust/workers/index.mdx b/docs/develop/rust/workers/index.mdx
index e9a9880c20..4a4876251b 100644
--- a/docs/develop/rust/workers/index.mdx
+++ b/docs/develop/rust/workers/index.mdx
@@ -20,4 +20,7 @@ import * as Components from '@site/src/components';
## Workers
+{/* SIDEBAR-LIST-START */}
- [Worker processes](/develop/rust/workers/worker-process)
+- [Serverless Workers](/develop/rust/workers/serverless-workers)
+{/* SIDEBAR-LIST-END */}
diff --git a/docs/develop/typescript/index.mdx b/docs/develop/typescript/index.mdx
index 4cf2e9edcc..efe968d1ca 100644
--- a/docs/develop/typescript/index.mdx
+++ b/docs/develop/typescript/index.mdx
@@ -34,6 +34,7 @@ Once your local Temporal Service is set up, continue building with the following
## [Workflows](/develop/typescript/workflows)
+{/* SIDEBAR-LIST-START section="/develop/typescript/workflows" */}
- [Workflow basics](/develop/typescript/workflows/basics)
- [Child Workflows](/develop/typescript/workflows/child-workflows)
- [Continue-As-New](/develop/typescript/workflows/continue-as-new)
@@ -45,45 +46,61 @@ Once your local Temporal Service is set up, continue building with the following
- [Timers](/develop/typescript/workflows/timers)
- [Versioning](/develop/typescript/workflows/versioning)
- [Workflow Streams](/develop/typescript/workflows/workflow-streams)
+{/* SIDEBAR-LIST-END */}
## [Activities](/develop/typescript/activities)
+{/* SIDEBAR-LIST-START section="/develop/typescript/activities" */}
- [Activity basics](/develop/typescript/activities/basics)
- [Activity execution](/develop/typescript/activities/execution)
+- [Standalone Activities](/develop/typescript/activities/standalone-activities-quickstart)
- [Timeouts](/develop/typescript/activities/timeouts)
- [Asynchronous Activity](/develop/typescript/activities/asynchronous-activity)
- [Benign exceptions](/develop/typescript/activities/benign-exceptions)
+{/* SIDEBAR-LIST-END */}
## [Workers](/develop/typescript/workers)
+{/* SIDEBAR-LIST-START section="/develop/typescript/workers" */}
- [Worker processes](/develop/typescript/workers/run-worker-process)
- [Interceptors](/develop/typescript/workers/interceptors)
+- [Serverless Workers](/develop/typescript/workers/serverless-workers)
+{/* SIDEBAR-LIST-END */}
## [Temporal Client](/develop/typescript/client)
+{/* SIDEBAR-LIST-START section="/develop/typescript/client" */}
- [Temporal Client](/develop/typescript/client/temporal-client)
- [Namespaces](/develop/typescript/client/namespaces)
+{/* SIDEBAR-LIST-END */}
## [Temporal Nexus](/develop/typescript/nexus)
+{/* SIDEBAR-LIST-START section="/develop/typescript/nexus" */}
- [Quickstart](/develop/typescript/nexus/quickstart)
- [Feature guide](/develop/typescript/nexus/feature-guide)
- [Standalone Operations](/develop/typescript/nexus/standalone-operations)
+{/* SIDEBAR-LIST-END */}
## [Platform](/develop/typescript/platform)
+{/* SIDEBAR-LIST-START section="/develop/typescript/platform" */}
- [Observability](/develop/typescript/platform/observability)
- [Enriching the UI](/develop/typescript/platform/enriching-ui)
+{/* SIDEBAR-LIST-END */}
## [Best practices](/develop/typescript/best-practices)
+{/* SIDEBAR-LIST-START section="/develop/typescript/best-practices" */}
- [Testing](/develop/typescript/best-practices/testing-suite)
- [Debugging](/develop/typescript/best-practices/debugging)
- [Converters and encryption](/develop/typescript/best-practices/data-handling)
- [Entity pattern](/develop/typescript/best-practices/entity-pattern)
+{/* SIDEBAR-LIST-END */}
## [Integrations](/develop/typescript/integrations)
+{/* SIDEBAR-LIST-START section="/develop/typescript/integrations" */}
- [Braintrust integration](https://www.braintrust.dev/docs/integrations/sdk-integrations/temporal#typescript)
- [LangSmith integration](/develop/typescript/integrations/langsmith)
- [Mastra integration](https://mastra.ai/guides/deployment/temporal)
@@ -91,6 +108,7 @@ Once your local Temporal Service is set up, continue building with the following
- [Parseable integration](https://github.com/parseablehq/temporal-plugin/blob/main/INTEGRATION.md)
- [Strands Agents integration](/develop/typescript/integrations/strands-agents)
- [Vercel AI SDK integration](/develop/typescript/integrations/ai-sdk)
+{/* SIDEBAR-LIST-END */}
## Temporal TypeScript technical resources
diff --git a/docs/develop/typescript/workers/index.mdx b/docs/develop/typescript/workers/index.mdx
index 6eec105f56..24ea437337 100644
--- a/docs/develop/typescript/workers/index.mdx
+++ b/docs/develop/typescript/workers/index.mdx
@@ -20,5 +20,8 @@ import * as Components from '@site/src/components';
## Workers
+{/* SIDEBAR-LIST-START */}
- [Worker processes](/develop/typescript/workers/run-worker-process)
- [Interceptors](/develop/typescript/workers/interceptors)
+- [Serverless Workers](/develop/typescript/workers/serverless-workers)
+{/* SIDEBAR-LIST-END */}
diff --git a/package.json b/package.json
index b3c2873fe0..1db7857441 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,8 @@
"check:metrics": "node ./bin/check-metrics-reference.js",
"check:metrics:sdks": "node ./bin/check-metrics-against-sdks.js",
"check:orphans": "node ./bin/check-orphan-pages.js",
+ "check:sidebar-links": "node ./bin/generate-sidebar-link-lists.js --check",
+ "sidebar-links": "node ./bin/generate-sidebar-link-lists.js",
"test": "node --test",
"lint": "vale --output=JSON docs/**/**/*.mdx > vale-output.json; vale --output=JSON docs/**/**/*.md > vale-md-output.json",
"lint:go": "vale docs/develop/go/*.mdx",