From 6bcf839a6b2ba26ecd3706a0b27fb5e1db3d60fe Mon Sep 17 00:00:00 2001 From: Iman Pouya Date: Fri, 9 Oct 2026 10:08:36 +0200 Subject: [PATCH 1/2] docs: fix broken and indirect links in learning/ (OPS-71) Links to the in-app quickstart and hosted quickstart pointed at site paths that do not exist (/sdk-auth/sdk-quickstart/, /quickstart/hosted-quickstart/), and usage limits sent readers to the How Bridge works overview instead of the React page. They are now relative .md links, which the docs sync rewrites to the React pages. Refs: OPS-71 --- learning/auth/ui/email-password.md | 2 +- learning/billing/limits/usage-limits.md | 2 +- learning/mechanisms.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/learning/auth/ui/email-password.md b/learning/auth/ui/email-password.md index 1874575..a029909 100644 --- a/learning/auth/ui/email-password.md +++ b/learning/auth/ui/email-password.md @@ -11,7 +11,7 @@ A complete login form with email/password fields. Handles multi-step auth flows > **Framework note:** in bridge-react, the magic link option is a link to a separate page (default `/auth/magic-link`, set with `magicLinkHref`), which `` serves with the [`MagicLink`](/auth/ui/magic-link/) component, rather than an inline step. -> **You may not need to write this page.** `` mounted at `/auth/*` already serves `/auth/login` with this form (see the [SDK auth quickstart](/sdk-auth/sdk-quickstart/)). Render `LoginForm` yourself only to take that page over: ` }} />`. +> **You may not need to write this page.** `` mounted at `/auth/*` already serves `/auth/login` with this form (see the [SDK auth quickstart](../../sdk-auth/sdk-quickstart.md)). Render `LoginForm` yourself only to take that page over: ` }} />`. **Usage:** diff --git a/learning/billing/limits/usage-limits.md b/learning/billing/limits/usage-limits.md index 4257fc6..4d05dc7 100644 --- a/learning/billing/limits/usage-limits.md +++ b/learning/billing/limits/usage-limits.md @@ -20,7 +20,7 @@ import { BridgeQuotaBanner } from '@nebulr-group/bridge-react'; ## Three ways to handle a limit -Pick the lowest level that does the job. Each is optional; level 0 is on without code. The rules behind them (where a limit is counted, counter or gauge) are in [How Bridge works](/mechanisms/). +Pick the lowest level that does the job. Each is optional; level 0 is on without code. The rules behind them (where a limit is counted, counter or gauge) are in [How Bridge works](../../mechanisms.md). | Level | What the page writes | What the user sees | |---|---|---| diff --git a/learning/mechanisms.md b/learning/mechanisms.md index 0537392..ce6008e 100644 --- a/learning/mechanisms.md +++ b/learning/mechanisms.md @@ -294,7 +294,7 @@ The derived tokens (hover, light, focus) follow `--bridge-primary` wherever you ## Where the details live -- Sign-in: [hosted quickstart](/quickstart/hosted-quickstart/), [in-app quickstart](/sdk-auth/sdk-quickstart/), [route guards](/auth/securing/route-guards/) +- Sign-in: [hosted quickstart](quickstart/hosted-quickstart.md), [in-app quickstart](sdk-auth/sdk-quickstart.md), [route guards](/auth/securing/route-guards/) - Plans and limits: [how billing works](/billing/how-it-works/), [usage limits](/billing/limits/usage-limits/), [lock features to a plan](/billing/limits/lock-features/), [report usage](/billing/limits/report-usage/) - Styling: [theming](/theming/) - The backend: the bridge-nestjs docs, "Plan limits" From 128854b39e148e31ddf0e80421603a83663e5ce0 Mon Sep 17 00:00:00 2001 From: Iman Pouya Date: Fri, 9 Oct 2026 10:10:06 +0200 Subject: [PATCH 2/2] ci: docs link check for learning/, on pull requests, before the docs sync and before push (OPS-71) scripts/check-docs-links.mjs (dependency-free node, about 0.05 s) checks every link in learning/ the way the docs sync publishes it to thebridge.dev/docs: relative .md links must point at an existing file and heading, and must be a form the sync rewrites to that file's own page; site paths like /auth/config/ must be real docs pages (scripts/docs-site-pages.txt, plus the pages this repo's own learning/ produces); #anchors must match a heading; two files must not publish to the same page. It runs in three places: - the Docs links workflow, on pull requests to main that touch learning/ or the checker (separate from build, test and publish); - the Sync Docs workflow, before the sync, so a broken link never reaches the website; - before push, via .githooks/pre-push, when a pushed commit touches learning/. Opt in per checkout with: git config workflows.localPrePush .githooks/pre-push Refs: OPS-71 --- .githooks/pre-push | 35 +++ .github/workflows/docs-links.yml | 26 ++ .github/workflows/sync-docs-only.yml | 2 + package.json | 1 + scripts/check-docs-links.mjs | 215 ++++++++++++++ scripts/docs-site-pages.txt | 412 +++++++++++++++++++++++++++ 6 files changed, 691 insertions(+) create mode 100755 .githooks/pre-push create mode 100644 .github/workflows/docs-links.yml create mode 100755 scripts/check-docs-links.mjs create mode 100644 scripts/docs-site-pages.txt diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..21159fa --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,35 @@ +#!/bin/sh +# Before-push docs link check (OPS-71). Runs from the shared pre-push in ~/Workflows/githooks once +# this repo opts in: git config workflows.localPrePush .githooks/pre-push +# (undo: git config --unset workflows.localPrePush) +# +# Runs only when a pushed commit touches learning/ (or the checker), and checks the commit being +# pushed, not the working tree: learning/ and scripts/ at that commit are exported to a temp dir. +# Dependency-free node, well under a second. Skip once with: SKIP_LINK_CHECK=1 git push +[ -n "${SKIP_LINK_CHECK:-}" ] && { cat >/dev/null; exit 0; } +command -v node >/dev/null 2>&1 || { cat >/dev/null; echo "docs link check: node not found, skipped" >&2; exit 0; } +ZERO=0000000000000000000000000000000000000000 +fail=0 +while read -r local_ref local_sha remote_ref remote_sha; do + [ -z "$local_sha" ] || [ "$local_sha" = "$ZERO" ] && continue + if [ "$remote_sha" = "$ZERO" ] || ! git cat-file -e "$remote_sha^{commit}" 2>/dev/null; then + touched=$(git log --format= --name-only "$local_sha" --not --remotes -- learning scripts/check-docs-links.mjs scripts/docs-site-pages.txt) + else + touched=$(git diff --name-only "$remote_sha" "$local_sha" -- learning scripts/check-docs-links.mjs scripts/docs-site-pages.txt) + fi + [ -n "$touched" ] || continue + git cat-file -e "$local_sha:scripts/check-docs-links.mjs" 2>/dev/null || continue + tmp=$(mktemp -d) || continue + git archive "$local_sha" learning scripts/check-docs-links.mjs scripts/docs-site-pages.txt 2>/dev/null | tar -x -C "$tmp" 2>/dev/null + out=$(cd "$tmp" && node scripts/check-docs-links.mjs 2>&1) + rc=$? + rm -rf "$tmp" + if [ $rc -eq 1 ]; then + printf '%s\n' "$out" >&2 + echo "docs link check: push of $local_ref refused, fix the links above (or skip once: SKIP_LINK_CHECK=1 git push)" >&2 + fail=1 + else + printf '%s\n' "$out" | tail -1 | sed "s|^|docs link check ($local_ref): |" >&2 + fi +done +exit $fail diff --git a/.github/workflows/docs-links.yml b/.github/workflows/docs-links.yml new file mode 100644 index 0000000..b712a39 --- /dev/null +++ b/.github/workflows/docs-links.yml @@ -0,0 +1,26 @@ +# Docs link check (OPS-71): every link in learning/ must work on thebridge.dev/docs as the docs +# sync publishes it. Dependency-free node script, under a second. Run locally: +# node scripts/check-docs-links.mjs +name: Docs links + +on: + pull_request: + branches: [main] + paths: + - "learning/**" + - "scripts/check-docs-links.mjs" + - "scripts/docs-site-pages.txt" + - ".github/workflows/docs-links.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + docs-links: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - name: Every learning/ link resolves the way the docs sync publishes it + run: node scripts/check-docs-links.mjs diff --git a/.github/workflows/sync-docs-only.yml b/.github/workflows/sync-docs-only.yml index ecbec08..2a6783f 100644 --- a/.github/workflows/sync-docs-only.yml +++ b/.github/workflows/sync-docs-only.yml @@ -22,6 +22,8 @@ jobs: steps: - name: ⬇️ Checkout repo uses: actions/checkout@v4 + - name: 🔗 Check docs links (a broken link stops the sync, OPS-71) + run: node scripts/check-docs-links.mjs - name: 🔄 Sync Docs uses: ./.github/actions/sync-docs env: diff --git a/package.json b/package.json index f5896c3..bdec095 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "author": "Iman Pouya", "workspaces": ["bridge-react", "demo"], "scripts": { + "check:docs-links": "node scripts/check-docs-links.mjs", "dev": "bun run --filter demo dev", "build": "bun run --filter @nebulr-group/bridge-react build", "build:watch": "bun run --filter @nebulr-group/bridge-react build:watch", diff --git a/scripts/check-docs-links.mjs b/scripts/check-docs-links.mjs new file mode 100755 index 0000000..0011cc5 --- /dev/null +++ b/scripts/check-docs-links.mjs @@ -0,0 +1,215 @@ +#!/usr/bin/env node +// Docs link check for learning/ (OPS-71). Dependency-free; runs in well under a second. +// +// learning/ is copied to thebridge.dev/docs by the docs sync (.github/actions/sync-docs, then +// bridge-web's process-synced-docs.ts). A link that works on GitHub can still 404 on the site, so +// this checks every markdown link the way the sync will publish it: +// +// relative .md links the file must exist, any #anchor must match a heading in it, and the +// sync must be able to rewrite the link to that file's own site page. +// The sync rewrites `[..](/.md)` (any number of leading ../) +// and `[..](.md)`; it cannot rewrite a link that names two or more +// folders, so a page nested two folders deep is linked by its site path. +// other relative links the sync publishes .md pages only, so a link to any other file 404s. +// site paths `/
/...` (or `/docs/
/...`) must be a real docs page: +// listed in scripts/docs-site-pages.txt, or a page this repo's own +// learning/ produces. +// #anchors on the same page must match a heading. +// README.md files are not published and not checked. External links (http:, mailto:, ...) are +// not checked either. Code blocks and inline code are skipped. +// +// How the sync maps files to pages (keep in step with bridge-web process-synced-docs.ts): +// mechanisms.md -> /mechanisms// +// auth/auth.md, auth/anything.md -> /auth// +// auth/ui/passkeys.md -> /auth/ui/passkeys// +// auth/logout/logout.md -> /auth/logout// +// and every page also gets a section index at //. +// +// Usage: +// node scripts/check-docs-links.mjs [learning-dir] +// node scripts/check-docs-links.mjs --print-site-pages /bridge-docs/src/content/docs +// prints a fresh scripts/docs-site-pages.txt from the site's docs content. +// Exits 1 when a link is broken. +import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs'; +import { dirname, join, posix, relative, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const args = process.argv.slice(2); + +function walk(dir, skipReadme = false, out = []) { + for (const e of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, e.name); + if (e.isDirectory()) walk(p, skipReadme, out); + else if (/\.mdx?$/.test(e.name) && !(skipReadme && e.name === 'README.md')) out.push(p); + } + return out; +} +const toPosix = (p) => p.split(sep).join('/'); + +if (args[0] === '--print-site-pages') { + const root = resolve(args[1] || ''); + if (!args[1] || !existsSync(root)) { + console.error('usage: --print-site-pages /bridge-docs/src/content/docs'); + process.exit(2); + } + const pages = walk(root) + .map((f) => toPosix(relative(root, f)).replace(/\.mdx?$/, '')) + .map((r) => (r === 'index' ? '/' : '/' + r.replace(/\/index$/, '') + '/')) + .sort(); + console.log('# Real thebridge.dev/docs pages (paths below /docs), for scripts/check-docs-links.mjs.'); + console.log('# Regenerate: node scripts/check-docs-links.mjs --print-site-pages /bridge-docs/src/content/docs'); + for (const p of pages) console.log(p); + process.exit(0); +} + +const LEARNING = resolve(args[0] || join(HERE, '..', 'learning')); +if (!existsSync(LEARNING)) { + console.error(`check-docs-links: no docs folder at ${LEARNING}`); + process.exit(2); +} + +// The page a learning file is published as (see the table above). +function topicOf(rel) { + const parts = rel.replace(/\.mdx?$/, '').split('/'); + const base = parts.pop(); + if (parts.length === 0) return base; + if (parts.length === 1) return parts[0]; + return parts[parts.length - 1] === base ? parts.join('/') : [...parts, base].join('/'); +} + +// Blank out what is not prose, keeping line numbers: fenced code and HTML comments, and (unless +// keepInline) inline code. +function prose(text, keepInline = false) { + const blank = (m) => m.replace(/[^\n]/g, ' '); + const out = text + .replace(/^( {0,3})(```|~~~)[^\n]*\n[\s\S]*?(\n {0,3}\2[^\n]*|$)/gm, blank) + .replace(//g, blank); + return keepInline ? out : out.replace(/(`+)[^`\n][\s\S]*?\1/g, blank); +} + +// Heading ids as the site makes them (github-slugger, numbered on repeats). +function anchorsOf(text) { + const ids = new Set(); + const counts = new Map(); + for (const [, raw] of prose(text, true).matchAll(/^ {0,3}#{1,6}[ \t]+(.+?)[ \t#]*$/gm)) { + const slug = raw + .replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1') + .replace(/<[^>]+>/g, '') + .replace(/[*`]/g, '') + .toLowerCase() + .replace(/[^\p{L}\p{M}\p{N}\p{Pc} -]/gu, '') + .replace(/ /g, '-'); + const n = counts.get(slug) || 0; + counts.set(slug, n + 1); + ids.add(n ? `${slug}-${n}` : slug); + } + return ids; +} + +// README.md files are for GitHub: the sync leaves them out (rsync --exclude README.md). +const files = walk(LEARNING, true).map((f) => toPosix(relative(LEARNING, f))); +const fileSet = new Set(files); +const topics = new Set(files.map(topicOf)); +const topLevel = new Set(files.filter((f) => !f.includes('/')).map(topicOf)); +const texts = new Map(files.map((f) => [f, readFileSync(join(LEARNING, f), 'utf8')])); +const anchorCache = new Map(); +const anchors = (f) => { + if (!anchorCache.has(f)) anchorCache.set(f, anchorsOf(texts.get(f))); + return anchorCache.get(f); +}; + +const sitePages = new Set(['/']); +const PAGES_FILE = join(HERE, 'docs-site-pages.txt'); +if (existsSync(PAGES_FILE)) { + for (const l of readFileSync(PAGES_FILE, 'utf8').split('\n')) { + const p = l.replace(/#.*/, '').trim(); + if (p) sitePages.add(p); + } +} +for (const t of topics) sitePages.add(`/${t}/`); + +// The sync's own rewrite (process-synced-docs.ts rewriteCrossDocLinks), applied to one target. +const SYNC_LINK = /^(?:\.\/)?(?:\.\.\/)*(?:([\w-]+)\/)?([\w-]+)\.mdx?(#.*)?$/; +function syncTopic(target, fromTopic) { + const m = target.match(SYNC_LINK); + if (!m) return null; + const [, dir, base] = m; + const topic = dir ?? (topLevel.has(base) ? base : fromTopic); + return topics.has(topic) ? topic : null; +} + +// What to write instead, for a link to learning file `to` from learning file `from`. +function suggest(from, to) { + const depth = to.split('/').length - 1; + if (depth >= 2) return `/${topicOf(to)}/`; + return posix.relative(posix.dirname(from), to); +} + +// Two files that publish to the same page: the sync keeps one and drops the other. +const byTopic = new Map(); +for (const f of files) byTopic.set(topicOf(f), [...(byTopic.get(topicOf(f)) || []), f]); +const collisions = [...byTopic].filter(([, fs]) => fs.length > 1); + +const LINK = /(!?)\[(?:[^\][]|\[[^\]]*\])*\]\(\s*]+)>?(?:\s+["'(][^)]*)?\)|^ {0,3}\[[^\]]+\]:\s*?(?:\s|$)/gm; +const problems = collisions.map(([t, fs]) => ({ + at: fs.join(', '), + target: `/${t}/`, + why: 'these files publish to the same site page; the docs sync keeps only one of them', +})); +let checked = 0; +for (const from of files) { + const text = prose(texts.get(from)); + const fromTopic = topicOf(from); + for (const m of text.matchAll(LINK)) { + const target = m[2] ?? m[3]; + if (!target || /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith('//')) continue; + checked++; + const line = text.slice(0, m.index).split('\n').length; + const report = (why) => problems.push({ at: `${from}:${line}`, target, why }); + const hash = target.indexOf('#'); + const path = hash >= 0 ? target.slice(0, hash) : target; + const anchor = hash >= 0 ? decodeURIComponent(target.slice(hash + 1)) : ''; + + if (path === '') { + if (anchor && !anchors(from).has(anchor)) report(`no heading "#${anchor}" on this page`); + continue; + } + if (path.startsWith('/')) { + let p = path.replace(/^\/docs(?=\/|$)/, '').split('?')[0] || '/'; + if (!p.endsWith('/')) p += '/'; + if (!sitePages.has(p)) + report(`not a docs page on the site (/docs${p}); link the page's .md file, or a real section`); + continue; + } + const to = posix.normalize(posix.join(posix.dirname(from), path)); + if (to.startsWith('../')) { + report('points outside learning/, which the sync does not publish'); + continue; + } + if (!/\.mdx?$/.test(to)) { + const isDir = existsSync(join(LEARNING, to)) && statSync(join(LEARNING, to)).isDirectory(); + report(isDir ? 'links a folder; link its .md file' : 'the docs sync publishes .md pages only; this file is not served'); + continue; + } + if (!fileSet.has(to)) { + report(`no such file (learning/${to})`); + continue; + } + if (anchor && !anchors(to).has(anchor)) report(`no heading "#${anchor}" in learning/${to}`); + const lands = syncTopic(target, fromTopic); + const want = topicOf(to); + if (lands === null) report(`the docs sync cannot rewrite this link, so it 404s on the site; use ${suggest(from, to)}`); + else if (lands !== want) + report(`the docs sync sends this to /${lands}/ instead of /${want}/; use ${suggest(from, to)}`); + } +} + +console.log(`check-docs-links: ${checked} links in ${files.length} docs under ${toPosix(relative(process.cwd(), LEARNING)) || '.'}`); +if (!problems.length) { + console.log('no broken docs links'); + process.exit(0); +} +console.log(`\nBROKEN: ${problems.length} link(s)`); +for (const p of problems) console.log(` ${p.at} (${p.target})\n ${p.why}`); +process.exit(1); diff --git a/scripts/docs-site-pages.txt b/scripts/docs-site-pages.txt new file mode 100644 index 0000000..110fad7 --- /dev/null +++ b/scripts/docs-site-pages.txt @@ -0,0 +1,412 @@ +# Real thebridge.dev/docs pages (paths below /docs), for scripts/check-docs-links.mjs. +# Regenerate: node scripts/check-docs-links.mjs --print-site-pages /bridge-docs/src/content/docs +/ +/ai-assistants/mcp/ +/api-reference/ +/api-reference/access/ +/api-reference/api-tokens/ +/api-reference/app-profile/ +/api-reference/auth-flows/ +/api-reference/authentication/ +/api-reference/branding/ +/api-reference/errors/ +/api-reference/event-log/ +/api-reference/feature-flags/ +/api-reference/federation/ +/api-reference/import/ +/api-reference/models/ +/api-reference/payments/ +/api-reference/subscriptions/ +/api-reference/tenants/ +/api-reference/usage-quotas/ +/api-reference/users/ +/api-reference/webhooks/ +/auth/ +/auth/angular/ +/auth/api-tokens/ +/auth/api-tokens/angular/ +/auth/api-tokens/express/ +/auth/api-tokens/nestjs/ +/auth/api-tokens/nextjs/ +/auth/api-tokens/react/ +/auth/api-tokens/svelte/ +/auth/config/ +/auth/config/angular/ +/auth/config/express/ +/auth/config/nestjs/ +/auth/config/nextjs/ +/auth/config/react/ +/auth/config/svelte/ +/auth/express/ +/auth/multi-tenancy/ +/auth/multi-tenancy/angular/ +/auth/multi-tenancy/express/ +/auth/multi-tenancy/nestjs/ +/auth/multi-tenancy/nextjs/ +/auth/multi-tenancy/react/ +/auth/multi-tenancy/svelte/ +/auth/nestjs/ +/auth/nextjs/ +/auth/react/ +/auth/roles/assign-roles/ +/auth/roles/assign-roles/angular/ +/auth/roles/assign-roles/express/ +/auth/roles/assign-roles/nestjs/ +/auth/roles/assign-roles/nextjs/ +/auth/roles/assign-roles/react/ +/auth/roles/assign-roles/svelte/ +/auth/roles/common-setups/ +/auth/roles/common-setups/angular/ +/auth/roles/common-setups/express/ +/auth/roles/common-setups/nestjs/ +/auth/roles/common-setups/nextjs/ +/auth/roles/common-setups/react/ +/auth/roles/common-setups/svelte/ +/auth/roles/define-roles/ +/auth/roles/define-roles/angular/ +/auth/roles/define-roles/express/ +/auth/roles/define-roles/nestjs/ +/auth/roles/define-roles/nextjs/ +/auth/roles/define-roles/react/ +/auth/roles/define-roles/svelte/ +/auth/roles/gate-with-flags/ +/auth/roles/gate-with-flags/angular/ +/auth/roles/gate-with-flags/express/ +/auth/roles/gate-with-flags/nestjs/ +/auth/roles/gate-with-flags/nextjs/ +/auth/roles/gate-with-flags/react/ +/auth/roles/gate-with-flags/svelte/ +/auth/roles/how-it-works/ +/auth/roles/how-it-works/angular/ +/auth/roles/how-it-works/express/ +/auth/roles/how-it-works/nestjs/ +/auth/roles/how-it-works/nextjs/ +/auth/roles/how-it-works/react/ +/auth/roles/how-it-works/svelte/ +/auth/roles/owner-role/ +/auth/roles/owner-role/angular/ +/auth/roles/owner-role/express/ +/auth/roles/owner-role/nestjs/ +/auth/roles/owner-role/nextjs/ +/auth/roles/owner-role/react/ +/auth/roles/owner-role/svelte/ +/auth/securing/route-guards/ +/auth/securing/route-guards/angular/ +/auth/securing/route-guards/express/ +/auth/securing/route-guards/nestjs/ +/auth/securing/route-guards/nextjs/ +/auth/securing/route-guards/react/ +/auth/securing/route-guards/svelte/ +/auth/sign-in/azure-ad/ +/auth/sign-in/azure-ad/angular/ +/auth/sign-in/azure-ad/nextjs/ +/auth/sign-in/azure-ad/react/ +/auth/sign-in/azure-ad/svelte/ +/auth/sign-in/email-password/ +/auth/sign-in/email-password/angular/ +/auth/sign-in/email-password/nextjs/ +/auth/sign-in/email-password/react/ +/auth/sign-in/email-password/svelte/ +/auth/sign-in/google-sso/ +/auth/sign-in/google-sso/angular/ +/auth/sign-in/google-sso/nextjs/ +/auth/sign-in/google-sso/react/ +/auth/sign-in/google-sso/svelte/ +/auth/sign-in/magic-link/ +/auth/sign-in/magic-link/angular/ +/auth/sign-in/magic-link/nextjs/ +/auth/sign-in/magic-link/react/ +/auth/sign-in/magic-link/svelte/ +/auth/sign-in/mfa/ +/auth/sign-in/mfa/angular/ +/auth/sign-in/mfa/nextjs/ +/auth/sign-in/mfa/react/ +/auth/sign-in/mfa/svelte/ +/auth/sign-in/passkeys/ +/auth/sign-in/passkeys/angular/ +/auth/sign-in/passkeys/nextjs/ +/auth/sign-in/passkeys/react/ +/auth/sign-in/passkeys/svelte/ +/auth/svelte/ +/auth/ui/email-password/ +/auth/ui/email-password/angular/ +/auth/ui/email-password/nextjs/ +/auth/ui/email-password/react/ +/auth/ui/email-password/svelte/ +/auth/ui/forgot-password/ +/auth/ui/forgot-password/angular/ +/auth/ui/forgot-password/nextjs/ +/auth/ui/forgot-password/react/ +/auth/ui/forgot-password/svelte/ +/auth/ui/google-sso/ +/auth/ui/google-sso/angular/ +/auth/ui/google-sso/nextjs/ +/auth/ui/google-sso/react/ +/auth/ui/google-sso/svelte/ +/auth/ui/magic-link/ +/auth/ui/magic-link/angular/ +/auth/ui/magic-link/nextjs/ +/auth/ui/magic-link/react/ +/auth/ui/magic-link/svelte/ +/auth/ui/mfa/ +/auth/ui/mfa/angular/ +/auth/ui/mfa/nextjs/ +/auth/ui/mfa/react/ +/auth/ui/mfa/svelte/ +/auth/ui/passkeys/ +/auth/ui/passkeys/angular/ +/auth/ui/passkeys/nextjs/ +/auth/ui/passkeys/react/ +/auth/ui/passkeys/svelte/ +/auth/ui/signup/ +/auth/ui/signup/angular/ +/auth/ui/signup/nextjs/ +/auth/ui/signup/react/ +/auth/ui/signup/svelte/ +/auth/ui/switching-workspaces/ +/auth/ui/switching-workspaces/angular/ +/auth/ui/switching-workspaces/nextjs/ +/auth/ui/switching-workspaces/react/ +/auth/ui/switching-workspaces/svelte/ +/auth/ui/team-management/ +/auth/ui/team-management/angular/ +/auth/ui/team-management/nextjs/ +/auth/ui/team-management/react/ +/auth/ui/team-management/svelte/ +/auth/ui/tokens/ +/auth/ui/tokens/angular/ +/auth/ui/tokens/nextjs/ +/auth/ui/tokens/react/ +/auth/ui/tokens/svelte/ +/auth/user-token/auth-states/ +/auth/user-token/auth-states/angular/ +/auth/user-token/auth-states/express/ +/auth/user-token/auth-states/nextjs/ +/auth/user-token/auth-states/react/ +/auth/user-token/auth-states/svelte/ +/auth/user-token/getting-the-token/ +/auth/user-token/getting-the-token/angular/ +/auth/user-token/getting-the-token/express/ +/auth/user-token/getting-the-token/nestjs/ +/auth/user-token/getting-the-token/nextjs/ +/auth/user-token/getting-the-token/react/ +/auth/user-token/getting-the-token/svelte/ +/auth/user-token/logging-in-and-out/ +/auth/user-token/logging-in-and-out/angular/ +/auth/user-token/logging-in-and-out/express/ +/auth/user-token/logging-in-and-out/nestjs/ +/auth/user-token/logging-in-and-out/nextjs/ +/auth/user-token/logging-in-and-out/react/ +/auth/user-token/logging-in-and-out/svelte/ +/auth/user-token/object-updates/ +/auth/user-token/object-updates/angular/ +/auth/user-token/object-updates/express/ +/auth/user-token/object-updates/nestjs/ +/auth/user-token/object-updates/nextjs/ +/auth/user-token/object-updates/react/ +/auth/user-token/object-updates/svelte/ +/billing/advanced/backend-checks/ +/billing/advanced/backend-checks/angular/ +/billing/advanced/backend-checks/nextjs/ +/billing/advanced/backend-checks/react/ +/billing/advanced/backend-checks/svelte/ +/billing/advanced/standalone/ +/billing/advanced/standalone/angular/ +/billing/advanced/standalone/nextjs/ +/billing/advanced/standalone/react/ +/billing/advanced/standalone/svelte/ +/billing/how-it-works/ +/billing/how-it-works/angular/ +/billing/how-it-works/nextjs/ +/billing/how-it-works/react/ +/billing/how-it-works/svelte/ +/billing/lifecycle/billing-events/ +/billing/lifecycle/billing-events/angular/ +/billing/lifecycle/billing-events/nextjs/ +/billing/lifecycle/billing-events/react/ +/billing/lifecycle/billing-events/svelte/ +/billing/lifecycle/billing-portal/ +/billing/lifecycle/billing-portal/angular/ +/billing/lifecycle/billing-portal/nextjs/ +/billing/lifecycle/billing-portal/react/ +/billing/lifecycle/billing-portal/svelte/ +/billing/lifecycle/failed-payments/ +/billing/lifecycle/failed-payments/angular/ +/billing/lifecycle/failed-payments/nextjs/ +/billing/lifecycle/failed-payments/react/ +/billing/lifecycle/failed-payments/svelte/ +/billing/lifecycle/free-trials/ +/billing/lifecycle/free-trials/angular/ +/billing/lifecycle/free-trials/nextjs/ +/billing/lifecycle/free-trials/react/ +/billing/lifecycle/free-trials/svelte/ +/billing/limits/lock-features/ +/billing/limits/lock-features/angular/ +/billing/limits/lock-features/nextjs/ +/billing/limits/lock-features/react/ +/billing/limits/lock-features/svelte/ +/billing/limits/report-usage/ +/billing/limits/report-usage/angular/ +/billing/limits/report-usage/nextjs/ +/billing/limits/report-usage/react/ +/billing/limits/report-usage/svelte/ +/billing/limits/usage-limits/ +/billing/limits/usage-limits/angular/ +/billing/limits/usage-limits/nextjs/ +/billing/limits/usage-limits/react/ +/billing/limits/usage-limits/svelte/ +/billing/onboarding/choose-switch-plans/ +/billing/onboarding/choose-switch-plans/angular/ +/billing/onboarding/choose-switch-plans/nextjs/ +/billing/onboarding/choose-switch-plans/react/ +/billing/onboarding/choose-switch-plans/svelte/ +/billing/onboarding/require-plan/ +/billing/onboarding/require-plan/angular/ +/billing/onboarding/require-plan/nextjs/ +/billing/onboarding/require-plan/react/ +/billing/onboarding/require-plan/svelte/ +/billing/setup/add-billing-to-your-app/ +/billing/setup/add-billing-to-your-app/angular/ +/billing/setup/add-billing-to-your-app/nextjs/ +/billing/setup/add-billing-to-your-app/react/ +/billing/setup/add-billing-to-your-app/svelte/ +/billing/setup/connect-stripe/ +/billing/setup/connect-stripe/angular/ +/billing/setup/connect-stripe/nextjs/ +/billing/setup/connect-stripe/react/ +/billing/setup/connect-stripe/svelte/ +/billing/setup/define-plans/ +/billing/setup/define-plans/angular/ +/billing/setup/define-plans/nextjs/ +/billing/setup/define-plans/react/ +/billing/setup/define-plans/svelte/ +/billing/status/billing-notices/ +/billing/status/billing-notices/angular/ +/billing/status/billing-notices/nextjs/ +/billing/status/billing-notices/react/ +/billing/status/billing-notices/svelte/ +/billing/status/subscription-status/ +/billing/status/subscription-status/angular/ +/billing/status/subscription-status/nextjs/ +/billing/status/subscription-status/react/ +/billing/status/subscription-status/svelte/ +/branding/ +/branding/angular/ +/branding/nextjs/ +/branding/react/ +/branding/svelte/ +/bridge-service/ +/bridge-service/express/ +/bridge-service/nestjs/ +/configuration/ +/configuration/express/ +/configuration/nestjs/ +/error-handling/ +/error-handling/express/ +/error-handling/nestjs/ +/examples/ +/examples/express/ +/examples/nestjs/ +/feature-flags/ +/feature-flags/angular/ +/feature-flags/express/ +/feature-flags/get-started/ +/feature-flags/get-started/angular/ +/feature-flags/get-started/express/ +/feature-flags/get-started/nestjs/ +/feature-flags/get-started/nextjs/ +/feature-flags/get-started/react/ +/feature-flags/get-started/svelte/ +/feature-flags/how-it-works/ +/feature-flags/how-it-works/angular/ +/feature-flags/how-it-works/express/ +/feature-flags/how-it-works/nestjs/ +/feature-flags/how-it-works/nextjs/ +/feature-flags/how-it-works/react/ +/feature-flags/how-it-works/svelte/ +/feature-flags/nestjs/ +/feature-flags/nextjs/ +/feature-flags/observability/ +/feature-flags/patterns/ +/feature-flags/quickstart/ +/feature-flags/react/ +/feature-flags/svelte/ +/feature-flags/targeting/ +/feature-flags/targeting/anonymous/ +/feature-flags/targeting/anonymous/angular/ +/feature-flags/targeting/anonymous/nestjs/ +/feature-flags/targeting/anonymous/nextjs/ +/feature-flags/targeting/anonymous/react/ +/feature-flags/targeting/anonymous/svelte/ +/feature-flags/targeting/by-plan-or-role/ +/feature-flags/targeting/by-plan-or-role/angular/ +/feature-flags/targeting/by-plan-or-role/express/ +/feature-flags/targeting/by-plan-or-role/nestjs/ +/feature-flags/targeting/by-plan-or-role/nextjs/ +/feature-flags/targeting/by-plan-or-role/react/ +/feature-flags/targeting/by-plan-or-role/svelte/ +/feature-flags/targeting/send-context/ +/feature-flags/targeting/send-context/angular/ +/feature-flags/targeting/send-context/express/ +/feature-flags/targeting/send-context/nestjs/ +/feature-flags/targeting/send-context/nextjs/ +/feature-flags/targeting/send-context/react/ +/feature-flags/targeting/send-context/svelte/ +/feature-flags/upgrade-to-platform/ +/feature-flags/using/backend/ +/feature-flags/using/backend/angular/ +/feature-flags/using/backend/express/ +/feature-flags/using/backend/nestjs/ +/feature-flags/using/backend/nextjs/ +/feature-flags/using/backend/react/ +/feature-flags/using/backend/svelte/ +/feature-flags/using/guard-routes/ +/feature-flags/using/guard-routes/angular/ +/feature-flags/using/guard-routes/express/ +/feature-flags/using/guard-routes/nestjs/ +/feature-flags/using/guard-routes/nextjs/ +/feature-flags/using/guard-routes/react/ +/feature-flags/using/guard-routes/svelte/ +/feature-flags/using/in-logic/ +/feature-flags/using/in-logic/angular/ +/feature-flags/using/in-logic/express/ +/feature-flags/using/in-logic/nestjs/ +/feature-flags/using/in-logic/nextjs/ +/feature-flags/using/in-logic/react/ +/feature-flags/using/in-logic/svelte/ +/feature-flags/using/show-hide-ui/ +/feature-flags/using/show-hide-ui/angular/ +/feature-flags/using/show-hide-ui/nextjs/ +/feature-flags/using/show-hide-ui/react/ +/feature-flags/using/show-hide-ui/svelte/ +/frontend-integration/ +/frontend-integration/express/ +/frontend-integration/nestjs/ +/getting-started/ +/live-updates/ +/live-updates/angular/ +/live-updates/nextjs/ +/live-updates/react/ +/live-updates/svelte/ +/live-updates/troubleshooting/ +/migration/nblocks-to-bridge/ +/multi-tenancy/ +/multi-tenancy/express/ +/multi-tenancy/nestjs/ +/quickstart/ +/quickstart/angular/ +/quickstart/express/ +/quickstart/nestjs/ +/quickstart/nextjs/ +/quickstart/react/ +/quickstart/svelte/ +/sdk-auth/ +/sdk-auth/angular/ +/sdk-auth/nextjs/ +/sdk-auth/react/ +/sdk-auth/svelte/ +/theming/ +/theming/angular/ +/theming/nextjs/ +/theming/react/ +/theming/svelte/