diff --git a/app/[[...path]]/page.tsx b/app/[[...path]]/page.tsx index a3df97f0c781f..63da6193e8860 100644 --- a/app/[[...path]]/page.tsx +++ b/app/[[...path]]/page.tsx @@ -11,8 +11,11 @@ import {Home} from 'sentry-docs/components/home'; import {Include} from 'sentry-docs/components/include'; import {PlatformContent} from 'sentry-docs/components/platformContent'; import { + DocNode, getCurrentPlatformOrGuide, getDocsRootNode, + getNextNode, + getPreviousNode, nodeForPath, } from 'sentry-docs/docTree'; import {isDeveloperDocs} from 'sentry-docs/isDeveloperDocs'; @@ -24,6 +27,7 @@ import { } from 'sentry-docs/mdx'; import {mdxComponents} from 'sentry-docs/mdxComponents'; import {setServerContext} from 'sentry-docs/serverContext'; +import {PaginationNavNode} from 'sentry-docs/types/paginationNavNode'; import {stripVersion} from 'sentry-docs/versioning'; export async function generateStaticParams() { @@ -42,7 +46,11 @@ export const dynamic = 'force-static'; const mdxComponentsWithWrapper = mdxComponents( {Include, PlatformContent}, - ({children, frontMatter}) => {children} + ({children, frontMatter, nextPage, previousPage}) => ( + + {children} + + ) ); function MDXLayoutRenderer({mdxSource, ...rest}) { @@ -59,6 +67,42 @@ export default async function Page({params}: {params: {path?: string[]}}) { path: params.path ?? [], }); + if (!params.path && !isDeveloperDocs) { + return ; + } + + const pageNode = nodeForPath(rootNode, params.path ?? ''); + + if (!pageNode) { + // eslint-disable-next-line no-console + console.warn('no page node', params.path); + return notFound(); + } + + // gather previous and next page that will be displayed in the bottom pagination + const getPaginationDetails = ( + getNode: (node: DocNode) => DocNode | undefined | 'root', + page: PaginationNavNode | undefined + ) => { + if (page && 'path' in page && 'title' in page) { + return page; + } + + const node = getNode(pageNode); + + if (node === 'root') { + return {path: '', title: 'Welcome to Sentry'}; + } + + return node ? {path: node.path, title: node.frontmatter.title} : undefined; + }; + + const previousPage = getPaginationDetails( + getPreviousNode, + pageNode?.frontmatter?.previousPage + ); + const nextPage = getPaginationDetails(getNextNode, pageNode?.frontmatter?.nextPage); + if (isDeveloperDocs) { // get the MDX for the current doc and render it let doc: Awaited> | null = null; @@ -74,13 +118,17 @@ export default async function Page({params}: {params: {path?: string[]}}) { } const {mdxSource, frontMatter} = doc; // pass frontmatter tree into sidebar, rendered page + fm into middle, headers into toc - return ; - } - if (!params.path) { - return ; + return ( + + ); } - if (params.path[0] === 'api' && params.path.length > 1) { + if (params.path?.[0] === 'api' && params.path.length > 1) { const categories = await apiCategories(); const category = categories.find(c => c.slug === params?.path?.[1]); if (category) { @@ -94,14 +142,6 @@ export default async function Page({params}: {params: {path?: string[]}}) { } } - const pageNode = nodeForPath(rootNode, params.path); - - if (!pageNode) { - // eslint-disable-next-line no-console - console.warn('no page node', params.path); - return notFound(); - } - // get the MDX for the current doc and render it let doc: Awaited> | null = null; try { @@ -122,7 +162,12 @@ export default async function Page({params}: {params: {path?: string[]}}) { // pass frontmatter tree into sidebar, rendered page + fm into middle, headers into toc. return ( - + ); } diff --git a/develop-docs/application/index.mdx b/develop-docs/application/index.mdx index 49613ad5a6e59..6a71b4d9473c5 100644 --- a/develop-docs/application/index.mdx +++ b/develop-docs/application/index.mdx @@ -1,5 +1,6 @@ --- title: Application +sidebar_order: 30 --- diff --git a/develop-docs/development/environment/index.mdx b/develop-docs/development/environment/index.mdx index 919eaeeac5fcf..449f77a5afc06 100644 --- a/develop-docs/development/environment/index.mdx +++ b/develop-docs/development/environment/index.mdx @@ -1,7 +1,7 @@ --- title: Environment description: This guide steps you through configuring a local development environment for the Sentry server on macOS and Linux. -sidebar_order: 1 +sidebar_order: 2 --- If you're using diff --git a/develop-docs/integrations/index.mdx b/develop-docs/integrations/index.mdx index 6dc434f2599e2..91fff36c5fb52 100644 --- a/develop-docs/integrations/index.mdx +++ b/develop-docs/integrations/index.mdx @@ -1,6 +1,6 @@ --- title: Developing Integrations -sidebar_order: 80 +sidebar_order: 90 --- diff --git a/develop-docs/relay/index.mdx b/develop-docs/relay/index.mdx index fddf4b8eaaf05..e5a08ce7a094c 100644 --- a/develop-docs/relay/index.mdx +++ b/develop-docs/relay/index.mdx @@ -1,6 +1,6 @@ --- title: Relay Development -sidebar_order: 60 +sidebar_order: 70 --- Relay is a service for event filtering, rate-limiting and processing. It can act as: diff --git a/develop-docs/sdk/index.mdx b/develop-docs/sdk/index.mdx index 348cd7a0fc432..ca0e378a5761d 100644 --- a/develop-docs/sdk/index.mdx +++ b/develop-docs/sdk/index.mdx @@ -1,6 +1,6 @@ --- title: SDK Development -sidebar_order: 70 +sidebar_order: 60 --- The following is a guide for implementing a Sentry SDK. diff --git a/develop-docs/self-hosted/index.mdx b/develop-docs/self-hosted/index.mdx index 71cd7db789278..ce425c4ad6b15 100644 --- a/develop-docs/self-hosted/index.mdx +++ b/develop-docs/self-hosted/index.mdx @@ -1,6 +1,6 @@ --- title: Self-Hosted Sentry -sidebar_order: 30 +sidebar_order: 100 --- In addition to making its source code available publicly, Sentry offers and maintains a minimal setup that works out-of-the-box for simple use cases. This version comes with no guarantees or dedicated support. Sentry engineers will do their best to answer questions and are dedicated to making sure self-hosted is running, but that's where our involvement ends. For anything else, we expect users to rely on the [Sentry Self-Hosted community](https://discord.gg/sentry) on Discord. The self-hosted repository should serve as a blueprint for how various Sentry services connect for a complete setup. This will be useful for folks willing to maintain larger installations with custom infrastructure. diff --git a/develop-docs/self-hosted/releases.mdx b/develop-docs/self-hosted/releases.mdx index bc2bfc945e62a..3bf93c03adee9 100644 --- a/develop-docs/self-hosted/releases.mdx +++ b/develop-docs/self-hosted/releases.mdx @@ -1,7 +1,7 @@ --- title: Self-Hosted Releases & Upgrading sidebar_title: Releases & Upgrading -sidebar_order: 10 +sidebar_order: 1 --- Sentry cuts regular releases for self-hosting to keep it as close to [sentry.io](https://sentry.io) as possible. We decided to follow a monthly release schedule using the [CalVer](https://calver.org/#scheme) versioning scheme, with a primary release on the [15th of each month](https://github.com/getsentry/self-hosted/blob/704e4c3b5b7360080f79bcfbe26583e5a95ae675/.github/workflows/release.yml#L20-L24). We don't patch old versions, but if a bug is bad enough we may cut an out-of-cycle point release, which, like our regular monthly releases, is a snapshot of the latest versions of all of our components. You can find the [latest release](https://github.com/getsentry/self-hosted/releases/latest) over at the [releases section of our self-hosted repository](https://github.com/getsentry/self-hosted/releases/). diff --git a/develop-docs/services/index.mdx b/develop-docs/services/index.mdx index f2a90bbfe0353..6d86a306fdc12 100644 --- a/develop-docs/services/index.mdx +++ b/develop-docs/services/index.mdx @@ -1,6 +1,6 @@ --- title: Services -sidebar_order: 60 +sidebar_order: 80 --- diff --git a/docs/account/index.mdx b/docs/account/index.mdx index bbc2812c09621..5a3f45de5441e 100644 --- a/docs/account/index.mdx +++ b/docs/account/index.mdx @@ -1,6 +1,6 @@ --- title: Account Settings -sidebar_order: 400 +sidebar_order: 10 description: "Learn about Sentry's user settings and auth tokens." --- diff --git a/docs/api/index.mdx b/docs/api/index.mdx index 1cc66825f444c..99af633b63f04 100644 --- a/docs/api/index.mdx +++ b/docs/api/index.mdx @@ -1,5 +1,6 @@ --- title: API Reference +sidebar_order: 60 --- The Sentry web API is used to access the Sentry platform programmatically. You can use the APIs to manage account-level resources, like organizations and teams, as well as manage and export data. diff --git a/docs/cli/index.mdx b/docs/cli/index.mdx index 2fedf46f55688..04ef078fca840 100644 --- a/docs/cli/index.mdx +++ b/docs/cli/index.mdx @@ -1,6 +1,6 @@ --- title: "Sentry CLI" -sidebar_order: 4000 +sidebar_order: 50 keywords: [ "cli", diff --git a/docs/concepts/index.mdx b/docs/concepts/index.mdx index e22c674675faf..98ffacb44ea2c 100644 --- a/docs/concepts/index.mdx +++ b/docs/concepts/index.mdx @@ -1,6 +1,6 @@ --- title: Concepts & Reference -sidebar_order: 160 +sidebar_order: 80 description: "Learn the basic concepts of Sentry such as searchable properties and data management settings." --- diff --git a/docs/contributing/pages/frontmatter.mdx b/docs/contributing/pages/frontmatter.mdx index 13713dc44e6d3..ec897f596a558 100644 --- a/docs/contributing/pages/frontmatter.mdx +++ b/docs/contributing/pages/frontmatter.mdx @@ -47,3 +47,13 @@ Much of the other functionality for pages is also driven via frontmatter, such a - [Redirects](../redirects/) - [Search](../search/) + +`nextPage` (`{ path: 'path/to/page', title: 'Page Title' }`) + +Overrides the next page shown in the bottom pagination navigation. + +`previousPage` (`{ path: 'path/to/page', title: 'Page Title' }`) + +Overrides the previous page shown in the bottom pagination navigation. + + diff --git a/docs/organization/index.mdx b/docs/organization/index.mdx index cff5bb5e22cc3..932d68187ff05 100644 --- a/docs/organization/index.mdx +++ b/docs/organization/index.mdx @@ -1,6 +1,6 @@ --- title: Organization Settings -sidebar_order: 400 +sidebar_order: 20 description: "Learn how to configure your organization's Sentry account, including 2FA authentication, user management, and data storage location." --- diff --git a/docs/pricing/index.mdx b/docs/pricing/index.mdx index 0d19628b498b1..c3e297ce23298 100644 --- a/docs/pricing/index.mdx +++ b/docs/pricing/index.mdx @@ -1,6 +1,6 @@ --- title: Pricing & Billing -sidebar_order: 1 +sidebar_order: 40 description: "Learn about pricing, managing volume, and the different Sentry plans." --- diff --git a/docs/product/index.mdx b/docs/product/index.mdx index 24bef4bba613a..2ca8fd6b939ae 100644 --- a/docs/product/index.mdx +++ b/docs/product/index.mdx @@ -1,6 +1,6 @@ --- title: Product Walkthroughs -sidebar_order: 1 +sidebar_order: 30 description: "Get an overview of how you can use Sentry to not just observe, but debug errors, get to the root of user complaints, and identify performance bottlenecks." --- diff --git a/docs/security-legal-pii/index.mdx b/docs/security-legal-pii/index.mdx index 7ae239c400396..7422e82adcdd6 100644 --- a/docs/security-legal-pii/index.mdx +++ b/docs/security-legal-pii/index.mdx @@ -1,6 +1,6 @@ --- title: Security, Legal, & PII -sidebar_order: 1 +sidebar_order: 70 description: "Learn about Sentry's security and compliance processes and how to scrub sensitive data." --- diff --git a/src/components/docPage/index.tsx b/src/components/docPage/index.tsx index c6a10390c48c1..5748ea9d5568d 100644 --- a/src/components/docPage/index.tsx +++ b/src/components/docPage/index.tsx @@ -3,6 +3,7 @@ import {ReactNode} from 'react'; import {getCurrentGuide, getCurrentPlatform, nodeForPath} from 'sentry-docs/docTree'; import {serverContext} from 'sentry-docs/serverContext'; import {FrontMatter} from 'sentry-docs/types'; +import {PaginationNavNode} from 'sentry-docs/types/paginationNavNode'; import {isTruthy} from 'sentry-docs/utils'; import {getUnversionedPath} from 'sentry-docs/versioning'; @@ -12,6 +13,7 @@ import {Breadcrumbs} from '../breadcrumbs'; import {CodeContextProvider} from '../codeContext'; import {GitHubCTA} from '../githubCTA'; import {Header} from '../header'; +import {PaginationNav} from '../paginationNav'; import {PlatformSdkDetail} from '../platformSdkDetail'; import {Sidebar} from '../sidebar'; import {TableOfContents} from '../tableOfContents'; @@ -21,8 +23,10 @@ type Props = { frontMatter: Omit; /** Whether to take all the available width */ fullWidth?: boolean; + nextPage?: PaginationNavNode; /** Whether to hide the table of contents & sdk details */ notoc?: boolean; + previousPage?: PaginationNavNode; sidebar?: ReactNode; }; @@ -32,6 +36,8 @@ export function DocPage({ notoc = false, fullWidth = false, sidebar, + nextPage, + previousPage, }: Props) { const {rootNode, path} = serverContext(); const currentPlatform = getCurrentPlatform(rootNode, path); @@ -78,6 +84,16 @@ export function DocPage({
{children}
+ +
+
+ {previousPage && } +
+
+ {nextPage && } +
+
+ {hasGithub && } diff --git a/src/components/githubCTA/styles.module.css b/src/components/githubCTA/styles.module.css index ecf750f7a9b8b..fd3b448f9ccdf 100644 --- a/src/components/githubCTA/styles.module.css +++ b/src/components/githubCTA/styles.module.css @@ -1,6 +1,6 @@ .cta { background: var(--accent-a2); - margin-top: 4rem; + margin-top: 2rem; margin-bottom: 2rem; padding: 1rem 1.25rem; border-radius: 0.25em; diff --git a/src/components/paginationNav.tsx b/src/components/paginationNav.tsx new file mode 100644 index 0000000000000..5b24f516460d4 --- /dev/null +++ b/src/components/paginationNav.tsx @@ -0,0 +1,34 @@ +import {DoubleArrowLeftIcon, DoubleArrowRightIcon} from '@radix-ui/react-icons'; + +import {PaginationNavNode} from 'sentry-docs/types/paginationNavNode'; + +export function PaginationNav({ + node, + title, +}: { + node: PaginationNavNode; + title: 'Previous' | 'Next'; +}) { + return ( + +
+
{title}
+
+ {title === 'Previous' && } + + {node.title} + + {title === 'Next' && } +
+
+
+ ); +} diff --git a/src/components/sidebar/sidebarLinks.tsx b/src/components/sidebar/sidebarLinks.tsx index c069a6f3410ab..c510d2046fc79 100644 --- a/src/components/sidebar/sidebarLinks.tsx +++ b/src/components/sidebar/sidebarLinks.tsx @@ -11,6 +11,7 @@ import {NavNode} from './types'; import {docNodeToNavNode, getNavNodes} from './utils'; /** a root of `"some-root"` maps to the `/some-root/` url */ +// todo: we should probably get rid of this const productSidebarItems = [ { title: 'Account Settings', diff --git a/src/docTree.spec.ts b/src/docTree.spec.ts index bd537f0f71d61..474e116e5dcbf 100644 --- a/src/docTree.spec.ts +++ b/src/docTree.spec.ts @@ -1,6 +1,15 @@ -import {describe, expect, test} from 'vitest'; +import {describe, expect, test, vi} from 'vitest'; -import {DocNode, getCurrentPlatformOrGuide, nodeForPath} from './docTree'; +import { + DocNode, + getCurrentPlatformOrGuide, + getNextNode, + getPreviousNode, + isRootGuidePath, + isRootPlatformPath, + nodeForPath, +} from './docTree'; +import {FrontMatter} from './types'; const createRootNode = (): DocNode => ({ children: [], @@ -11,6 +20,19 @@ const createRootNode = (): DocNode => ({ sourcePath: '', }); +const createNode = (path: string, title: string, frontmatter?: FrontMatter): DocNode => ({ + children: [], + frontmatter: { + title, + slug: path, + ...frontmatter, + }, + missing: false, + path, + slug: path, + sourcePath: 'sourcepath', +}); + const nextjsRoot = createRootNode(); nextjsRoot.children = [ { @@ -119,4 +141,207 @@ describe('docTree', () => { expect(node?.name).toBe('javascript'); }); }); + + describe('getNextNode', () => { + const rootNode = createRootNode(); + + const nodeWithChildren = createNode('a', 'A'); + nodeWithChildren.children = [createNode('a1', 'A1'), createNode('a2', 'A2')]; + nodeWithChildren.children.forEach(child => { + child.parent = nodeWithChildren; + }); + + rootNode.children = [nodeWithChildren, createNode('b', 'B'), createNode('c', 'C')]; + rootNode.children.forEach(child => { + child.parent = rootNode; + }); + + test('should return first child for root node', () => { + const nextNode = getNextNode(rootNode); + expect(nextNode?.slug).toBe('a'); + }); + + test('should return first child for node with children', () => { + const nextNode = getNextNode(nodeWithChildren); + expect(nextNode?.slug).toBe('a1'); + }); + + test('should return next sibling', () => { + const nextNode = getNextNode(nodeWithChildren.children[0]); + expect(nextNode?.slug).toBe('a2'); + }); + + test('should return sibling of parent if no siblings available', () => { + const nextNode = getNextNode(nodeWithChildren.children[1]); + expect(nextNode?.slug).toBe('b'); + }); + + test('should return undefined if no children or siblings', () => { + const nextNode = getNextNode(createNode('d', 'D')); + expect(nextNode).toBeUndefined(); + }); + + test('should respect sidebar order for sorting', () => { + const root = createRootNode(); + const a = createNode('a', 'A', {sidebar_order: 2} as FrontMatter); + const b = createNode('b', 'B', {sidebar_order: 1} as FrontMatter); + root.children = [a, b]; + root.children.forEach(child => { + child.parent = root; + }); + + const a1 = createNode('a1', 'A1', {sidebar_order: 2} as FrontMatter); + const a2 = createNode('a2', 'A2', {sidebar_order: 1} as FrontMatter); + a.children = [a1, a2]; + a.children.forEach(child => { + child.parent = a; + }); + + expect(getNextNode(a)?.slug).toBe('a2'); + expect(getNextNode(b)?.slug).toBe('a'); + expect(getNextNode(a1)?.slug).toBeUndefined(); + }); + + test('should not return siblings for root platform or guide paths', () => { + const platforms = createNode('platforms', 'Platforms'); + const js = createNode('platforms/javascript', 'JavaScript'); + const python = createNode('platforms/python', 'Python'); + platforms.children = [js, python]; + platforms.children.forEach(child => { + child.parent = platforms; + }); + + const nextjs = createNode('platforms/javascript/guides/nextjs', 'Next.js'); + const angular = createNode('platforms/javascript/guides/angular', 'Angular'); + js.children = [nextjs, angular]; + js.children.forEach(child => { + child.parent = js; + }); + + expect(getNextNode(js)).toBeUndefined(); + expect(getNextNode(nextjs)).toBeUndefined(); + expect(getNextNode(angular)).toBeUndefined(); + }); + }); + + describe('getPreviousNode', () => { + const root = createRootNode(); + + const a = createNode('a', 'A'); + const a1 = createNode('a1', 'A1'); + const a2 = createNode('a2', 'A2'); + a.children = [a1, a2]; + a.children.forEach(child => { + child.parent = a; + }); + + const b = createNode('b', 'B'); + const c = createNode('c', 'C'); + root.children = [a, b, c]; + root.children.forEach(child => { + child.parent = root; + }); + + test('should return previous child of parent', () => { + expect(getPreviousNode(c)).toBe(b); + }); + + test('should return previous sibling if previous sibling has children', () => { + expect(getPreviousNode(b)).toBe(a); + }); + + test('should return undefined if no children or siblings', () => { + expect(getPreviousNode(createNode('d', 'D'))).toBeUndefined(); + }); + + test('should return parent for first child', () => { + expect(getPreviousNode(a1)).toBe(a); + }); + + test('should respect sidebar order for sorting', () => { + const xRoot = createRootNode(); + const xA = createNode('a', 'A', {sidebar_order: 2} as FrontMatter); + const xB = createNode('b', 'B', {sidebar_order: 1} as FrontMatter); + xRoot.children = [xA, xB]; + xRoot.children.forEach(child => { + child.parent = xRoot; + }); + + expect(getPreviousNode(xA)).toBe(xB); + }); + + test('should return root as previous page for root platform or guide paths', () => { + const platforms = createNode('platforms', 'Platforms'); + const js = createNode('platforms/javascript', 'JavaScript'); + const python = createNode('platforms/python', 'Python'); + platforms.children = [js, python]; + platforms.children.forEach(child => { + child.parent = platforms; + }); + + const nextjs = createNode('platforms/javascript/guides/nextjs', 'Next.js'); + const angular = createNode('platforms/javascript/guides/angular', 'Angular'); + js.children = [nextjs, angular]; + js.children.forEach(child => { + child.parent = js; + }); + + expect(getPreviousNode(js)).toBe('root'); + expect(getPreviousNode(python)).toBe('root'); + expect(getPreviousNode(nextjs)).toBe('root'); + expect(getPreviousNode(angular)).toBe('root'); + }); + + test('should not return /platforms as previous page', () => { + const docs = createNode('', 'Docs'); + const platforms = createNode('platforms', 'Platforms'); + const accounts = createNode('accounts', 'Accounts'); + docs.children = [platforms, accounts]; + docs.children.forEach(child => { + child.parent = docs; + }); + + expect(getPreviousNode(accounts)).toBe(undefined); + }); + + test('should return undefined for getting-started page in developer docs', () => { + vi.mock('./isDeveloperDocs', () => ({ + isDeveloperDocs: true, + })); + + const home = createNode('', 'Home'); + const gettingStarted = createNode('getting-started', 'Getting Started'); + home.children = [gettingStarted]; + gettingStarted.parent = home; + + expect(getPreviousNode(gettingStarted)).toBeUndefined(); + }); + }); + + describe('isRootPlatformPath', () => { + test('should return true for root platform path', () => { + expect(isRootPlatformPath('platforms/javascript')).toBe(true); + expect(isRootPlatformPath('platforms/python')).toBe(true); + }); + + test('should return false for non-root platform path', () => { + expect(isRootPlatformPath('platforms/javascript/guides/nextjs')).toBe(false); + expect(isRootPlatformPath('platforms/javascript/troubleshooting')).toBe(false); + }); + }); + + describe('isRootGuidePath', () => { + test('should return true for root guide path', () => { + expect(isRootGuidePath('platforms/javascript/guides/nextjs')).toBe(true); + expect(isRootGuidePath('platforms/python/guides/django')).toBe(true); + }); + + test('should return false for non-root guide path', () => { + expect(isRootGuidePath('platforms/javascript')).toBe(false); + expect(isRootGuidePath('platforms/javascript/troubleshooting/get-started')).toBe( + false + ); + expect(isRootGuidePath('platforms/python/guides/django/installation')).toBe(false); + }); + }); }); diff --git a/src/docTree.ts b/src/docTree.ts index 008b2205a5044..d568f30a6556e 100644 --- a/src/docTree.ts +++ b/src/docTree.ts @@ -146,6 +146,108 @@ export function nodeForPath(node: DocNode, path: string | string[]): DocNode | u return node; } +/** + * Returns the next node in the tree, which is either the first child, + * the next sibling, or the next sibling of a parent node. + * + * @param node The current DocNode + * @returns The next DocNode in the tree, or undefined if there is no next node + */ +export const getNextNode = (node: DocNode): DocNode | undefined => { + const children = node.children.filter(filterVisibleSiblings).sort(sortBySidebarOrder); + // Check for children first + if ( + children.length > 0 && + !isRootPlatformPath(children[0].path) && + !isRootGuidePath(children[0].path) + ) { + return children[0]; + } + + // If no children, look for siblings or parent siblings + let currentNode: DocNode | undefined = node; + while (currentNode?.parent) { + const nextSibling = getNextSiblingNode(currentNode); + if (nextSibling) { + if (isRootPlatformPath(nextSibling.path) || isRootGuidePath(nextSibling.path)) { + return undefined; + } + return nextSibling; + } + currentNode = currentNode.parent; + } + + // If we've reached this point, there are no more nodes to traverse + return undefined; +}; + +/** + * Returns the previous node in the tree, which is either the last child of the parent, + * the previous sibling, or the previous sibling of a parent node. + */ +export const getPreviousNode = (node: DocNode): DocNode | undefined | 'root' => { + // in this special case, calculating the root node is unnecessary so we return a string instead + if (isRootPlatformPath(node.path) || isRootGuidePath(node.path)) { + return 'root'; + } + + if (node.path === 'getting-started' && isDeveloperDocs) { + return undefined; + } + + const previousSibling = getPreviousSiblingNode(node); + if (previousSibling) { + if (previousSibling.path === 'platforms') { + return undefined; + } + return previousSibling; + } + return node.parent; +}; + +const getNextSiblingNode = (node: DocNode): DocNode | undefined => { + if (!node.parent) { + return undefined; + } + + const siblings = node.parent.children + .sort(sortBySidebarOrder) + .filter(filterVisibleSiblings); + + const index = siblings.indexOf(node); + if (index < siblings.length - 1) { + return siblings[index + 1]; + } + + return undefined; +}; + +const getPreviousSiblingNode = (node: DocNode): DocNode | undefined => { + if (!node.parent) { + return undefined; + } + + const siblings = node.parent.children + .sort(sortBySidebarOrder) + .filter(filterVisibleSiblings); + + const index = siblings.indexOf(node); + if (index > 0) { + return siblings[index - 1]; + } + + return undefined; +}; + +const sortBySidebarOrder = (a: DocNode, b: DocNode) => + (a.frontmatter.sidebar_order ?? 10) - (b.frontmatter.sidebar_order ?? 10); + +const filterVisibleSiblings = (s: DocNode) => + (s.frontmatter.sidebar_title || s.frontmatter.title) && + !s.frontmatter.sidebar_hidden && + !s.frontmatter.draft && + s.path; + function nodeToPlatform(n: DocNode): Platform { const platformData = platformsData()[n.slug]; const caseStyle = platformData?.case_style || n.frontmatter.caseStyle; @@ -183,6 +285,18 @@ function nodeToGuide(platform: string, n: DocNode): PlatformGuide { }; } +export const isRootPlatformPath = (path: string) => { + return path.startsWith('platforms/') && path.split('/').length === 2; +}; + +export const isRootGuidePath = (path: string) => { + return ( + path.startsWith('platforms/') && + path.split('/').length === 4 && + path.split('/')[2].startsWith('guides') + ); +}; + export function getPlatform(rootNode: DocNode, name: string): Platform | undefined { const platformNode = nodeForPath(rootNode, ['platforms', name]); if (!platformNode) { diff --git a/src/types/frontmatter.ts b/src/types/frontmatter.ts index 708b1cbd20997..ce50c8efc5239 100644 --- a/src/types/frontmatter.ts +++ b/src/types/frontmatter.ts @@ -1,3 +1,5 @@ +import {PaginationNavNode} from './paginationNavNode'; + /** ** a YAML-formatted blob defined at the top of every markdown or mdx file */ @@ -23,6 +25,11 @@ export interface FrontMatter { * A list of keywords for indexing with search. */ keywords?: string[]; + + /** + * The next page in the bottom pagination navigation. + */ + nextPage?: PaginationNavNode; /** * Set this to true to disable indexing (robots, algolia) of this content. */ @@ -31,11 +38,20 @@ export interface FrontMatter { * Specific guides that this page is not relevant to. */ notSupported?: string[]; + /** * Set this to true to disable page-level table of contents rendering. */ notoc?: boolean; + /** + * The previous page in the bottom pagination navigation. + */ + previousPage?: PaginationNavNode; + + /** + * The next page in the sidebar navigation. + */ /** * Set this to true to hide from the sidebar */ @@ -60,7 +76,6 @@ export interface FrontMatter { * Specific guides that this page is relevant to. */ supported?: string[]; - /** * Available versions for this page * @example ['v7.119.0', 'next'] diff --git a/src/types/paginationNavNode.ts b/src/types/paginationNavNode.ts new file mode 100644 index 0000000000000..5cfde0ff52a78 --- /dev/null +++ b/src/types/paginationNavNode.ts @@ -0,0 +1,4 @@ +export type PaginationNavNode = { + path: string; + title: string; +};