Refactor CMS to handle ContentLayer and WordPress platforms
This commit refactors the CMS to handle two platforms: ContentLayer and WordPress. The CMS layer is abstracted into a core package, and separate implementations for each platform are created. This change allows the app to switch the CMS type based on environment variable, which can improve the flexibility of content management. It also updates several functions in the `server-sitemap.xml` route to accommodate these changes and generate sitemaps based on the CMS client. Further, documentation content and posts have been relocated to align with the new structure. Notably, this refactor is a comprehensive update to the way the CMS is structured and managed.
This commit is contained in:
@@ -2,20 +2,20 @@ import { cache } from 'react';
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { allDocumentationPages } from 'contentlayer/generated';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
|
||||
import { ContentRenderer, createCmsClient } from '@kit/cms';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Mdx } from '@kit/ui/mdx';
|
||||
|
||||
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
||||
import { DocsCards } from '~/(marketing)/docs/_components/docs-cards';
|
||||
import { DocumentationPageLink } from '~/(marketing)/docs/_components/documentation-page-link';
|
||||
import { getDocumentationPageTree } from '~/(marketing)/docs/_lib/get-documentation-page-tree';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
const getPageBySlug = cache((slug: string) => {
|
||||
return allDocumentationPages.find((post) => post.resolvedPath === slug);
|
||||
const getPageBySlug = cache(async (slug: string) => {
|
||||
const client = await createCmsClient();
|
||||
|
||||
return client.getContentItemById(slug);
|
||||
});
|
||||
|
||||
interface PageParams {
|
||||
@@ -24,8 +24,8 @@ interface PageParams {
|
||||
};
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params }: PageParams) => {
|
||||
const page = getPageBySlug(params.slug.join('/'));
|
||||
export const generateMetadata = async ({ params }: PageParams) => {
|
||||
const page = await getPageBySlug(params.slug.join('/'));
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
@@ -39,16 +39,13 @@ export const generateMetadata = ({ params }: PageParams) => {
|
||||
};
|
||||
};
|
||||
|
||||
function DocumentationPage({ params }: PageParams) {
|
||||
const page = getPageBySlug(params.slug.join('/'));
|
||||
async function DocumentationPage({ params }: PageParams) {
|
||||
const page = await getPageBySlug(params.slug.join('/'));
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { nextPage, previousPage, children } =
|
||||
getDocumentationPageTree(page.resolvedPath) ?? {};
|
||||
|
||||
const description = page?.description ?? '';
|
||||
|
||||
return (
|
||||
@@ -60,40 +57,11 @@ function DocumentationPage({ params }: PageParams) {
|
||||
className={'items-start'}
|
||||
/>
|
||||
|
||||
<Mdx code={page.body.code} />
|
||||
<ContentRenderer content={page.content} />
|
||||
|
||||
<If condition={children}>
|
||||
<DocsCards pages={children ?? []} />
|
||||
<If condition={page.children}>
|
||||
<DocsCards pages={page.children ?? []} />
|
||||
</If>
|
||||
|
||||
<div
|
||||
className={
|
||||
'flex flex-col justify-between space-y-4 md:flex-row md:space-x-8' +
|
||||
' md:space-y-0'
|
||||
}
|
||||
>
|
||||
<div className={'w-full'}>
|
||||
<If condition={previousPage}>
|
||||
{(page) => (
|
||||
<DocumentationPageLink
|
||||
page={page}
|
||||
before={<ChevronLeft className={'w-4'} />}
|
||||
/>
|
||||
)}
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<div className={'w-full'}>
|
||||
<If condition={nextPage}>
|
||||
{(page) => (
|
||||
<DocumentationPageLink
|
||||
page={page}
|
||||
after={<ChevronRight className={'w-4'} />}
|
||||
/>
|
||||
)}
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,18 +4,18 @@ import { ChevronRight } from 'lucide-react';
|
||||
|
||||
export const DocsCard: React.FC<
|
||||
React.PropsWithChildren<{
|
||||
label: string;
|
||||
title: string;
|
||||
subtitle?: string | null;
|
||||
link?: { url: string; label: string };
|
||||
}>
|
||||
> = ({ label, subtitle, children, link }) => {
|
||||
> = ({ title, subtitle, children, link }) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div
|
||||
className={`flex grow flex-col space-y-2.5 border bg-background p-6
|
||||
${link ? 'rounded-t-2xl border-b-0' : 'rounded-2xl'}`}
|
||||
>
|
||||
<h3 className="mt-0 text-lg font-semibold dark:text-white">{label}</h3>
|
||||
<h3 className="mt-0 text-lg font-semibold dark:text-white">{title}</h3>
|
||||
|
||||
{subtitle && (
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
@@ -31,7 +31,7 @@ export const DocsCard: React.FC<
|
||||
<span className={'flex items-center space-x-2'}>
|
||||
<Link
|
||||
className={'text-sm font-medium hover:underline'}
|
||||
href={`/docs/${link.url}`}
|
||||
href={link.url}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { DocumentationPage } from 'contentlayer/generated';
|
||||
import { Cms } from '@kit/cms';
|
||||
|
||||
import { DocsCard } from './docs-card';
|
||||
|
||||
export function DocsCards({ pages }: { pages: DocumentationPage[] }) {
|
||||
export function DocsCards({ pages }: { pages: Cms.ContentItem[] }) {
|
||||
return (
|
||||
<div className={'grid grid-cols-1 gap-8 lg:grid-cols-2'}>
|
||||
{pages.map((item) => {
|
||||
return (
|
||||
<DocsCard
|
||||
key={item.label}
|
||||
label={item.label}
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
subtitle={item.description}
|
||||
link={{
|
||||
url: item.resolvedPath,
|
||||
label: item.cardCTA ?? 'Read more',
|
||||
url: item.url,
|
||||
label: 'Read more',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -5,33 +5,21 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { ChevronDown, Menu } from 'lucide-react';
|
||||
import { Menu } from 'lucide-react';
|
||||
|
||||
import { Cms } from '@kit/cms';
|
||||
import { isBrowser } from '@kit/shared/utils';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import type { ProcessedDocumentationPage } from '~/(marketing)/docs/_lib/build-documentation-tree';
|
||||
|
||||
const DocsNavLink: React.FC<{
|
||||
label: string;
|
||||
url: string;
|
||||
level: number;
|
||||
activePath: string;
|
||||
collapsible: boolean;
|
||||
collapsed: boolean;
|
||||
toggleCollapsed: () => void;
|
||||
}> = ({
|
||||
label,
|
||||
url,
|
||||
level,
|
||||
activePath,
|
||||
collapsible,
|
||||
collapsed,
|
||||
toggleCollapsed,
|
||||
}) => {
|
||||
}> = ({ label, url, level, activePath }) => {
|
||||
const isCurrent = url == activePath;
|
||||
const isFirstLevel = level === 0;
|
||||
|
||||
@@ -39,76 +27,51 @@ const DocsNavLink: React.FC<{
|
||||
<div className={getNavLinkClassName(isCurrent, isFirstLevel)}>
|
||||
<Link
|
||||
className="flex h-full max-w-full grow items-center space-x-2"
|
||||
href={`/docs/${url}`}
|
||||
href={url}
|
||||
>
|
||||
<span className="block max-w-full truncate">{label}</span>
|
||||
</Link>
|
||||
|
||||
{collapsible && (
|
||||
<button
|
||||
aria-label="Toggle children"
|
||||
onClick={toggleCollapsed}
|
||||
className="mr-2 shrink-0 px-2 py-1"
|
||||
>
|
||||
<span
|
||||
className={`block w-2.5 ${collapsed ? '-rotate-90 transform' : ''}`}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Node: React.FC<{
|
||||
node: ProcessedDocumentationPage;
|
||||
node: Cms.ContentItem;
|
||||
level: number;
|
||||
activePath: string;
|
||||
}> = ({ node, level, activePath }) => {
|
||||
const [collapsed, setCollapsed] = useState<boolean>(node.collapsed ?? false);
|
||||
const toggleCollapsed = () => setCollapsed(!collapsed);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
activePath == node.resolvedPath ||
|
||||
node.children.map((_) => _.resolvedPath).includes(activePath)
|
||||
) {
|
||||
setCollapsed(false);
|
||||
}
|
||||
}, [activePath, node.children, node.resolvedPath]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocsNavLink
|
||||
label={node.label}
|
||||
url={node.resolvedPath}
|
||||
label={node.title}
|
||||
url={node.url}
|
||||
level={level}
|
||||
activePath={activePath}
|
||||
collapsible={node.collapsible}
|
||||
collapsed={collapsed}
|
||||
toggleCollapsed={toggleCollapsed}
|
||||
/>
|
||||
|
||||
{node.children.length > 0 && !collapsed && (
|
||||
<Tree tree={node.children} level={level + 1} activePath={activePath} />
|
||||
{(node.children ?? []).length > 0 && (
|
||||
<Tree
|
||||
pages={node.children ?? []}
|
||||
level={level + 1}
|
||||
activePath={activePath}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function Tree({
|
||||
tree,
|
||||
pages,
|
||||
level,
|
||||
activePath,
|
||||
}: {
|
||||
tree: ProcessedDocumentationPage[];
|
||||
pages: Cms.ContentItem[];
|
||||
level: number;
|
||||
activePath: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('w-full space-y-2.5 pl-3', level > 0 ? 'border-l' : '')}>
|
||||
{tree.map((treeNode, index) => (
|
||||
{pages.map((treeNode, index) => (
|
||||
<Node
|
||||
key={index}
|
||||
node={treeNode}
|
||||
@@ -120,11 +83,7 @@ function Tree({
|
||||
);
|
||||
}
|
||||
|
||||
export default function DocsNavigation({
|
||||
tree,
|
||||
}: {
|
||||
tree: ProcessedDocumentationPage[];
|
||||
}) {
|
||||
export function DocsNavigation({ pages }: { pages: Cms.ContentItem[] }) {
|
||||
const activePath = usePathname().replace('/docs/', '');
|
||||
|
||||
return (
|
||||
@@ -135,11 +94,14 @@ export default function DocsNavigation({
|
||||
}}
|
||||
className="sticky top-2 hidden w-80 shrink-0 border-r p-4 lg:flex"
|
||||
>
|
||||
<Tree tree={tree} level={0} activePath={activePath} />
|
||||
<Tree pages={pages} level={0} activePath={activePath} />
|
||||
</aside>
|
||||
|
||||
<div className={'lg:hidden'}>
|
||||
<FloatingDocumentationNavigation tree={tree} activePath={activePath} />
|
||||
<FloatingDocumentationNavigation
|
||||
pages={pages}
|
||||
activePath={activePath}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -159,10 +121,10 @@ function getNavLinkClassName(isCurrent: boolean, isFirstLevel: boolean) {
|
||||
}
|
||||
|
||||
function FloatingDocumentationNavigation({
|
||||
tree,
|
||||
pages,
|
||||
activePath,
|
||||
}: React.PropsWithChildren<{
|
||||
tree: ProcessedDocumentationPage[];
|
||||
pages: Cms.ContentItem[];
|
||||
activePath: string;
|
||||
}>) {
|
||||
const body = useMemo(() => {
|
||||
@@ -210,7 +172,7 @@ function FloatingDocumentationNavigation({
|
||||
>
|
||||
<Heading level={1}>Table of Contents</Heading>
|
||||
|
||||
<Tree tree={tree} level={0} activePath={activePath} />
|
||||
<Tree pages={pages} level={0} activePath={activePath} />
|
||||
</div>
|
||||
</If>
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import type { DocumentationPage } from 'contentlayer/generated';
|
||||
|
||||
import { If } from '@kit/ui/if';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
@@ -10,7 +8,10 @@ export function DocumentationPageLink({
|
||||
before,
|
||||
after,
|
||||
}: React.PropsWithChildren<{
|
||||
page: DocumentationPage;
|
||||
page: {
|
||||
url: string;
|
||||
title: string;
|
||||
};
|
||||
before?: React.ReactNode;
|
||||
after?: React.ReactNode;
|
||||
}>) {
|
||||
@@ -23,7 +24,7 @@ export function DocumentationPageLink({
|
||||
'justify-end self-end': after,
|
||||
},
|
||||
)}
|
||||
href={`/docs/${page.resolvedPath}`}
|
||||
href={page.url}
|
||||
>
|
||||
<If condition={before}>{(node) => <>{node}</>}</If>
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
import type { DocumentationPage } from 'contentlayer/generated';
|
||||
|
||||
export interface ProcessedDocumentationPage extends DocumentationPage {
|
||||
collapsible: boolean;
|
||||
pathSegments: string[];
|
||||
nextPage: ProcessedDocumentationPage | DocumentationPage | undefined;
|
||||
previousPage: ProcessedDocumentationPage | DocumentationPage | undefined;
|
||||
children: DocsTree;
|
||||
}
|
||||
|
||||
export type DocsTree = ProcessedDocumentationPage[];
|
||||
|
||||
/**
|
||||
* Build a tree of documentation pages from a flat list of pages with path segments
|
||||
* @param docs
|
||||
* @param parentPathNames
|
||||
*/
|
||||
export const buildDocumentationTree = cache(
|
||||
(docs: DocumentationPage[], parentPathNames: string[] = []): DocsTree => {
|
||||
const level = parentPathNames.length;
|
||||
|
||||
const pages = docs
|
||||
.filter(
|
||||
(_) =>
|
||||
_.pathSegments.length === level + 1 &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
_.pathSegments
|
||||
.map(({ pathName }: { pathName: string }) => pathName)
|
||||
.join('/')
|
||||
.startsWith(parentPathNames.join('/')),
|
||||
)
|
||||
.sort(
|
||||
(a, b) => a.pathSegments[level].order - b.pathSegments[level].order,
|
||||
);
|
||||
|
||||
return pages.map((doc, index) => {
|
||||
const children = buildDocumentationTree(
|
||||
docs,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
doc.pathSegments.map(({ pathName }: { pathName: string }) => pathName),
|
||||
);
|
||||
|
||||
return {
|
||||
...doc,
|
||||
pathSegments: doc.pathSegments || ([] as string[]),
|
||||
collapsible: children.length > 0,
|
||||
nextPage: children[0] ?? pages[index + 1],
|
||||
previousPage: pages[index - 1],
|
||||
children,
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1,45 +0,0 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
import type { DocumentationPage } from 'contentlayer/generated';
|
||||
import { allDocumentationPages } from 'contentlayer/generated';
|
||||
|
||||
import { buildDocumentationTree } from './build-documentation-tree';
|
||||
|
||||
/**
|
||||
* Retrieves a specific documentation page from the page tree by its path.
|
||||
*
|
||||
* @param {string} pagePath - The path of the documentation page to retrieve.
|
||||
* @returns {DocumentationPageWithChildren | undefined} The documentation page found in the tree, if any.
|
||||
*/
|
||||
export const getDocumentationPageTree = cache((pagePath: string) => {
|
||||
const tree = buildDocumentationTree(allDocumentationPages);
|
||||
|
||||
type DocumentationPageWithChildren = DocumentationPage & {
|
||||
previousPage?: DocumentationPage | null;
|
||||
nextPage?: DocumentationPage | null;
|
||||
children?: DocumentationPage[];
|
||||
};
|
||||
|
||||
const findPageInTree = (
|
||||
pages: DocumentationPageWithChildren[],
|
||||
path: string,
|
||||
): DocumentationPageWithChildren | undefined => {
|
||||
for (const page of pages) {
|
||||
if (page.resolvedPath === path) {
|
||||
return page;
|
||||
}
|
||||
|
||||
const hasChildren = page.children && page.children.length > 0;
|
||||
|
||||
if (hasChildren) {
|
||||
const foundPage = findPageInTree(page.children ?? [], path);
|
||||
|
||||
if (foundPage) {
|
||||
return foundPage;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return findPageInTree(tree, pagePath);
|
||||
});
|
||||
@@ -1,15 +1,22 @@
|
||||
import { allDocumentationPages } from 'contentlayer/generated';
|
||||
import { createCmsClient } from '@kit/cms';
|
||||
|
||||
import DocsNavigation from '~/(marketing)/docs/_components/docs-navigation';
|
||||
import { buildDocumentationTree } from '~/(marketing)/docs/_lib/build-documentation-tree';
|
||||
import { DocsNavigation } from '~/(marketing)/docs/_components/docs-navigation';
|
||||
|
||||
function DocsLayout({ children }: React.PropsWithChildren) {
|
||||
const tree = buildDocumentationTree(allDocumentationPages);
|
||||
async function DocsLayout({ children }: React.PropsWithChildren) {
|
||||
const cms = await createCmsClient();
|
||||
|
||||
const pages = await cms.getContentItems({
|
||||
type: 'page',
|
||||
categories: ['documentation'],
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
console.log(pages);
|
||||
|
||||
return (
|
||||
<div className={'container mx-auto'}>
|
||||
<div className={'flex'}>
|
||||
<DocsNavigation tree={tree} />
|
||||
<DocsNavigation pages={pages} />
|
||||
|
||||
<div className={'flex w-full flex-col items-center'}>{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { allDocumentationPages } from 'contentlayer/generated';
|
||||
import { createCmsClient } from '@kit/cms';
|
||||
|
||||
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
||||
import { DocsCards } from '~/(marketing)/docs/_components/docs-cards';
|
||||
import { buildDocumentationTree } from '~/(marketing)/docs/_lib/build-documentation-tree';
|
||||
import appConfig from '~/config/app.config';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
@@ -10,8 +9,16 @@ export const metadata = {
|
||||
title: `Documentation - ${appConfig.name}`,
|
||||
};
|
||||
|
||||
function DocsPage() {
|
||||
const tree = buildDocumentationTree(allDocumentationPages);
|
||||
async function DocsPage() {
|
||||
const client = await createCmsClient();
|
||||
|
||||
const docs = await client.getContentItems({
|
||||
type: 'page',
|
||||
categories: ['documentation'],
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
console.log(docs);
|
||||
|
||||
return (
|
||||
<div className={'my-8 flex flex-col space-y-16'}>
|
||||
@@ -21,7 +28,7 @@ function DocsPage() {
|
||||
/>
|
||||
|
||||
<div>
|
||||
<DocsCards pages={tree ?? []} />
|
||||
<DocsCards pages={docs} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user