Skip to content

Commit 0094049

Browse files
authored
feat(previous-next): add frontmatter arguments for automatically displaying next and previous buttons (#216)
* feat(previous-next): add frontmatter arguments for automatically displaying next and previous buttons * feat(previous-next): formatting * feat(previous-next): add automatically infer next/previous
1 parent 76464aa commit 0094049

File tree

5 files changed

+217
-9
lines changed

5 files changed

+217
-9
lines changed

docs/configuration.mdx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,15 @@ For example:
114114
}
115115
```
116116

117+
### `automaticallyInferNextPrevious`
118+
119+
A boolean indicating if the next and previous links should be automatically inferred from the sidebar configuration.
120+
You can still overwrite a specific link by providing the `next` and `previous` keys in the [frontmatter](/frontmatter).
121+
122+
| Key | Type | Default |
123+
| -------------------------------- | --------- | ------- |
124+
| `automaticallyInferNextPrevious` | `boolean` | `true` |
125+
117126
### `sidebar`
118127

119128
An array containing nested sidebar array items. If provided, will be rendered on every page down the left hand side.
@@ -194,8 +203,8 @@ If provided, Google Tag Manager will be added to all of your documentation pages
194203

195204
If provided, Google Analytics will be added to all of your documentation pages.
196205

197-
| Key | Type | Default |
198-
| ------------------ | -------- | ------- |
206+
| Key | Type | Default |
207+
| ----------------- | -------- | ------- |
199208
| `googleAnalytics` | `string` | |
200209

201210
### `zoomImages`

docs/frontmatter.mdx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,43 @@ redirect: /getting-started
7575
redirect: https://github.com/acme/awesome-project
7676
---
7777
```
78+
79+
## previous
80+
81+
A path which if set, will show a link to the previous page at the bottom of the page.
82+
If the previousTitle is not set, the title will be get from the `docs.json`.
83+
If not found in the configuration file, an empty string will be used.
84+
85+
Can be a relative internal path or external URL.
86+
87+
| Key | Type | Default |
88+
| --------------- | -------- | ----------------------------------------------------- |
89+
| `previous` | `string` | |
90+
| `previousTitle` | `string` | The value for the previous path in the docs.json file |
91+
92+
```text title=index.md
93+
---
94+
previous: /getting-started
95+
previousTitle: Getting started
96+
---
97+
```
98+
99+
## next
100+
101+
A path which if set, will show a link to the next page at the bottom of the page.
102+
If the nextTitle is not set, the title will be get from the `docs.json`.
103+
If not found in the configuration file, an empty string will be used.
104+
105+
Can be a relative internal path or external URL.
106+
107+
| Key | Type | Default |
108+
| ----------- | -------- | ------------------------------------------------- |
109+
| `next` | `string` | |
110+
| `nextTitle` | `string` | The value for the next path in the docs.json file |
111+
112+
```text title=index.md
113+
---
114+
next: /getting-started
115+
nextTitle: Getting started
116+
---
117+
```

website/app/components/Documentation.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import { useHydratedMdx } from '@docs.page/client';
22
import cx from 'classnames';
33

4+
import { useEffect, useState } from 'react';
5+
import { useTransition } from 'remix';
46
import { Footer } from '~/components/Footer';
57
import { Header } from '~/components/Header';
8+
import components from '~/components/mdx';
9+
import { ScrollSpy } from '~/components/ScrollSpy';
610
import { Sidebar } from '~/components/Sidebar';
711
import { Theme } from '~/components/Theme';
8-
import components from '~/components/mdx';
912
import { DocumentationProvider, DomainProvider } from '~/context';
13+
import { hash as createHash } from '~/utils';
14+
import domains from '../../../domains.json';
1015
import { DocumentationLoader } from '../loaders/documentation.server';
11-
import { ScrollSpy } from '~/components/ScrollSpy';
1216
import { TabsContext } from './mdx/Tabs';
13-
import { hash as createHash } from '~/utils';
1417
import { MobileNav } from './MobileNav';
15-
import { useEffect, useState } from 'react';
16-
import { useTransition } from 'remix';
17-
import domains from '../../../domains.json';
18+
import { PreviousNext } from './PreviousNext';
1819

