From f839d78c98ee26d3009cfd6863c22af025a81b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20Pr=C3=A9?= Date: Wed, 22 Apr 2026 01:01:48 -0700 Subject: [PATCH 01/11] ci: Add redirect target validator and fix 18 broken redirects Add validate-redirects CI check that verifies all .gitbook.yaml redirect targets resolve to existing pages. Fix 18 broken targets caused by pages being moved or removed over time. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitbook.yaml | 36 ++++++------ .github/workflows/generate.yml | 2 + codegen/validate-redirects.ts | 103 +++++++++++++++++++++++++++++++++ package.json | 1 + 4 files changed, 124 insertions(+), 18 deletions(-) create mode 100644 codegen/validate-redirects.ts diff --git a/.gitbook.yaml b/.gitbook.yaml index 4d2830808..74535f92d 100644 --- a/.gitbook.yaml +++ b/.gitbook.yaml @@ -4,7 +4,7 @@ root: ./docs/guides/ readme: README.md summary: SUMMARY.md redirects: - latest/quickstart/get-api-key: latest/quickstart.md + latest/quickstart/get-api-key: quickstart.md quickstart/get-api-key: quickstart.md quickstart/installation: quickstart.md quickstart/authentication: quickstart.md @@ -16,18 +16,18 @@ redirects: core-concepts/workspaces/personal-access-tokens: core-concepts/authentication/personal-access-tokens.md core-concepts/workspaces/client-session-tokens: core-concepts/authentication/client-session-tokens/README.md core-concepts/workspaces/client-session-tokens/implementing-client-sessions-for-device-management-in-the-backend: core-concepts/authentication/client-session-tokens/implementing-client-sessions-for-device-management-in-the-backend.md - products/access-systems/user-management: capability-guides/access-systems/user-management/README.md - capability-guides/access-systems/managing-credentials: capability-guides/access-systems/managing-credentials/README.md + products/access-systems/user-management: capability-guides/access-systems/user-management.md + capability-guides/access-systems/managing-credentials: capability-guides/access-systems/managing-credentials.md capability-guides/access-systems/assigning-credentials-to-users: capability-guides/access-systems/managing-credentials/assigning-credentials-to-users.md products/access-systems/suspending-and-unsuspending-users: capability-guides/access-systems/user-management/suspending-and-unsuspending-users.md products/seam-bridge-in-development: capability-guides/seam-bridge.md core-concepts/connect-webviews: core-concepts/connect-webviews/README.md device-guides/4suites-locks: device-and-system-integration-guides/4suites-locks/README.md device-guides/assa-abloy-visionline-access-control-system-in-development: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/README.md - device-guides/assa-abloy-visionline-access-control-system-in-development/credential-types: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/credential-types.md - device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/visionline-credential-metadata: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/visionline-credential-metadata.md + device-guides/assa-abloy-visionline-access-control-system-in-development/credential-types: device-and-system-integration-guides/assa-abloy-visionline-access-control-system + device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/visionline-credential-metadata: device-and-system-integration-guides/assa-abloy-visionline-access-control-system device-guides/assa-abloy-visionline-access-control-system-in-development/common-use-cases: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/common-use-cases.md - capability-guides/access-systems/understanding-acs-differences: capability-guides/access-systems/understanding-access-control-system-differences.md + capability-guides/access-systems/understanding-acs-differences: capability-guides/access-systems capability-guides/thermostats/creating-and-managing-climate-schedules: capability-guides/thermostats/creating-and-managing-thermostat-schedules.md device-guides/get-started-with-schlage-locks: device-and-system-integration-guides/schlage-locks/get-started-with-schlage-locks.md api-clients/access-codes/convert-an-unmanaged-access-code: api/access_codes/unmanaged/convert_to_managed.md @@ -95,7 +95,7 @@ redirects: api-clients/connected-accounts/list-connected-accounts: api/connected_accounts/list.md api-clients/connected-accounts/update-a-connected-account: api/connected_accounts/update.md api-clients/connected-accounts: api/connected_accounts/README.md - api-clients/devices/delete-device: api/devices/delete.md + api-clients/devices/delete-device: api/devices api-clients/devices/get-device-1: api/devices/unmanaged/get.md api-clients/devices/get-device: api/devices/get.md api-clients/devices/list-device-providers: api/devices/list_device_providers.md @@ -103,8 +103,8 @@ redirects: api-clients/devices/list-unmanaged-devices: api/devices/unmanaged/list.md api-clients/devices/update-device: api/devices/update.md api-clients/devices/update-unmanaged-device: api/devices/unmanaged/update.md - api-clients/events/get-an-event: api-clients/events/get.md - api-clients/events/list-events: api-clients/events/list.md + api-clients/events/get-an-event: api/events/get.md + api-clients/events/list-events: api/events/list.md api-clients/locks/get-lock: api/locks/get.md api-clients/locks/list-locks: api/locks/list.md api-clients/locks/lock-a-lock: api/locks/lock_door.md @@ -114,7 +114,7 @@ redirects: api-clients/noise-sensors/list-noise-thresholds: api/noise_sensors/noise_thresholds/list.md api-clients/noise-sensors/update-noise-threshold: api/noise_sensors/noise_thresholds/update.md api-clients/noise-sensors: api/noise_sensors/README.md - api-clients/thermostats/get-thermostat: api/thermostats/get.md + api-clients/thermostats/get-thermostat: api/thermostats api-clients/thermostats/list-thermostats: api/thermostats/list.md api-clients/thermostats/set-fan-mode: api/thermostats/set_fan_mode.md api-clients/thermostats/set-to-cool-mode: api/thermostats/cool.md @@ -136,7 +136,7 @@ redirects: api-clients/workspaces/get-workspace: api/workspaces/get.md api-clients/workspaces/reset-workspace: api/workspaces/reset_sandbox.md api-clients/phones/create-a-sandbox-phone: api/phones/simulate/create_sandbox_phone.md - device-and-system-integration-guides/assa-abloy-credential-services-credential-manager-in-development: device-and-system-integration-guides/assa-abloy-credential-services-credential-manager/README.md + device-and-system-integration-guides/assa-abloy-credential-services-credential-manager-in-development: device-and-system-integration-guides/assa-abloy-vingcard-credential-services.md device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/README.md device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/visionline-acs-setup-instructions: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/visionline-acs-setup-instructions/README.md device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/visionline-acs-setup-instructions/developing-and-launching-your-visionline-plastic-card-encoding-app: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/visionline-acs-setup-instructions/developing-and-launching-your-visionline-plastic-card-encoding-app/README.md @@ -183,13 +183,13 @@ redirects: device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/visionline-acs-setup-instructions/developing-and-launching-your-visionline-mobile-key-app/launching-your-visionline-mobile-key-app/step-7-purchase-and-import-the-callback-and-mobile-service-options-from-assa-abloy: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/visionline-acs-setup-instructions/developing-and-launching-your-visionline-mobile-key-app/launching-your-visionline-mobile-key-app/step-7-purchase-and-import-the-callback-and-mobile-service-options-from-assa-abloy.md device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/visionline-acs-setup-instructions/developing-and-launching-your-visionline-mobile-key-app/launching-your-visionline-mobile-key-app/step-8-create-a-production-workspace: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/visionline-acs-setup-instructions/developing-and-launching-your-visionline-mobile-key-app/launching-your-visionline-mobile-key-app/step-8-create-a-production-workspace.md device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/visionline-acs-setup-instructions/developing-and-launching-your-visionline-mobile-key-app/launching-your-visionline-mobile-key-app/step-9-connect-your-visionline-production-account-to-seam: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/visionline-acs-setup-instructions/developing-and-launching-your-visionline-mobile-key-app/launching-your-visionline-mobile-key-app/step-9-connect-your-visionline-production-account-to-seam.md - device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/credential-types/README.md - device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/issuing-various-types-of-guest-mobile-credentials: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/credential-types/issuing-various-types-of-guest-mobile-credentials.md - device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/issuing-various-types-of-guest-joiner-mobile-credentials: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/credential-types/issuing-various-types-of-guest-joiner-mobile-credentials.md - device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/retrieving-guest-and-common-entrances: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/credential-types/retrieving-guest-and-common-entrances.md - device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/checking-if-a-user-identity-has-a-phone-that-is-set-up-for-a-credential-manager: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/credential-types/checking-if-a-user-identity-has-a-phone-that-is-set-up-for-a-credential-manager.md - device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/updating-guest-mobile-credentials: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/credential-types/updating-guest-mobile-credentials.md - device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/revoking-mobile-credentials: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/credential-types/revoking-mobile-credentials.md + device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types: device-and-system-integration-guides/assa-abloy-visionline-access-control-system + device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/issuing-various-types-of-guest-mobile-credentials: device-and-system-integration-guides/assa-abloy-visionline-access-control-system + device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/issuing-various-types-of-guest-joiner-mobile-credentials: device-and-system-integration-guides/assa-abloy-visionline-access-control-system + device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/retrieving-guest-and-common-entrances: device-and-system-integration-guides/assa-abloy-visionline-access-control-system + device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/checking-if-a-user-identity-has-a-phone-that-is-set-up-for-a-credential-manager: device-and-system-integration-guides/assa-abloy-visionline-access-control-system + device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/updating-guest-mobile-credentials: device-and-system-integration-guides/assa-abloy-visionline-access-control-system + device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/revoking-mobile-credentials: device-and-system-integration-guides/assa-abloy-visionline-access-control-system device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/mobile-credential-related-properties: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/mobile-credential-related-properties.md device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/common-use-cases: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/common-use-cases.md device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/special-requirements-for-android-mobile-access-sdk-development: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/special-requirements-for-android-mobile-access-sdk-development.md diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml index 3554c3085..dc689ea2e 100644 --- a/.github/workflows/generate.yml +++ b/.github/workflows/generate.yml @@ -84,3 +84,5 @@ jobs: run: npm run validate-links - name: Validate all pages are in SUMMARY.md run: npm run validate-orphan-pages + - name: Validate redirect targets + run: npm run validate-redirects diff --git a/codegen/validate-redirects.ts b/codegen/validate-redirects.ts new file mode 100644 index 000000000..e0fef9a52 --- /dev/null +++ b/codegen/validate-redirects.ts @@ -0,0 +1,103 @@ +import { existsSync, readFileSync } from 'node:fs' +import { join } from 'node:path' + +import { siteSections } from './lib/config.js' + +// Parse the redirects section from .gitbook.yaml. +// Each redirect line is: " source-path: target-path" +const gitbookYaml = readFileSync('.gitbook.yaml', 'utf-8') +const lines = gitbookYaml.split('\n') + +interface Redirect { + line: number + source: string + target: string +} + +const redirects: Redirect[] = [] +let inRedirects = false + +for (let i = 0; i < lines.length; i++) { + const line = lines[i] ?? '' + + if (line === 'redirects:') { + inRedirects = true + continue + } + + // A non-indented, non-empty line ends the redirects section + if (inRedirects && line !== '' && !line.startsWith(' ')) { + break + } + + if (!inRedirects) continue + + const match = line.match(/^\s+(.+?):\s+(.+)$/) + if (match?.[1] != null && match[2] != null) { + redirects.push({ line: i + 1, source: match[1], target: match[2] }) + } +} + +// Check if a path resolves to a page as a file, URL slug, or directory index. +function pageExists(fullPath: string): boolean { + // Exact file match (e.g., quickstart.md) + if (existsSync(fullPath)) return true + // Directory with README.md (e.g., core-concepts/ → core-concepts/README.md) + if (existsSync(join(fullPath, 'README.md'))) return true + // URL-style target without .md extension (e.g., api/devices → api/devices.md) + if (!fullPath.endsWith('.md') && existsSync(fullPath + '.md')) return true + return false +} + +// Resolve a redirect target to check it points to a real page. +// Targets may reference other site sections via URL prefix. +function resolveTarget(target: string): boolean { + // Try matching against site sections by URL prefix (most specific first). + // The siteSections array is already ordered with more-specific prefixes first. + for (const section of siteSections) { + if (section.urlPrefix === '') continue + + const prefix = section.urlPrefix.replace(/^\//, '') + '/' + if (target.startsWith(prefix)) { + const relativePath = target.slice(prefix.length) + return pageExists(join(section.root, relativePath)) + } + } + + // Fall back to the default section (Guides, urlPrefix '') + const guidesSection = siteSections.find((s) => s.urlPrefix === '') + if (guidesSection == null) return false + + return pageExists(join(guidesSection.root, target)) +} + +interface BrokenRedirect { + line: number + source: string + target: string +} + +const broken: BrokenRedirect[] = [] + +for (const redirect of redirects) { + if (!resolveTarget(redirect.target)) { + broken.push(redirect) + } +} + +if (broken.length > 0) { + // eslint-disable-next-line no-console + console.error( + `Found ${broken.length} redirect(s) with missing target(s) in .gitbook.yaml:\n`, + ) + for (const { line, source, target } of broken) { + // eslint-disable-next-line no-console + console.error(` line ${line}: ${source}`) + // eslint-disable-next-line no-console + console.error(` target: ${target}\n`) + } + process.exit(1) +} else { + // eslint-disable-next-line no-console + console.log('All .gitbook.yaml redirect targets are valid.') +} diff --git a/package.json b/package.json index a4135399c..cc2ac010c 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "validate-paths": "tsx codegen/validate-paths.ts", "validate-links": "tsx codegen/validate-links.ts", "validate-orphan-pages": "tsx codegen/validate-orphan-pages.ts", + "validate-redirects": "tsx codegen/validate-redirects.ts", "typecheck": "tsc", "lint": "eslint .", "postlint": "prettier --check --ignore-path .prettierignore .", From 6782eaee9a932d8bbd9614803d8de4cfac2826de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20Pr=C3=A9?= Date: Wed, 22 Apr 2026 01:05:01 -0700 Subject: [PATCH 02/11] fix: Keep README.md redirect targets, update validator to handle them GitBook resolves README.md in redirect targets as directory URLs, so these don't need to be changed. Update the validator to accept foo/README.md when foo.md exists as the page file. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitbook.yaml | 4 ++-- codegen/validate-redirects.ts | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.gitbook.yaml b/.gitbook.yaml index 74535f92d..59e46e483 100644 --- a/.gitbook.yaml +++ b/.gitbook.yaml @@ -16,8 +16,8 @@ redirects: core-concepts/workspaces/personal-access-tokens: core-concepts/authentication/personal-access-tokens.md core-concepts/workspaces/client-session-tokens: core-concepts/authentication/client-session-tokens/README.md core-concepts/workspaces/client-session-tokens/implementing-client-sessions-for-device-management-in-the-backend: core-concepts/authentication/client-session-tokens/implementing-client-sessions-for-device-management-in-the-backend.md - products/access-systems/user-management: capability-guides/access-systems/user-management.md - capability-guides/access-systems/managing-credentials: capability-guides/access-systems/managing-credentials.md + products/access-systems/user-management: capability-guides/access-systems/user-management/README.md + capability-guides/access-systems/managing-credentials: capability-guides/access-systems/managing-credentials/README.md capability-guides/access-systems/assigning-credentials-to-users: capability-guides/access-systems/managing-credentials/assigning-credentials-to-users.md products/access-systems/suspending-and-unsuspending-users: capability-guides/access-systems/user-management/suspending-and-unsuspending-users.md products/seam-bridge-in-development: capability-guides/seam-bridge.md diff --git a/codegen/validate-redirects.ts b/codegen/validate-redirects.ts index e0fef9a52..2f84ad2b9 100644 --- a/codegen/validate-redirects.ts +++ b/codegen/validate-redirects.ts @@ -46,6 +46,12 @@ function pageExists(fullPath: string): boolean { if (existsSync(join(fullPath, 'README.md'))) return true // URL-style target without .md extension (e.g., api/devices → api/devices.md) if (!fullPath.endsWith('.md') && existsSync(fullPath + '.md')) return true + // README.md targets resolve as directory URLs in GitBook, so check if the + // parent path resolves as a page (e.g., foo/README.md → foo.md). + if (fullPath.endsWith('/README.md')) { + const parentPath = fullPath.slice(0, -'/README.md'.length) + if (existsSync(parentPath + '.md')) return true + } return false } From 076e084fca00b5ca337d5bae33e8aa91723f0a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20Pr=C3=A9?= Date: Wed, 22 Apr 2026 01:06:25 -0700 Subject: [PATCH 03/11] fix: Remove redundant latest/ redirect prefix and deduplicate entry Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitbook.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitbook.yaml b/.gitbook.yaml index 59e46e483..6b2b2ba85 100644 --- a/.gitbook.yaml +++ b/.gitbook.yaml @@ -4,7 +4,6 @@ root: ./docs/guides/ readme: README.md summary: SUMMARY.md redirects: - latest/quickstart/get-api-key: quickstart.md quickstart/get-api-key: quickstart.md quickstart/installation: quickstart.md quickstart/authentication: quickstart.md From 757669ffca47b991eadf4ce98d590381e78f5a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20Pr=C3=A9?= Date: Wed, 22 Apr 2026 11:29:46 -0700 Subject: [PATCH 04/11] fix: Replace ad-hoc YAML parser with yaml package in redirect validator Co-Authored-By: Claude Opus 4.6 (1M context) --- codegen/validate-redirects.ts | 54 +++++++---------------------------- package-lock.json | 17 +++++++++++ package.json | 1 + 3 files changed, 29 insertions(+), 43 deletions(-) diff --git a/codegen/validate-redirects.ts b/codegen/validate-redirects.ts index 2f84ad2b9..55132cc47 100644 --- a/codegen/validate-redirects.ts +++ b/codegen/validate-redirects.ts @@ -1,42 +1,22 @@ import { existsSync, readFileSync } from 'node:fs' import { join } from 'node:path' +import YAML from 'yaml' + import { siteSections } from './lib/config.js' -// Parse the redirects section from .gitbook.yaml. -// Each redirect line is: " source-path: target-path" -const gitbookYaml = readFileSync('.gitbook.yaml', 'utf-8') -const lines = gitbookYaml.split('\n') +const gitbookConfig = YAML.parse(readFileSync('.gitbook.yaml', 'utf-8')) as { + redirects?: Record +} interface Redirect { - line: number source: string target: string } -const redirects: Redirect[] = [] -let inRedirects = false - -for (let i = 0; i < lines.length; i++) { - const line = lines[i] ?? '' - - if (line === 'redirects:') { - inRedirects = true - continue - } - - // A non-indented, non-empty line ends the redirects section - if (inRedirects && line !== '' && !line.startsWith(' ')) { - break - } - - if (!inRedirects) continue - - const match = line.match(/^\s+(.+?):\s+(.+)$/) - if (match?.[1] != null && match[2] != null) { - redirects.push({ line: i + 1, source: match[1], target: match[2] }) - } -} +const redirects: Redirect[] = Object.entries( + gitbookConfig.redirects ?? {}, +).map(([source, target]) => ({ source, target })) // Check if a path resolves to a page as a file, URL slug, or directory index. function pageExists(fullPath: string): boolean { @@ -77,28 +57,16 @@ function resolveTarget(target: string): boolean { return pageExists(join(guidesSection.root, target)) } -interface BrokenRedirect { - line: number - source: string - target: string -} - -const broken: BrokenRedirect[] = [] - -for (const redirect of redirects) { - if (!resolveTarget(redirect.target)) { - broken.push(redirect) - } -} +const broken: Redirect[] = redirects.filter((r) => !resolveTarget(r.target)) if (broken.length > 0) { // eslint-disable-next-line no-console console.error( `Found ${broken.length} redirect(s) with missing target(s) in .gitbook.yaml:\n`, ) - for (const { line, source, target } of broken) { + for (const { source, target } of broken) { // eslint-disable-next-line no-console - console.error(` line ${line}: ${source}`) + console.error(` ${source}`) // eslint-disable-next-line no-console console.error(` target: ${target}\n`) } diff --git a/package-lock.json b/package-lock.json index 22548b929..db3436635 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "command-exists": "^1.2.9", "execa": "^9.3.1", "prettier": "^3.0.0", + "yaml": "^2.8.3", "zod": "^3.23.8" }, "engines": { @@ -6810,6 +6811,22 @@ "co": "3.1.0" } }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index cc2ac010c..b2d685cc7 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "command-exists": "^1.2.9", "execa": "^9.3.1", "prettier": "^3.0.0", + "yaml": "^2.8.3", "zod": "^3.23.8" } } From b64f7d4adb7df650b1f4335e4aff87edce96e260 Mon Sep 17 00:00:00 2001 From: Seam Bot Date: Wed, 22 Apr 2026 18:30:58 +0000 Subject: [PATCH 05/11] ci: Format code --- codegen/validate-redirects.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/codegen/validate-redirects.ts b/codegen/validate-redirects.ts index 55132cc47..29b20a0b7 100644 --- a/codegen/validate-redirects.ts +++ b/codegen/validate-redirects.ts @@ -14,9 +14,9 @@ interface Redirect { target: string } -const redirects: Redirect[] = Object.entries( - gitbookConfig.redirects ?? {}, -).map(([source, target]) => ({ source, target })) +const redirects: Redirect[] = Object.entries(gitbookConfig.redirects ?? {}).map( + ([source, target]) => ({ source, target }), +) // Check if a path resolves to a page as a file, URL slug, or directory index. function pageExists(fullPath: string): boolean { From e3245ffe92ebbd5779e44f6cb949d6c8a4898266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20Pr=C3=A9?= Date: Wed, 22 Apr 2026 11:32:03 -0700 Subject: [PATCH 06/11] fix: Use actual file paths for redirect targets instead of README.md Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitbook.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitbook.yaml b/.gitbook.yaml index 6b2b2ba85..4cccd70b2 100644 --- a/.gitbook.yaml +++ b/.gitbook.yaml @@ -15,8 +15,8 @@ redirects: core-concepts/workspaces/personal-access-tokens: core-concepts/authentication/personal-access-tokens.md core-concepts/workspaces/client-session-tokens: core-concepts/authentication/client-session-tokens/README.md core-concepts/workspaces/client-session-tokens/implementing-client-sessions-for-device-management-in-the-backend: core-concepts/authentication/client-session-tokens/implementing-client-sessions-for-device-management-in-the-backend.md - products/access-systems/user-management: capability-guides/access-systems/user-management/README.md - capability-guides/access-systems/managing-credentials: capability-guides/access-systems/managing-credentials/README.md + products/access-systems/user-management: capability-guides/access-systems/user-management.md + capability-guides/access-systems/managing-credentials: capability-guides/access-systems/managing-credentials.md capability-guides/access-systems/assigning-credentials-to-users: capability-guides/access-systems/managing-credentials/assigning-credentials-to-users.md products/access-systems/suspending-and-unsuspending-users: capability-guides/access-systems/user-management/suspending-and-unsuspending-users.md products/seam-bridge-in-development: capability-guides/seam-bridge.md From 30fc291620a4ae74faf05cc4d4151fba875fd034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20Pr=C3=A9?= Date: Wed, 22 Apr 2026 11:46:10 -0700 Subject: [PATCH 07/11] fix: Remove implicit directory/README.md resolution from redirect validator Redirect targets should always point to exact files. Co-Authored-By: Claude Opus 4.6 (1M context) --- codegen/validate-redirects.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/codegen/validate-redirects.ts b/codegen/validate-redirects.ts index 29b20a0b7..1eb14f3e0 100644 --- a/codegen/validate-redirects.ts +++ b/codegen/validate-redirects.ts @@ -22,8 +22,6 @@ const redirects: Redirect[] = Object.entries(gitbookConfig.redirects ?? {}).map( function pageExists(fullPath: string): boolean { // Exact file match (e.g., quickstart.md) if (existsSync(fullPath)) return true - // Directory with README.md (e.g., core-concepts/ → core-concepts/README.md) - if (existsSync(join(fullPath, 'README.md'))) return true // URL-style target without .md extension (e.g., api/devices → api/devices.md) if (!fullPath.endsWith('.md') && existsSync(fullPath + '.md')) return true // README.md targets resolve as directory URLs in GitBook, so check if the From 263f868be3d93803c5528294136c6c3a1f025782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20Pr=C3=A9?= Date: Wed, 22 Apr 2026 11:52:27 -0700 Subject: [PATCH 08/11] fix: Ensure all redirect targets point to files, not bare directories Replace bare directory targets with explicit README.md paths and update the validator to reject directories. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitbook.yaml | 24 ++++++++++++------------ codegen/validate-redirects.ts | 6 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.gitbook.yaml b/.gitbook.yaml index 4cccd70b2..c85b34c1b 100644 --- a/.gitbook.yaml +++ b/.gitbook.yaml @@ -23,10 +23,10 @@ redirects: core-concepts/connect-webviews: core-concepts/connect-webviews/README.md device-guides/4suites-locks: device-and-system-integration-guides/4suites-locks/README.md device-guides/assa-abloy-visionline-access-control-system-in-development: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/README.md - device-guides/assa-abloy-visionline-access-control-system-in-development/credential-types: device-and-system-integration-guides/assa-abloy-visionline-access-control-system - device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/visionline-credential-metadata: device-and-system-integration-guides/assa-abloy-visionline-access-control-system + device-guides/assa-abloy-visionline-access-control-system-in-development/credential-types: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/README.md + device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/visionline-credential-metadata: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/README.md device-guides/assa-abloy-visionline-access-control-system-in-development/common-use-cases: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/common-use-cases.md - capability-guides/access-systems/understanding-acs-differences: capability-guides/access-systems + capability-guides/access-systems/understanding-acs-differences: capability-guides/access-systems/README.md capability-guides/thermostats/creating-and-managing-climate-schedules: capability-guides/thermostats/creating-and-managing-thermostat-schedules.md device-guides/get-started-with-schlage-locks: device-and-system-integration-guides/schlage-locks/get-started-with-schlage-locks.md api-clients/access-codes/convert-an-unmanaged-access-code: api/access_codes/unmanaged/convert_to_managed.md @@ -94,7 +94,7 @@ redirects: api-clients/connected-accounts/list-connected-accounts: api/connected_accounts/list.md api-clients/connected-accounts/update-a-connected-account: api/connected_accounts/update.md api-clients/connected-accounts: api/connected_accounts/README.md - api-clients/devices/delete-device: api/devices + api-clients/devices/delete-device: api/devices/README.md api-clients/devices/get-device-1: api/devices/unmanaged/get.md api-clients/devices/get-device: api/devices/get.md api-clients/devices/list-device-providers: api/devices/list_device_providers.md @@ -113,7 +113,7 @@ redirects: api-clients/noise-sensors/list-noise-thresholds: api/noise_sensors/noise_thresholds/list.md api-clients/noise-sensors/update-noise-threshold: api/noise_sensors/noise_thresholds/update.md api-clients/noise-sensors: api/noise_sensors/README.md - api-clients/thermostats/get-thermostat: api/thermostats + api-clients/thermostats/get-thermostat: api/thermostats/README.md api-clients/thermostats/list-thermostats: api/thermostats/list.md api-clients/thermostats/set-fan-mode: api/thermostats/set_fan_mode.md api-clients/thermostats/set-to-cool-mode: api/thermostats/cool.md @@ -182,13 +182,13 @@ redirects: device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/visionline-acs-setup-instructions/developing-and-launching-your-visionline-mobile-key-app/launching-your-visionline-mobile-key-app/step-7-purchase-and-import-the-callback-and-mobile-service-options-from-assa-abloy: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/visionline-acs-setup-instructions/developing-and-launching-your-visionline-mobile-key-app/launching-your-visionline-mobile-key-app/step-7-purchase-and-import-the-callback-and-mobile-service-options-from-assa-abloy.md device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/visionline-acs-setup-instructions/developing-and-launching-your-visionline-mobile-key-app/launching-your-visionline-mobile-key-app/step-8-create-a-production-workspace: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/visionline-acs-setup-instructions/developing-and-launching-your-visionline-mobile-key-app/launching-your-visionline-mobile-key-app/step-8-create-a-production-workspace.md device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/visionline-acs-setup-instructions/developing-and-launching-your-visionline-mobile-key-app/launching-your-visionline-mobile-key-app/step-9-connect-your-visionline-production-account-to-seam: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/visionline-acs-setup-instructions/developing-and-launching-your-visionline-mobile-key-app/launching-your-visionline-mobile-key-app/step-9-connect-your-visionline-production-account-to-seam.md - device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types: device-and-system-integration-guides/assa-abloy-visionline-access-control-system - device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/issuing-various-types-of-guest-mobile-credentials: device-and-system-integration-guides/assa-abloy-visionline-access-control-system - device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/issuing-various-types-of-guest-joiner-mobile-credentials: device-and-system-integration-guides/assa-abloy-visionline-access-control-system - device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/retrieving-guest-and-common-entrances: device-and-system-integration-guides/assa-abloy-visionline-access-control-system - device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/checking-if-a-user-identity-has-a-phone-that-is-set-up-for-a-credential-manager: device-and-system-integration-guides/assa-abloy-visionline-access-control-system - device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/updating-guest-mobile-credentials: device-and-system-integration-guides/assa-abloy-visionline-access-control-system - device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/revoking-mobile-credentials: device-and-system-integration-guides/assa-abloy-visionline-access-control-system + device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/README.md + device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/issuing-various-types-of-guest-mobile-credentials: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/README.md + device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/issuing-various-types-of-guest-joiner-mobile-credentials: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/README.md + device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/retrieving-guest-and-common-entrances: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/README.md + device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/checking-if-a-user-identity-has-a-phone-that-is-set-up-for-a-credential-manager: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/README.md + device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/updating-guest-mobile-credentials: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/README.md + device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/credential-types/revoking-mobile-credentials: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/README.md device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/mobile-credential-related-properties: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/mobile-credential-related-properties.md device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/common-use-cases: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/common-use-cases.md device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/special-requirements-for-android-mobile-access-sdk-development: device-and-system-integration-guides/assa-abloy-visionline-access-control-system/special-requirements-for-android-mobile-access-sdk-development.md diff --git a/codegen/validate-redirects.ts b/codegen/validate-redirects.ts index 1eb14f3e0..f1e7075c9 100644 --- a/codegen/validate-redirects.ts +++ b/codegen/validate-redirects.ts @@ -1,4 +1,4 @@ -import { existsSync, readFileSync } from 'node:fs' +import { existsSync, readFileSync, statSync } from 'node:fs' import { join } from 'node:path' import YAML from 'yaml' @@ -20,8 +20,8 @@ const redirects: Redirect[] = Object.entries(gitbookConfig.redirects ?? {}).map( // Check if a path resolves to a page as a file, URL slug, or directory index. function pageExists(fullPath: string): boolean { - // Exact file match (e.g., quickstart.md) - if (existsSync(fullPath)) return true + // Exact file match (e.g., quickstart.md) — must be a file, not a directory + if (existsSync(fullPath) && statSync(fullPath).isFile()) return true // URL-style target without .md extension (e.g., api/devices → api/devices.md) if (!fullPath.endsWith('.md') && existsSync(fullPath + '.md')) return true // README.md targets resolve as directory URLs in GitBook, so check if the From 760dc8f86ce7421921c3c4f368ab33c389f620a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20Pr=C3=A9?= Date: Wed, 22 Apr 2026 11:55:46 -0700 Subject: [PATCH 09/11] chore: Remove unnecessary comments from redirect validator Co-Authored-By: Claude Opus 4.6 (1M context) --- codegen/validate-redirects.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/codegen/validate-redirects.ts b/codegen/validate-redirects.ts index f1e7075c9..dc4ac86ed 100644 --- a/codegen/validate-redirects.ts +++ b/codegen/validate-redirects.ts @@ -18,14 +18,9 @@ const redirects: Redirect[] = Object.entries(gitbookConfig.redirects ?? {}).map( ([source, target]) => ({ source, target }), ) -// Check if a path resolves to a page as a file, URL slug, or directory index. function pageExists(fullPath: string): boolean { - // Exact file match (e.g., quickstart.md) — must be a file, not a directory if (existsSync(fullPath) && statSync(fullPath).isFile()) return true - // URL-style target without .md extension (e.g., api/devices → api/devices.md) if (!fullPath.endsWith('.md') && existsSync(fullPath + '.md')) return true - // README.md targets resolve as directory URLs in GitBook, so check if the - // parent path resolves as a page (e.g., foo/README.md → foo.md). if (fullPath.endsWith('/README.md')) { const parentPath = fullPath.slice(0, -'/README.md'.length) if (existsSync(parentPath + '.md')) return true @@ -33,11 +28,7 @@ function pageExists(fullPath: string): boolean { return false } -// Resolve a redirect target to check it points to a real page. -// Targets may reference other site sections via URL prefix. function resolveTarget(target: string): boolean { - // Try matching against site sections by URL prefix (most specific first). - // The siteSections array is already ordered with more-specific prefixes first. for (const section of siteSections) { if (section.urlPrefix === '') continue @@ -48,7 +39,6 @@ function resolveTarget(target: string): boolean { } } - // Fall back to the default section (Guides, urlPrefix '') const guidesSection = siteSections.find((s) => s.urlPrefix === '') if (guidesSection == null) return false From d2f412a62fde7173a6779e2e52d94ff2dc3b9ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20Pr=C3=A9?= Date: Wed, 22 Apr 2026 11:57:05 -0700 Subject: [PATCH 10/11] chore: Remove unused .md extension fallback from redirect validator Co-Authored-By: Claude Opus 4.6 (1M context) --- codegen/validate-redirects.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/codegen/validate-redirects.ts b/codegen/validate-redirects.ts index dc4ac86ed..d4562934c 100644 --- a/codegen/validate-redirects.ts +++ b/codegen/validate-redirects.ts @@ -20,7 +20,6 @@ const redirects: Redirect[] = Object.entries(gitbookConfig.redirects ?? {}).map( function pageExists(fullPath: string): boolean { if (existsSync(fullPath) && statSync(fullPath).isFile()) return true - if (!fullPath.endsWith('.md') && existsSync(fullPath + '.md')) return true if (fullPath.endsWith('/README.md')) { const parentPath = fullPath.slice(0, -'/README.md'.length) if (existsSync(parentPath + '.md')) return true From 96d304805a208804b6feb88d06649c8679758c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20Pr=C3=A9?= Date: Wed, 22 Apr 2026 11:58:37 -0700 Subject: [PATCH 11/11] =?UTF-8?q?chore:=20Remove=20README.md=E2=86=92paren?= =?UTF-8?q?t.md=20fallback=20from=20redirect=20validator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- codegen/validate-redirects.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/codegen/validate-redirects.ts b/codegen/validate-redirects.ts index d4562934c..29fc51955 100644 --- a/codegen/validate-redirects.ts +++ b/codegen/validate-redirects.ts @@ -19,12 +19,7 @@ const redirects: Redirect[] = Object.entries(gitbookConfig.redirects ?? {}).map( ) function pageExists(fullPath: string): boolean { - if (existsSync(fullPath) && statSync(fullPath).isFile()) return true - if (fullPath.endsWith('/README.md')) { - const parentPath = fullPath.slice(0, -'/README.md'.length) - if (existsSync(parentPath + '.md')) return true - } - return false + return existsSync(fullPath) && statSync(fullPath).isFile() } function resolveTarget(target: string): boolean {