Documentation Updates (#79)

* Docs: Added Shadcn sidebar; added algorithm to automatically infer parents without needing to specify it.
* Extracted Markdoc compilation in a separate file
* Site Navigation: simplify nav by removing the border
* Docs Navigation: added TOC; improved layout on mobile
This commit is contained in:
Giancarlo Buomprisco
2024-10-30 13:49:44 +01:00
committed by GitHub
parent 6490102e9f
commit 9615d1a4bb
20 changed files with 551 additions and 266 deletions

View File

@@ -1,3 +1,3 @@
export function KeystaticContentRenderer(props: { content: unknown }) {
return <div>{props.content as React.ReactNode}</div>;
return props.content as React.ReactNode;
}

View File

@@ -1,13 +1,8 @@
import React from 'react';
import { Cms, CmsClient } from '@kit/cms-types';
import { createKeystaticReader } from './create-reader';
import {
CustomMarkdocComponents,
CustomMarkdocTags,
} from './custom-components';
import { PostEntryProps } from './keystatic.config';
import { renderMarkdoc } from './markdoc';
export function createKeystaticClient() {
return new KeystaticClient();
@@ -85,12 +80,78 @@ class KeystaticClient implements CmsClient {
return right - left;
});
const items = await Promise.all(
filtered.slice(startOffset, endOffset).map(async (item) => {
const children = docs.filter((item) => item.entry.parent === item.slug);
function processItems(items: typeof docs) {
const result: typeof docs = [...items];
return this.mapPost(item, children);
}),
const indexFiles = items.filter((item) => {
const parts = item.slug.split('/');
return (
parts.length > 1 &&
parts[parts.length - 1] === parts[parts.length - 2]
);
});
function findParentIndex(pathParts: string[]): string | null {
// Try each level up from the current path until we find an index file
for (let i = pathParts.length - 1; i > 0; i--) {
const currentPath = pathParts.slice(0, i).join('/');
const possibleParent = indexFiles.find((indexFile) => {
const indexParts = indexFile.slug.split('/');
const indexFolderPath = indexParts.slice(0, -1).join('/');
return indexFolderPath === currentPath;
});
if (possibleParent) {
return possibleParent.slug;
}
}
return null;
}
result.forEach((item) => {
const pathParts = item.slug.split('/');
// Skip if this is a root level index file (e.g., "authentication/authentication")
if (pathParts.length === 2 && pathParts[0] === pathParts[1]) {
item.entry.parent = null;
return;
}
// Check if current item is an index file
const isIndexFile =
pathParts[pathParts.length - 1] === pathParts[pathParts.length - 2];
if (isIndexFile) {
// For index files, find parent in the level above
const parentPath = pathParts.slice(0, -2);
if (parentPath.length > 0) {
item.entry.parent = findParentIndex(
parentPath.concat(parentPath[parentPath.length - 1]!),
);
} else {
item.entry.parent = null;
}
} else {
// For regular files, find parent in the current folder
item.entry.parent = findParentIndex(pathParts);
}
});
return result;
}
const itemsWithParents = processItems(filtered);
const items = await Promise.all(
itemsWithParents
.slice(startOffset, endOffset)
.sort((a, b) => {
return (a.entry.order ?? 0) - (b.entry.order ?? 0);
})
.map((item) => this.mapPost(item)),
);
return {
@@ -157,25 +218,17 @@ class KeystaticClient implements CmsClient {
slug: string;
},
>(item: Type, children: Type[] = []): Promise<Cms.ContentItem> {
const { transform, renderers } = await import('@markdoc/markdoc');
const publishedAt = item.entry.publishedAt
? new Date(item.entry.publishedAt)
: new Date();
const markdoc = await item.entry.content();
const content = transform(markdoc.node, {
tags: CustomMarkdocTags,
});
const html = renderers.react(content, React, {
components: CustomMarkdocComponents,
});
const content = await item.entry.content();
const html = await renderMarkdoc(content.node);
return {
id: item.slug,
title: item.entry.title,
label: item.entry.label,
url: item.slug,
slug: item.slug,
description: item.entry.description,
@@ -201,7 +254,7 @@ class KeystaticClient implements CmsClient {
parentId: item.entry.parent ?? undefined,
order: item.entry.order ?? 1,
children: await Promise.all(
children.map(async (child) => this.mapPost(child, [])),
children.map((child) => this.mapPost(child, [])),
),
};
}

View File

@@ -66,6 +66,7 @@ function getKeystaticCollections(path: string) {
format: { contentField: 'content' },
schema: {
title: fields.slug({ name: { label: 'Title' } }),
label: fields.text({ label: 'Label', validation: { isRequired: false } }),
image: fields.image({
label: 'Image',
directory: 'public/site/images',
@@ -101,6 +102,7 @@ function getKeystaticCollections(path: string) {
format: { contentField: 'content' },
schema: {
title: fields.slug({ name: { label: 'Title' } }),
label: fields.text({ label: 'Label', validation: { isRequired: false } }),
content: getContentField(),
image: fields.image({
label: 'Image',

View File

@@ -0,0 +1,42 @@
// Or replace this with your own function
import { Config, Node, RenderableTreeNode, Tag } from '@markdoc/markdoc';
function generateID(
children: Array<RenderableTreeNode>,
attributes: Record<string, unknown>,
) {
if (attributes.id && typeof attributes.id === 'string') {
return attributes.id;
}
return children
.filter((child) => typeof child === 'string')
.join(' ')
.replace(/[?]/g, '')
.replace(/\s+/g, '-')
.toLowerCase();
}
const heading = {
children: ['inline'],
attributes: {
id: { type: String },
level: { type: Number, required: true, default: 1 },
},
transform(node: Node, config: Config) {
const attributes = node.transformAttributes(config);
const children = node.transformChildren(config);
const id = generateID(children, attributes);
return new Tag(
`h${node.attributes.level}`,
{ ...attributes, id },
children,
);
},
};
export const MarkdocNodes = {
heading,
};

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { Node } from '@markdoc/markdoc';
import {
CustomMarkdocComponents,
CustomMarkdocTags,
} from './custom-components';
import { MarkdocNodes } from './markdoc-nodes';
/**
* @name renderMarkdoc
* @description Renders a Markdoc tree to React
*/
export async function renderMarkdoc(node: Node) {
const { transform, renderers } = await import('@markdoc/markdoc');
const content = transform(node, {
tags: {
...CustomMarkdocTags,
},
nodes: {
...MarkdocNodes,
},
});
return renderers.react(content, React, {
components: CustomMarkdocComponents,
});
}

View File

@@ -3,6 +3,7 @@ export namespace Cms {
export interface ContentItem {
id: string;
title: string;
label: string | undefined;
url: string;
description: string | undefined;
content: unknown;

View File

@@ -139,6 +139,7 @@ class WordpressClient implements CmsClient {
return {
id: item.id.toString(),
title: item.title.rendered,
label: item.title.rendered,
content: item.content.rendered,
description: item.excerpt.rendered,
image,
@@ -217,6 +218,7 @@ class WordpressClient implements CmsClient {
description: item.excerpt.rendered,
children: [],
title: item.title.rendered,
label: item.title.rendered,
content: item.content.rendered,
slug: item.slug,
publishedAt: item.date,

View File

@@ -209,7 +209,7 @@ const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
side?: 'left' | 'right';
variant?: 'sidebar' | 'floating' | 'inset';
variant?: 'sidebar' | 'floating' | 'inset' | 'ghost';
collapsible?: 'offcanvas' | 'icon' | 'none';
}
>(
@@ -285,7 +285,13 @@ const Sidebar = React.forwardRef<
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
className={cn(
'w-[--sidebar-width] p-0 text-sidebar-foreground [&>button]:hidden',
{
'bg-background': variant === 'ghost',
'bg-sidebar': variant !== 'ghost',
},
)}
style={
{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
@@ -313,12 +319,15 @@ const Sidebar = React.forwardRef<
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
'relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear',
'relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]',
{
'h-svh': variant !== 'ghost',
},
)}
/>
<div
@@ -337,7 +346,12 @@ const Sidebar = React.forwardRef<
>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
className={cn(
'flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow',
{
'bg-background': variant === 'ghost',
},
)}
>
{children}
</div>
@@ -862,11 +876,7 @@ export function SidebarNavigation({
const ContentContainer = (props: React.PropsWithChildren) => {
if (item.collapsible) {
return (
<CollapsibleContent>
{props.children}
</CollapsibleContent>
);
return <CollapsibleContent>{props.children}</CollapsibleContent>;
}
return props.children;