Skip to content

Commit fb3202e

Browse files
committed
* simply logic to generate the breadcrumb items which fixed an issue if there is another file with the same name in the directory structure ( found it while testing some nesting docs )
* updated the docs page styling and hide sidebar if there are no headings available * added `showToc` prop to the frontmatter to force hide the right sidebar in the docs page * moved datatable components to `components/data-table` * added h1-h6 to `mdx-components` to support a quick link for each heading * updated the sidebar width from `w-64` to `w-72`
1 parent 4c4f2bf commit fb3202e

11 files changed

+156
-79
lines changed

src/app/docs/[...slug]/page.tsx

+82-63
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
import type { EntryType } from "@/collections"
1+
import type { EntryType, frontmatterSchema } from "@/collections"
22
import type { Metadata } from "next"
3+
import type { z } from "zod"
4+
import { cache } from "react"
35
import { notFound } from "next/navigation"
4-
import { CollectionInfo, getFileContent, getSections } from "@/collections"
6+
import {
7+
CollectionInfo,
8+
getFileContent,
9+
getSections,
10+
getTitle,
11+
} from "@/collections"
512
import { SiteBreadcrumb } from "@/components/breadcrumb"
613
import { Comments } from "@/components/comments"
714
import SectionGrid from "@/components/section-grid"
@@ -36,47 +43,42 @@ interface PageProps {
3643
params: Promise<{ slug: string[] }>
3744
}
3845

