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/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" 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/