add breadcrumb and cleanup docs loader
All checks were successful
Deploy App / docker (ubuntu-latest) (push) Successful in 2m8s

This commit is contained in:
Lee 2024-04-20 22:16:30 +01:00
parent 325fe62569
commit f7a3bb00a5
6 changed files with 244 additions and 94 deletions

@ -1,6 +1,6 @@
--- ---
title: Minecraft Utilities Documentation title: Minecraft Utilities Documentation
description: Welcome to the Minecraft Utilities documentation! Here you can find information on how to use the various features of the API. summary: Welcome to the Minecraft Utilities documentation! Here you can find information on how to use the various features of the API.
--- ---
# Getting Started # Getting Started

@ -1,6 +1,6 @@
--- ---
title: Java Library title: Java Library
description: The Java library for Minecraft Utilities is a simple way to interact with the API using Java! summary: The Java library for Minecraft Utilities is a simple way to interact with the API using Java!
--- ---
# Java Library # Java Library

@ -1,6 +1,6 @@
--- ---
title: Javascript Library title: Javascript Library
description: The Javascript library for Minecraft Utilities is a simple way to interact with the API using Javascript! summary: The Javascript library for Minecraft Utilities is a simple way to interact with the API using Javascript!
--- ---
# Javascript Library # Javascript Library

