Skip to content

Commit

Permalink
* simply logic to generate the breadcrumb items which fixed an issue …
Browse files Browse the repository at this point in the history
…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`
  • Loading branch information
noxify committed Feb 12, 2025
1 parent 4c4f2bf commit fb3202e
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 79 deletions.
145 changes: 82 additions & 63 deletions src/app/docs/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import type { EntryType } from "@/collections"
import type { EntryType, frontmatterSchema } from "@/collections"
import type { Metadata } from "next"
import type { z } from "zod"
import { cache } from "react"
import { notFound } from "next/navigation"
import { CollectionInfo, getFileContent, getSections } from "@/collections"
import {
CollectionInfo,
getFileContent,
getSections,
getTitle,
} from "@/collections"
import { SiteBreadcrumb } from "@/components/breadcrumb"
import { Comments } from "@/components/comments"
import SectionGrid from "@/components/section-grid"
Expand Down Expand Up @@ -36,47 +43,42 @@ interface PageProps {
params: Promise<{ slug: string[] }>
}

async function getBreadcrumbItems(slug: string[]) {
// we do not want to have "docs" as breadcrumb element
// also, we do not need the index file in our breadcrumb
const combinations = removeFromArray(slug, ["docs", "index"]).reduce(
(acc: string[][], curr) => acc.concat(acc.map((sub) => [...sub, curr])),
[[]],
const getBreadcrumbItems = cache(async (slug: string[]) => {
// we do not want to have "index" as breadcrumb element
const cleanedSlug = removeFromArray(slug, ["index"])

const combinations = cleanedSlug.map((_, index) =>
cleanedSlug.slice(0, index + 1),
)

const items = []

for (const currentPageSegement of combinations) {
let collection
let collection: EntryType
let file: Awaited<ReturnType<typeof getFileContent>>
let frontmatter: z.infer<typeof frontmatterSchema> | undefined
try {
collection = await CollectionInfo.getEntry(currentPageSegement)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
if (collection.getPathSegments().includes("index")) {
file = await getFileContent(collection.getParent())
} else {
file = await getFileContent(collection)
}

frontmatter = await file?.getExportValue("frontmatter")
} catch (e: unknown) {
continue
}

if (isDirectory(collection)) {
if (!frontmatter) {
items.push({
title: collection.getTitle(),
path: ["docs", ...collection.getPathSegments()],
})
} else {
const file = await getFileContent(collection)

if (!file) {
continue
}
const frontmatter = await file.getExportValue("frontmatter")

// in case we have an index file inside a directory
// we have also to fetch the directory name, otherwise we get "Index" as title
// if there is no `frontmatter.navTitle` defined
const parentTitle = collection.getPathSegments().includes("index")
? collection.getParent().getTitle()
: null

const title = getTitle(collection, frontmatter, true)
items.push({
title: frontmatter.navTitle ?? parentTitle ?? collection.getTitle(),
title,
path: [
"docs",
...removeFromArray(collection.getPathSegments(), ["index"]),
Expand All @@ -86,17 +88,13 @@ async function getBreadcrumbItems(slug: string[]) {
}

return items
}

async function getParentTitle(slug: string[]) {
const elements = await getBreadcrumbItems(slug)

return elements.map((ele) => ele.title)
}
})

export async function generateMetadata(props: PageProps): Promise<Metadata> {
const params = await props.params
const titles = await getParentTitle(params.slug)
const breadcrumbItems = await getBreadcrumbItems(params.slug)

const titles = breadcrumbItems.map((ele) => ele.title)

return {
title: titles.join(" - "),
Expand All @@ -122,13 +120,21 @@ export default async function DocsPage(props: PageProps) {
// if we can't find an index file, but we have a valid directory
// use the directory component for rendering
if (!file && isDirectory(collection)) {
return <DirectoryContent source={collection} />
return (
<>
<DirectoryContent source={collection} />
</>
)
}

// if we have a valid file ( including the index file )
// use the file component for rendering
if (file) {
return <FileContent source={collection} />
return (
<>
<FileContent source={collection} />
</>
)
}

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

<article data-pagefind-body>
Expand All @@ -156,6 +162,7 @@ async function DirectoryContent({ source }: { source: EntryType }) {
"prose-code:before:hidden prose-code:after:hidden",
// use full width
"w-full max-w-full",
"prose-a:text-indigo-400 prose-a:hover:text-white",
)}
>
<h1
Expand Down Expand Up @@ -197,24 +204,34 @@ async function FileContent({ source }: { source: EntryType }) {
return (
<>
<div className="container py-6">
{headings.length > 0 && <MobileTableOfContents toc={headings} />}
{headings.length > 0 && frontmatter.showToc && (
<MobileTableOfContents toc={headings} />
)}

<div
className={cn("gap-8 xl:grid xl:grid-cols-[1fr_300px]", {
"mt-12 xl:mt-0": headings.length > 0,
className={cn("gap-8 xl:grid", {
"mt-12 xl:mt-0": frontmatter.showToc && headings.length > 0,
"xl:grid-cols-[1fr_300px]":
frontmatter.showToc && headings.length > 0,
"xl:grid-cols-1": !frontmatter.showToc || headings.length == 0,
})}
>
<div>
<div
className={cn("mx-auto", {
"w-full 2xl:w-6xl": !frontmatter.showToc || headings.length == 0,
"w-full 2xl:w-4xl": frontmatter.showToc && headings.length > 0,
})}
>
<SiteBreadcrumb items={breadcrumbItems} />

<div data-pagefind-body>
<h1
className="no-prose mb-2 scroll-m-20 text-4xl font-light tracking-tight lg:text-5xl"
className="no-prose mb-2 scroll-m-20 text-3xl font-light tracking-tight sm:text-4xl md:text-5xl"
data-pagefind-meta="title"
>
{frontmatter.title ?? source.getTitle()}
</h1>
<p className="mb-8 text-lg font-medium text-pretty text-gray-500 sm:text-xl/8">
<p className="text-muted-foreground mb-8 text-lg font-medium text-pretty sm:text-xl/8">
{frontmatter.description ?? ""}
</p>
<article>
Expand Down Expand Up @@ -256,27 +273,29 @@ async function FileContent({ source }: { source: EntryType }) {
<Comments />
</div>
</div>
<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">
<TableOfContents toc={headings} />

<div className="my-6 grid gap-y-4 border-t pt-6">
<div>
<a
href={file.getEditUrl()}
target="_blank"
className="text-muted-foreground hover:text-foreground flex items-center text-sm no-underline transition-colors"
>
Edit this page <ExternalLinkIcon className="ml-2 h-4 w-4" />
</a>
</div>

{lastUpdate && (
<div className="text-muted-foreground text-sm">
Last updated: {format(lastUpdate, "dd.MM.yyyy")}
{frontmatter.showToc && headings.length > 0 ? (
<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">
<TableOfContents toc={headings} />

<div className="my-6 grid gap-y-4 border-t pt-6">
<div>
<a
href={file.getEditUrl()}
target="_blank"
className="text-muted-foreground hover:text-foreground flex items-center text-sm no-underline transition-colors"
>
Edit this page <ExternalLinkIcon className="ml-2 h-4 w-4" />
</a>
</div>
)}

{lastUpdate && (
<div className="text-muted-foreground text-sm">
Last updated: {format(lastUpdate, "dd.MM.yyyy")}
</div>
)}
</div>
</div>
</div>
) : null}
</div>
</div>
</>
Expand Down
11 changes: 11 additions & 0 deletions src/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const frontmatterSchema = z.object({
navTitle: z.string().optional(),
entrypoint: z.string().optional(),
alias: z.string().optional(),
showToc: z.boolean().optional().default(true),
})

export const headingSchema = z.array(
Expand Down Expand Up @@ -86,6 +87,16 @@ export type DirectoryType = Awaited<
ReturnType<typeof CollectionInfo.getDirectory>
>

export function getTitle(
collection: EntryType,
frontmatter: z.infer<typeof frontmatterSchema>,
includeTitle = false,
) {
return includeTitle
? (frontmatter.navTitle ?? frontmatter.title ?? collection.getTitle())
: (frontmatter.navTitle ?? collection.getTitle())
}

export async function getDirectoryContent(source: EntryType) {
// first, try to get the file based on the given path

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
36 changes: 36 additions & 0 deletions src/components/heading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ElementType, ReactNode } from "react"

type IntrinsicElement = keyof JSX.IntrinsicElements
type PolymorphicComponentProps<T extends IntrinsicElement> = {
as?: T
} & JSX.IntrinsicElements[T]

const PolymorphicComponent = <T extends IntrinsicElement>({
as: elementType = "div" as T,
...rest
}: PolymorphicComponentProps<T>) => {
const Component = elementType as ElementType
return <Component {...rest} />
}

export function Heading({
level,
id,
children,
}: {
level: number
id: string
children: ReactNode
}) {
return (
<PolymorphicComponent as={`h${level}`} id={id} className="group">
{children}{" "}
<a
href={`#${id}`}
className="hidden no-underline group-hover:inline-block"
>
#
</a>
</PolymorphicComponent>
)
}
6 changes: 4 additions & 2 deletions src/components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
SidebarItem,
SidebarLabel,
} from "@/components/ui/sidebar"
import { useIsMobile } from "@/hooks/use-mobile"
import { current } from "@/lib/helpers"
import { cn } from "@/lib/utils"
import { ChevronsUpDown } from "lucide-react"
Expand Down Expand Up @@ -42,6 +43,7 @@ export function SiteSidebar({
defaultHidden?: boolean
}) {
const pathname = usePathname()
const isMobile = useIsMobile()

return (
<Sidebar className="lg:mt-12" defaultHidden={defaultHidden}>
Expand All @@ -60,9 +62,9 @@ export function SiteSidebar({
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-64"
className="w-72"
align="start"
side="right"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-muted-foreground text-xs">
Expand Down
2 changes: 1 addition & 1 deletion src/components/table-of-contents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function MobileTableOfContents({ toc }: TocProps) {
const filteredToc = toc.filter((item) => item.depth > 1 && item.depth <= 4)

return (
<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">
<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">
<DropdownMenu>
<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">
<div className="flex items-center gap-1.5 overflow-hidden px-2 py-1.5 text-left text-sm transition-all">
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const SidebarLayout = React.forwardRef<
ref={ref}
data-sidebar={state}
className={cn(
"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))]",
"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))]",
className,
)}
{...props}
Expand Down Expand Up @@ -84,7 +84,7 @@ const Sidebar = React.forwardRef<

<aside
className={cn(
"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)]",
"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)]",
defaultHidden ? "hidden" : "hidden lg:block",
)}
>
Expand Down
10 changes: 0 additions & 10 deletions src/lib/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,6 @@ export function isHidden(entry: EntryType) {
return entry.getBaseName().startsWith("_")
}

/** Create a slug from a string. */
// source: https://github.com/souporserious/renoun/blob/main/packages/renoun/src/utils/create-slug.ts
export function createSlug(input: string) {
return input
.replace(/([a-z])([A-Z])/g, "$1-$2") // Add a hyphen between lower and upper case letters
.replace(/([A-Z])([A-Z][a-z])/g, "$1-$2") // Add a hyphen between consecutive upper case letters followed by a lower case letter
.replace(/[_\s]+/g, "-") // Replace underscores and spaces with a hyphen
.toLowerCase() // Convert the entire string to lowercase
}

// source:
// https://github.com/souporserious/renoun/blob/main/packages/renoun/src/file-system/index.test.ts
async function buildTreeNavigation(entry: EntryType): Promise<TreeItem | null> {
Expand Down
Loading

0 comments on commit fb3202e

Please sign in to comment.