39-
async function getBreadcrumbItems(slug: string[]) {
40-
// we do not want to have "docs" as breadcrumb element
41-
// also, we do not need the index file in our breadcrumb
42-
const combinations = removeFromArray(slug, ["docs", "index"]).reduce(
43-
(acc: string[][], curr) => acc.concat(acc.map((sub) => [...sub, curr])),
44-
[[]],
46+
const getBreadcrumbItems = cache(async (slug: string[]) => {
47+
// we do not want to have "index" as breadcrumb element
48+
const cleanedSlug = removeFromArray(slug, ["index"])
49+
50+
const combinations = cleanedSlug.map((_, index) =>
51+
cleanedSlug.slice(0, index + 1),
4552
)
4653

4754
const items = []
4855

4956
for (const currentPageSegement of combinations) {
50-
let collection
57+
let collection: EntryType
58+
let file: Awaited<ReturnType<typeof getFileContent>>
59+
let frontmatter: z.infer<typeof frontmatterSchema> | undefined
5160
try {
5261
collection = await CollectionInfo.getEntry(currentPageSegement)
53-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
62+
if (collection.getPathSegments().includes("index")) {
63+
file = await getFileContent(collection.getParent())
64+
} else {
65+
file = await getFileContent(collection)
66+
}
67+
68+
frontmatter = await file?.getExportValue("frontmatter")
5469
} catch (e: unknown) {
5570
continue
5671
}
5772

58-
if (isDirectory(collection)) {
73+
if (!frontmatter) {
5974
items.push({
6075
title: collection.getTitle(),
6176
path: ["docs", ...collection.getPathSegments()],
6277
})
6378
} else {
64-
const file = await getFileContent(collection)
65-
66-
if (!file) {
67-
continue
68-
}
69-
const frontmatter = await file.getExportValue("frontmatter")
70-
71-
// in case we have an index file inside a directory
72-
// we have also to fetch the directory name, otherwise we get "Index" as title
73-
// if there is no `frontmatter.navTitle` defined
74-
const parentTitle = collection.getPathSegments().includes("index")
75-
? collection.getParent().getTitle()
76-
: null
77-
79+
const title = getTitle(collection, frontmatter, true)
7880
items.push({
79-
title: frontmatter.navTitle ?? parentTitle ?? collection.getTitle(),
81+
title,
8082
path: [
8183
"docs",
8284
...removeFromArray(collection.getPathSegments(), ["index"]),
@@ -86,17 +88,13 @@ async function getBreadcrumbItems(slug: string[]) {
8688
}
8789

8890
return items
89-
}
90-
91-
async function getParentTitle(slug: string[]) {
92-
const elements = await getBreadcrumbItems(slug)
93-
94-
return elements.map((ele) => ele.title)
95-
}
91+
})
9692

9793
export async function generateMetadata(props: PageProps): Promise<Metadata> {
9894
const params = await props.params
99-
const titles = await getParentTitle(params.slug)
95+
const breadcrumbItems = await getBreadcrumbItems(params.slug)
96+
97+
const titles = breadcrumbItems.map((ele) => ele.title)
10098

10199
return {
102100
title: titles.join(" - "),
@@ -122,13 +120,21 @@ export default async function DocsPage(props: PageProps) {
122120
// if we can't find an index file, but we have a valid directory
123121
// use the directory component for rendering
124122
if (!file && isDirectory(collection)) {
125-
return <DirectoryContent source={collection} />
123+
return (
124+
<>
125+
<DirectoryContent source={collection} />
126+
</>
127+
)
126128
}
127129

128130
// if we have a valid file ( including the index file )
129131
// use the file component for rendering
130132
if (file) {
131-
return <FileContent source={collection} />
133+
return (
134+
<>
135+
<FileContent source={collection} />
136+
</>
137+
)
132138
}
133139

134140
// seems to be an invalid path
@@ -143,8 +149,8 @@ async function DirectoryContent({ source }: { source: EntryType }) {
143149
return (
144150
<>
145151
<div className="container py-6">
146-
<div className={cn("flex flex-col gap-y-8")}>
147-
<div>
152+
<div className={cn("gap-8 xl:grid")}>
153+
<div className="mx-auto w-full 2xl:w-6xl">
148154
<SiteBreadcrumb items={breadcrumbItems} />
149155

150156
<article data-pagefind-body>
@@ -156,6 +162,7 @@ async function DirectoryContent({ source }: { source: EntryType }) {
156162
"prose-code:before:hidden prose-code:after:hidden",
157163
// use full width
158164
"w-full max-w-full",
165+
"prose-a:text-indigo-400 prose-a:hover:text-white",
159166
)}
160167
>
161168
<h1
@@ -197,24 +204,34 @@ async function FileContent({ source }: { source: EntryType }) {
197204
return (
198205
<>
199206
<div className="container py-6">
200-
{headings.length > 0 && <MobileTableOfContents toc={headings} />}
207+
{headings.length > 0 && frontmatter.showToc && (
208+
<MobileTableOfContents toc={headings} />
209+
)}
201210

202211
<div
203-
className={cn("gap-8 xl:grid xl:grid-cols-[1fr_300px]", {
204-
"mt-12 xl:mt-0": headings.length > 0,
212+
className={cn("gap-8 xl:grid", {
213+
"mt-12 xl:mt-0": frontmatter.showToc && headings.length > 0,
214+
"xl:grid-cols-[1fr_300px]":
215+
frontmatter.showToc && headings.length > 0,
216+
"xl:grid-cols-1": !frontmatter.showToc || headings.length == 0,
205217
})}
206218
>
207-
<div>
219+
<div
220+
className={cn("mx-auto", {
221+
"w-full 2xl:w-6xl": !frontmatter.showToc || headings.length == 0,
222+
"w-full 2xl:w-4xl": frontmatter.showToc && headings.length > 0,
223+
})}
224+
>
208225
<SiteBreadcrumb items={breadcrumbItems} />
209226

210227
<div data-pagefind-body>
211228
<h1
212-
className="no-prose mb-2 scroll-m-20 text-4xl font-light tracking-tight lg:text-5xl"
229+
className="no-prose mb-2 scroll-m-20 text-3xl font-light tracking-tight sm:text-4xl md:text-5xl"
213230
data-pagefind-meta="title"
214231
>
215232
{frontmatter.title ?? source.getTitle()}
216233
</h1>
217-
<p className="mb-8 text-lg font-medium text-pretty text-gray-500 sm:text-xl/8">
234+
<p className="text-muted-foreground mb-8 text-lg font-medium text-pretty sm:text-xl/8">
218235
{frontmatter.description ?? ""}
219236
</p>
220237
<article>
@@ -256,27 +273,29 @@ async function FileContent({ source }: { source: EntryType }) {
256273
<Comments />
257274
</div>
258275
</div>
259-
<div className="hidden w-[19.5rem] xl:sticky xl:top-[4.75rem] xl:-mr-6 xl:block xl:h-[calc(100vh-4.75rem)] xl:flex-none xl:overflow-y-auto xl:pr-6 xl:pb-16">
260-
<TableOfContents toc={headings} />
261-
262-
<div className="my-6 grid gap-y-4 border-t pt-6">
263-
<div>
264-
<a
265-
href={file.getEditUrl()}
266-
target="_blank"
267-
className="text-muted-foreground hover:text-foreground flex items-center text-sm no-underline transition-colors"
268-
>
269-
Edit this page <ExternalLinkIcon className="ml-2 h-4 w-4" />
270-
</a>
271-
</div>
272-
273-
{lastUpdate && (
274-
<div className="text-muted-foreground text-sm">
275-
Last updated: {format(lastUpdate, "dd.MM.yyyy")}
276+
{frontmatter.showToc && headings.length > 0 ? (
277+
<div className="hidden w-[19.5rem] xl:sticky xl:top-[4.75rem] xl:-mr-6 xl:block xl:h-[calc(100vh-4.75rem)] xl:flex-none xl:overflow-y-auto xl:pr-6 xl:pb-16">
278+
<TableOfContents toc={headings} />
279+
280+
<div className="my-6 grid gap-y-4 border-t pt-6">
281+
<div>
282+
<a
283+
href={file.getEditUrl()}
284+
target="_blank"
285+
className="text-muted-foreground hover:text-foreground flex items-center text-sm no-underline transition-colors"
286+
>
287+
Edit this page <ExternalLinkIcon className="ml-2 h-4 w-4" />
288+
</a>
276289
</div>
277-
)}
290+
291+
{lastUpdate && (
292+
<div className="text-muted-foreground text-sm">
293+
Last updated: {format(lastUpdate, "dd.MM.yyyy")}
294+
</div>
295+
)}
296+
</div>
278297
</div>
279-
</div>
298+
) : null}
280299
</div>
281300
</div>
282301
</>

src/collections.ts

+11
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const frontmatterSchema = z.object({
1414
navTitle: z.string().optional(),
1515
entrypoint: z.string().optional(),
1616
alias: z.string().optional(),
17+
showToc: z.boolean().optional().default(true),
1718
})
1819

1920
export const headingSchema = z.array(
@@ -86,6 +87,16 @@ export type DirectoryType = Awaited<
8687
ReturnType<typeof CollectionInfo.getDirectory>
8788
>
8889

90+
export function getTitle(
91+
collection: EntryType,
92+
frontmatter: z.infer<typeof frontmatterSchema>,
93+
includeTitle = false,
94+
) {
95+
return includeTitle
96+
? (frontmatter.navTitle ?? frontmatter.title ?? collection.getTitle())
97+
: (frontmatter.navTitle ?? collection.getTitle())
98+
}
99+
89100
export async function getDirectoryContent(source: EntryType) {
90101
// first, try to get the file based on the given path
91102

src/components/heading.tsx

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { ElementType, ReactNode } from "react"
2+
3+
type IntrinsicElement = keyof JSX.IntrinsicElements
4+
type PolymorphicComponentProps<T extends IntrinsicElement> = {
5+
as?: T
6+
} & JSX.IntrinsicElements[T]
7+
8+
const PolymorphicComponent = <T extends IntrinsicElement>({
9+
as: elementType = "div" as T,
10+
...rest
11+
}: PolymorphicComponentProps<T>) => {
12+
const Component = elementType as ElementType
13+
return <Component {...rest} />
14+
}
15+
16+
export function Heading({
17+
level,
18+
id,
19+
children,
20+
}: {
21+
level: number
22+
id: string
23+
children: ReactNode
24+
}) {
25+
return (
26+
<PolymorphicComponent as={`h${level}`} id={id} className="group">
27+
{children}{" "}
28+
<a
29+
href={`#${id}`}
30+
className="hidden no-underline group-hover:inline-block"
31+
>
32+
#
33+
</a>
34+
</PolymorphicComponent>
35+
)
36+
}

src/components/sidebar.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
SidebarItem,
1313
SidebarLabel,
1414
} from "@/components/ui/sidebar"
15+
import { useIsMobile } from "@/hooks/use-mobile"
1516
import { current } from "@/lib/helpers"
1617
import { cn } from "@/lib/utils"
1718
import { ChevronsUpDown } from "lucide-react"
@@ -42,6 +43,7 @@ export function SiteSidebar({
4243
defaultHidden?: boolean
4344
}) {
4445
const pathname = usePathname()
46+
const isMobile = useIsMobile()
4547

4648
return (
4749
<Sidebar className="lg:mt-12" defaultHidden={defaultHidden}>
@@ -60,9 +62,9 @@ export function SiteSidebar({
6062
</div>
6163
</DropdownMenuTrigger>
6264
<DropdownMenuContent
63-
className="w-64"
65+
className="w-72"
6466
align="start"
65-
side="right"
67+
side={isMobile ? "bottom" : "right"}
6668
sideOffset={4}
6769
>
6870
<DropdownMenuLabel className="text-muted-foreground text-xs">

src/components/table-of-contents.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function MobileTableOfContents({ toc }: TocProps) {
6868
const filteredToc = toc.filter((item) => item.depth > 1 && item.depth <= 4)
6969

7070
return (
71-
<div className="bg-background fixed top-12 left-0 z-20 h-[calc(theme(height.12)+1px)] w-full border-b px-2 py-2.5 lg:left-[theme(width.64)] lg:w-[calc(theme(width.full)-theme(width.64))] xl:hidden">
71+
<div className="bg-background fixed top-12 left-0 z-20 h-[calc(theme(height.12)+1px)] w-full border-b px-2 py-2.5 lg:left-[theme(width.72)] lg:w-[calc(theme(width.full)-theme(width.72))] xl:hidden">
7272
<DropdownMenu>
7373
<DropdownMenuTrigger className="ring-ring hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent w-full rounded-md focus-visible:ring-2 focus-visible:outline-hidden">
7474
<div className="flex items-center gap-1.5 overflow-hidden px-2 py-1.5 text-left text-sm transition-all">

src/components/ui/sidebar.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const SidebarLayout = React.forwardRef<
2222
ref={ref}
2323
data-sidebar={state}
2424
className={cn(
25-
"bg-accent/50 top-20 flex min-h-screen pl-0 transition-all duration-300 ease-in-out data-[sidebar=closed]:pl-0 sm:pl-[calc(theme(width.64))]",
25+
"bg-accent/50 top-20 flex min-h-screen pl-0 transition-all duration-300 ease-in-out data-[sidebar=closed]:pl-0 sm:pl-[calc(theme(width.72))]",
2626
className,
2727
)}
2828
{...props}
@@ -84,7 +84,7 @@ const Sidebar = React.forwardRef<
8484

8585
<aside
8686
className={cn(
87-
"fixed inset-y-0 left-0 z-10 w-64 transition-all duration-300 ease-in-out in-data-[sidebar=closed]:left-[calc(theme(width.64)*-1)]",
87+
"fixed inset-y-0 left-0 z-10 w-72 transition-all duration-300 ease-in-out in-data-[sidebar=closed]:left-[calc(theme(width.72)*-1)]",
8888
defaultHidden ? "hidden" : "hidden lg:block",
8989
)}
9090
>

src/lib/navigation.ts

-10
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,6 @@ export function isHidden(entry: EntryType) {
2020
return entry.getBaseName().startsWith("_")
2121
}
2222

23-
/** Create a slug from a string. */
24-
// source: https://github.com/souporserious/renoun/blob/main/packages/renoun/src/utils/create-slug.ts
25-
export function createSlug(input: string) {
26-
return input
27-
.replace(/([a-z])([A-Z])/g, "$1-$2") // Add a hyphen between lower and upper case letters
28-
.replace(/([A-Z])([A-Z][a-z])/g, "$1-$2") // Add a hyphen between consecutive upper case letters followed by a lower case letter
29-
.replace(/[_\s]+/g, "-") // Replace underscores and spaces with a hyphen
30-
.toLowerCase() // Convert the entire string to lowercase
31-
}
32-
3323
// source:
3424
// https://github.com/souporserious/renoun/blob/main/packages/renoun/src/file-system/index.test.ts
3525
async function buildTreeNavigation(entry: EntryType): Promise<TreeItem | null> {

0 commit comments

Comments
 (0)