@ -1,8 +1,17 @@
import { getDocumentation } from "@/app/common/documentation";
import { CustomMDX } from "@/app/components/mdx-components"; import { CustomMDX } from "@/app/components/mdx-components";
import { Metadata } from "next"; import { Metadata } from "next";
import { generateEmbed } from "@/app/common/embed"; import { generateEmbed } from "@/app/common/embed";
import { Title } from "@/app/components/title"; import { Title } from "@/app/components/title";
import { getDocContent, getDocsContent, getDocsDirectories, getMetadata } from "@/app/common/documentation";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/app/components/ui/breadcrumb";
import { capitalizeFirstLetter } from "@/app/common/string-utils";
type DocumentationPageParams = { type DocumentationPageParams = {
params: { params: {
@ -11,36 +20,15 @@ type DocumentationPageParams = {
}; };
export async function generateStaticParams() { export async function generateStaticParams() {
let documentationPages = getDocumentation(); let documentationPages = getDocsContent();
return documentationPages.map(page => ({ return documentationPages.map(page => ({
slug: [page.slug], slug: [page.slug],
})); }));
} }
/**
* Gets a documentation page by its slug.
*
* @param slug The slug of the documentation page.
*/
function getPage(slug?: string) {
const documentationPages = getDocumentation();
let page = documentationPages.find(page => page.slug === slug);
// Fallback to the landing page
if (!page && !slug) {
page = documentationPages.find(page => page.slug === "landing");
}
// We still can't find the page, return undefined
if (!page) {
return undefined;
}
return page;
}
export async function generateMetadata({ params: { slug } }: DocumentationPageParams): Promise<Metadata> { export async function generateMetadata({ params: { slug } }: DocumentationPageParams): Promise<Metadata> {
const page = getPage(slug?.join("/")); const page = getDocContent(slug);
// Fallback to page not found // Fallback to page not found
if (!page) { if (!page) {
@ -51,13 +39,13 @@ export async function generateMetadata({ params: { slug } }: DocumentationPagePa
} }
return generateEmbed({ return generateEmbed({
title: `${page.metadata.title} - Documentation`, title: `${page.title} - Documentation`,
description: `${page.metadata.description}\n\nClick to view this page`, description: `${page.summary}\n\nClick to view this page`,
}); });
} }
export default function Page({ params: { slug } }: DocumentationPageParams) { export default function Page({ params: { slug } }: DocumentationPageParams) {
const page = getPage(slug?.join("/")); const page = getDocContent(slug);
// Page was not found, show an error page // Page was not found, show an error page
if (!page) { if (!page) {
@ -69,11 +57,35 @@ export default function Page({ params: { slug } }: DocumentationPageParams) {
); );
} }
const slugParts = page.slug.split("/");
return ( return (
<div className="w-full px-4 flex flex-col gap-4"> <div className="w-full px-4 flex flex-col gap-4">
{slugParts.length > 1 && (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/documentation">Home</BreadcrumbLink>
</BreadcrumbItem>
{slugParts.map((slug, index, array) => {
const path = array.slice(0, index + 1).join("/");
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem key={slug}>
<BreadcrumbLink href={`/documentation/${path}`}>{capitalizeFirstLetter(slug)}</BreadcrumbLink>
</BreadcrumbItem>
</>
);
})}
</BreadcrumbList>
</Breadcrumb>
)}
{/* The documentation page title and description */} {/* The documentation page title and description */}
<div className="text-center mb-4"> <div className="text-center mb-4">
<Title title={page.metadata.title} subtitle={page.metadata.description} /> <Title title={page.title} subtitle={page.summary} />
</div> </div>
{/* The content of the documentation page */} {/* The content of the documentation page */}

@ -1,99 +1,147 @@
/* eslint-disable */
import * as fs from "node:fs"; import * as fs from "node:fs";
import path from "node:path"; import path from "node:path";
// @ts-ignore
import read from "read-file";
type Metadata = { const docsDir = path.join(process.cwd(), "documentation");
/**
* Metadata for documentation content.
*/
type DocsContentMetadata = MDXMetadata & {
/** /**
* The title of the documentation page. * The title of this content.
*/ */
title: string; title: string;
/** /**
* The description of the documentation page. * The date this content was published.
*/ */
description: string; published: string;
/**
* The summary of this content.
*/
summary: string;
}; };
/** /**
* The directory where the documentation files are stored. * Metadata for an MDX file.
*/ */
const documentationDirectory = path.join(process.cwd(), "documentation"); type MDXMetadata = {
/**
* The slug of the file, defined once read.
*/
slug: string;
/**
* The metadata of the file.
*/
metadata: {
[key: string]: string;
};
/**
* The content of the file.
*/
content: string;
};
/** /**
* Gets all the documentation files recursively. * The regex to match for metadata.
*
* @param dirPath the directory path to search for documentation files.
*/ */
function getDocumentationFiles(dirPath: string): string[] { const METADATA_REGEX: RegExp = /---\s*([\s\S]*?)\s*---/;
let files: string[] = [];
const items = fs.readdirSync(dirPath);
items.forEach(item => { /**
const itemPath = path.join(dirPath, item); * Get the directories in the
* given directory.
*/
export function getDocsDirectories(dir: string) {
const dirs: string[] = [dir];
const paths = fs.readdirSync(dir);
for (const item of paths) {
const itemPath = path.join(dir, item);
const stat = fs.statSync(itemPath); const stat = fs.statSync(itemPath);
if (stat.isDirectory()) { if (stat.isDirectory()) {
// Recursively traverse directories dirs.push(...getDocsDirectories(itemPath));
files.push(...getDocumentationFiles(itemPath));
} else if (stat.isFile() && path.extname(item) === ".md") {
// Collect markdown files
files.push(itemPath);
} }
}); }
return dirs;
return files;
} }
/** /**
* Gets the content of a documentation file. * Get the content to
* display in the docs.
*/
export function getDocsContent() {
const directories = getDocsDirectories(docsDir);
const content: DocsContentMetadata[] = [];
for (let directory of directories) {
content.push(...getMetadata<DocsContentMetadata>(directory));
}
return content;
}
export function getDocContent(path?: string[]) {
const docs = getDocsContent();
const slug = path ? path.join("/") : "landing";
return docs.find(doc => doc.slug === slug);
}
/**
* Get the metadata of mdx
* files in the given directory.
* *
* @param file the file to get the content of. * @param directory the directory to search
*/ */
function getDocumentationFileContent(file: string) { export function getMetadata<T extends MDXMetadata>(directory: string): T[] {
return read.sync(file, "utf8"); const files: string[] = fs.readdirSync(directory).filter((file: string): boolean => {
} const extension: string = path.extname(file); // The file extension
return extension === ".md" || extension === ".mdx";
/** }); // Read the MDX files
* Gets all the documentation pages. return files.map((file: string): T => {
*/ const filePath: string = path.join(directory, file); // The path of the file
export function getDocumentation() {
const files = getDocumentationFiles(documentationDirectory);
return files.map(file => {
const { metadata, content } = parseFrontmatter(getDocumentationFileContent(file));
let slug = path.relative(documentationDirectory, file).replace(/\.(md)$/, "");
slug = slug.replace(/\\/g, "/"); // Normalize path separators
return { return {
metadata, ...parseMetadata<T>(fs.readFileSync(filePath, "utf-8")),
content, slug: filePath
slug, .replace(docsDir, "")
}; .replace(/\.mdx?$/, "")
.replace(/\\/g, "/")
.substring(1),
}; // Map each file to its metadata
}); });
} }
/** /**
* Parses the frontmatter of a file. * Parse the metadata from
* the given content.
* *
* @param fileContent the content of the file. * @param content the content to parse
* @returns the metadata and content
* @template T the type of metadata
*/ */
function parseFrontmatter(fileContent: string) { function parseMetadata<T extends MDXMetadata>(content: string): T {
let frontmatterRegex = /---\s*([\s\S]*?)\s*---/; const metadataBlock: string = METADATA_REGEX.exec(content)![1]; // Get the block of metadata
let match = frontmatterRegex.exec(fileContent); content = content.replace(METADATA_REGEX, "").trim(); // Remove the metadata block from the content
let frontMatterBlock = match![1]; let metadata: Partial<{
let content = fileContent.replace(frontmatterRegex, "").trim(); [key: string]: string;
let frontMatterLines = frontMatterBlock.trim().split("\n"); }> = {}; // The metadata to return
let metadata: Partial<Metadata> = {};
frontMatterLines.forEach(line => { // Parse the metadata block as a key-value pair
let [key, ...valueArr] = line.split(": "); metadataBlock
let value = valueArr.join(": ").trim(); .trim() // Trim any leading or trailing whitespace
value = value.replace(/^['"](.*)['"]$/, "$1"); // Remove quotes .split("\n") // Get each line
metadata[key.trim() as keyof Metadata] = value; .forEach((line: string): void => {
}); const split: string[] = line.split(": "); // Split the metadata by the colon
let value: string = split[1].trim(); // The value of the metadata
value = value.replace(/^['"](.*)['"]$/, "$1"); // Remove quotes
metadata[split[0].trim()] = value; // Add the metadata to the object
});
return { metadata: metadata as Metadata, content }; // Return the metadata and content. The initial
// slug is empty, and is defined later on.
return { ...metadata, content } as T;
} }

@ -0,0 +1,90 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/app/common/utils";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className,
)}
{...props}
/>
),
);
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
({ className, ...props }, ref) => (
<li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
),
);
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return <Comp ref={ref} className={cn("transition-colors hover:text-foreground", className)} {...props} />;
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
),
);
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
<li role="presentation" aria-hidden="true" className={cn("[&>svg]:size-3.5", className)} {...props}>
{children ?? <ChevronRight />}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};