1920
export default function Documentation({ data }: { data: DocumentationLoader }) {
2021
const [open, toggleMenu] = useState<boolean>(false);
@@ -58,6 +59,8 @@ export default function Documentation({ data }: { data: DocumentationLoader }) {
5859
<TabsContext hash={hash}>
5960
<MDX components={components} />
6061
</TabsContext>
62+
63+
<PreviousNext frontmatter={data.frontmatter} />
6164
</main>
6265
<Footer />
6366
</div>
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { BundleSuccess, SidebarItem } from '@docs.page/server';
2+
import { useLocation } from 'react-router-dom';
3+
import { useDocumentationContext } from '~/context';
4+
import { DocsLink } from './DocsLink';
5+
6+
interface PreviousNextProps {
7+
frontmatter: BundleSuccess['frontmatter'];
8+
}
9+
10+
function findNameInSidebar(sidebarItems: SidebarItem[], url: string): string | undefined {
11+
for (const item of sidebarItems) {
12+
const [title, urlOrChildren] = item;
13+
if (typeof urlOrChildren === 'string') {
14+
if (urlOrChildren === url) {
15+
return title as string;
16+
}
17+
} else {
18+
const name = findNameInSidebar(urlOrChildren, url);
19+
if (name) {
20+
return name;
21+
}
22+
}
23+
}
24+
}
25+
26+
/**
27+
* Return the previous item;
28+
* The found parameters allow use to know we should use this item or not.
29+
*/
30+
function findPreviousInSidebar(
31+
sidebarItems: SidebarItem[],
32+
url: string,
33+
previousItem?: { url: string; name: string; found?: boolean },
34+
): { url: string; name: string; found?: boolean } | undefined {
35+
let previous = previousItem;
36+
for (const item of sidebarItems) {
37+
const [title, urlOrChildren] = item;
38+
if (typeof urlOrChildren === 'string') {
39+
if (urlOrChildren === url && previous) {
40+
return { ...previous, found: true };
41+
}
42+
previous = { url: urlOrChildren, name: title as string };
43+
} else {
44+
const previousRecursive = findPreviousInSidebar(urlOrChildren, url, previous);
45+
if (previousRecursive?.found) {
46+
return previousRecursive;
47+
} else {
48+
previous = previousRecursive;
49+
}
50+
}
51+
}
52+
return previous;
53+
}
54+
55+
/**
56+
* Return the next item or a boolean;
57+
* If a boolean is returned, there were no next items.
58+
*/
59+
function findNextInSidebar(
60+
sidebarItems: SidebarItem[],
61+
url: string,
62+
takeNext: boolean,
63+
): { url: string; name: string } | undefined | boolean {
64+
let _takeNext = takeNext;
65+
for (const item of sidebarItems) {
66+
const [title, urlOrChildren] = item;
67+
if (typeof urlOrChildren === 'string') {
68+
if (_takeNext) {
69+
return { url: urlOrChildren, name: title as string };
70+
}
71+
72+
if (urlOrChildren === url) {
73+
_takeNext = true;
74+
}
75+
} else {
76+
const nextRecursive = findNextInSidebar(urlOrChildren, url, _takeNext);
77+
if (typeof nextRecursive === 'boolean') {
78+
_takeNext = nextRecursive;
79+
} else if (nextRecursive) {
80+
return nextRecursive;
81+
}
82+
}
83+
}
84+
if (_takeNext) {
85+
return true;
86+
}
87+
return undefined;
88+
}
89+
90+
export function PreviousNext({ frontmatter }: PreviousNextProps) {
91+
const { owner, repo } = useDocumentationContext();
92+
const { sidebar, automaticallyInferNextPrevious } = useDocumentationContext().config;
93+
const { pathname } = useLocation();
94+
95+
const formattedPathname = pathname.replace(`/${owner}/${repo}`, '') || '/';
96+
97+
let previous: string | undefined = frontmatter.previous;
98+
let next: string | undefined = frontmatter.next;
99+
100+
let previousTitle: string | undefined = frontmatter.previousTitle;
101+
let nextTitle: string | undefined = frontmatter.nextTitle;
102+
103+
if (!previous && !next && !automaticallyInferNextPrevious) {
104+
return null;
105+
}
106+
107+
if (previous === undefined && automaticallyInferNextPrevious) {
108+
const result = findPreviousInSidebar(sidebar, formattedPathname);
109+
if (result?.found) {
110+
previous = result?.url;
111+
previousTitle = result?.name;
112+
}
113+
}
114+
115+
if (next === undefined && automaticallyInferNextPrevious) {
116+
const result = findNextInSidebar(sidebar, formattedPathname, false);
117+
if (typeof result !== 'boolean') {
118+
next = result?.url;
119+
nextTitle = result?.name;
120+
}
121+
}
122+
123+
return (
124+
<nav aria-label="Docs pages navigation" className="mt-10 flex items-center justify-between">
125+
{!!previous && (
126+
<DocsLink
127+
to={previous}
128+
className="transition-border rounded-md border p-4 no-underline hover:border-gray-300"
129+
>
130+
<div className="text-sm text-gray-600 dark:text-gray-200">Previous</div>
131+
<div className="text-docs-theme">
132+
« {previousTitle ?? findNameInSidebar(sidebar, previous)}
133+
</div>
134+
</DocsLink>
135+
)}
136+
<div />
137+
{!!next && (
138+
<DocsLink
139+
to={next}
140+
className="transition-border rounded-md border p-4 text-right no-underline hover:border-gray-300"
141+
>
142+
<div className="text-sm text-gray-600 dark:text-gray-200">Next</div>
143+
<div className="text-docs-theme">{nextTitle ?? findNameInSidebar(sidebar, next)} »</div>
144+
</DocsLink>
145+
)}
146+
</nav>
147+
);
148+
}

website/app/utils/config.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { getBoolean, getNumber, getString, getValue } from './get';
21
import get from 'lodash.get';
2+
import { getBoolean, getNumber, getString, getValue } from './get';
33

44
// Represents how the sidebar should look in the config file.
55
export type SidebarItem = [string, Array<[string, string]>] | [string, string];
@@ -100,6 +100,8 @@ export interface ProjectConfig {
100100
experimentalCodehike: boolean;
101101
// Whether Math is enabled
102102
experimentalMath: boolean;
103+
// Whether Next/Previous buttons are showing automatically based on the sidebar
104+
automaticallyInferNextPrevious: boolean;
103105
}
104106

105107
export const defaultConfig: ProjectConfig = {
@@ -120,6 +122,7 @@ export const defaultConfig: ProjectConfig = {
120122
zoomImages: false,
121123
experimentalCodehike: false,
122124
experimentalMath: false,
125+
automaticallyInferNextPrevious: true,
123126
};
124127

125128
// Merges any user config with default values.
@@ -155,6 +158,11 @@ export function mergeConfig(json: Record<string, unknown>): ProjectConfig {
155158
defaultConfig.experimentalCodehike,
156159
),
157160
experimentalMath: getBoolean(json, 'experimentalMath', defaultConfig.experimentalMath),
161+
automaticallyInferNextPrevious: getBoolean(
162+
json,
163+
'automaticallyInferNextPrevious',
164+
defaultConfig.automaticallyInferNextPrevious,
165+
),
158166
};
159167
}
160168

0 commit comments

Comments
 (0)