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:
192
packages/cms/contentlayer/src/client.ts
Normal file
192
packages/cms/contentlayer/src/client.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { Cms, CmsClient } from '@kit/cms';
|
||||
|
||||
import type { DocumentationPage, Post } from '../.contentlayer/generated';
|
||||
|
||||
async function getAllContentItems() {
|
||||
const { allDocumentationPages, allPosts } = await import(
|
||||
'../.contentlayer/generated'
|
||||
);
|
||||
|
||||
return [
|
||||
...allPosts.map((item) => {
|
||||
return { ...item, type: 'post' };
|
||||
}),
|
||||
...allDocumentationPages.map((item) => {
|
||||
return { ...item, type: 'page', categories: ['documentation'] };
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that represents a Content Layer CMS client.
|
||||
* This class implements the base CmsClient class.
|
||||
*
|
||||
* @class ContentlayerClient
|
||||
* @extends {CmsClient}
|
||||
*/
|
||||
export class ContentlayerClient implements CmsClient {
|
||||
async getContentItems(options?: Cms.GetContentItemsOptions) {
|
||||
const allContentItems = await getAllContentItems();
|
||||
const { startOffset, endOffset } = this.getOffset(options);
|
||||
|
||||
const promise = allContentItems
|
||||
.filter((item) => {
|
||||
const tagMatch = options?.tags
|
||||
? item.tags?.some((tag) => options.tags?.includes(tag))
|
||||
: true;
|
||||
|
||||
const categoryMatch = options?.categories
|
||||
? item.categories?.some((category) =>
|
||||
options.categories?.includes(category),
|
||||
)
|
||||
: true;
|
||||
|
||||
const typeMatch = options?.type ? item.type === options.type : true;
|
||||
const path = item._raw.flattenedPath;
|
||||
const splitPath = path.split('/');
|
||||
|
||||
const depthMatch =
|
||||
options?.depth !== undefined
|
||||
? splitPath.length - 1 === options.depth
|
||||
: true;
|
||||
|
||||
return tagMatch && categoryMatch && typeMatch && depthMatch;
|
||||
})
|
||||
.slice(startOffset, endOffset)
|
||||
.map((post) => {
|
||||
const children: Cms.ContentItem[] = [];
|
||||
|
||||
for (const item of allContentItems) {
|
||||
if (item.url.startsWith(post.url + '/')) {
|
||||
children.push(this.mapPost(item));
|
||||
}
|
||||
}
|
||||
|
||||
return this.mapPost(post, children);
|
||||
});
|
||||
|
||||
return Promise.resolve(promise);
|
||||
}
|
||||
|
||||
async getContentItemById(id: string) {
|
||||
const allContentItems = await getAllContentItems();
|
||||
const post = allContentItems.find((item) => item.slug === id);
|
||||
|
||||
if (!post) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
const children: Cms.ContentItem[] = [];
|
||||
|
||||
for (const item of allContentItems) {
|
||||
if (item.url.startsWith(post.url + '/')) {
|
||||
children.push(this.mapPost(item));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(post ? this.mapPost(post, children) : undefined);
|
||||
}
|
||||
|
||||
async getCategoryBySlug(slug: string) {
|
||||
return Promise.resolve({
|
||||
id: slug,
|
||||
name: slug,
|
||||
slug,
|
||||
});
|
||||
}
|
||||
|
||||
async getTagBySlug(slug: string) {
|
||||
return Promise.resolve({
|
||||
id: slug,
|
||||
name: slug,
|
||||
slug,
|
||||
});
|
||||
}
|
||||
|
||||
async getCategories(options?: Cms.GetCategoriesOptions) {
|
||||
const { startOffset, endOffset } = this.getOffset(options);
|
||||
const allContentItems = await getAllContentItems();
|
||||
|
||||
const categories = allContentItems
|
||||
.filter((item) => {
|
||||
if (options?.type) {
|
||||
return item.type === options.type;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.slice(startOffset, endOffset)
|
||||
.flatMap((post) => post.categories)
|
||||
.filter((category): category is string => !!category)
|
||||
.map((category) => ({
|
||||
id: category,
|
||||
name: category,
|
||||
slug: category,
|
||||
}));
|
||||
|
||||
return Promise.resolve(categories);
|
||||
}
|
||||
|
||||
async getTags(options?: Cms.GetTagsOptions) {
|
||||
const { startOffset, endOffset } = this.getOffset(options);
|
||||
const allContentItems = await getAllContentItems();
|
||||
|
||||
const tags = allContentItems
|
||||
.filter((item) => {
|
||||
if (options?.type) {
|
||||
return item.type === options.type;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.slice(startOffset, endOffset)
|
||||
.flatMap((post) => post.tags)
|
||||
.filter((tag): tag is string => !!tag)
|
||||
.map((tag) => ({
|
||||
id: tag,
|
||||
name: tag,
|
||||
slug: tag,
|
||||
}));
|
||||
|
||||
return Promise.resolve(tags);
|
||||
}
|
||||
|
||||
private getOffset(options?: { offset?: number; limit?: number }) {
|
||||
const startOffset = options?.offset ?? 0;
|
||||
const endOffset = options?.limit ? startOffset + options.limit : undefined;
|
||||
|
||||
return { startOffset, endOffset };
|
||||
}
|
||||
|
||||
private mapPost(
|
||||
post: Post | DocumentationPage,
|
||||
children: Array<Post | DocumentationPage> = [],
|
||||
): Cms.ContentItem {
|
||||
console.log(post);
|
||||
return {
|
||||
id: post.slug,
|
||||
title: post.title,
|
||||
description: post.description ?? '',
|
||||
content: post.body?.code,
|
||||
image: 'image' in post ? post.image : undefined,
|
||||
publishedAt: 'date' in post ? new Date(post.date) : new Date(),
|
||||
parentId: 'parentId' in post ? post.parentId : undefined,
|
||||
url: post.url,
|
||||
slug: post.slug,
|
||||
author: 'author' in post ? post.author : '',
|
||||
children: children.map((child) => this.mapPost(child)),
|
||||
categories:
|
||||
post.categories?.map((category) => ({
|
||||
id: category,
|
||||
name: category,
|
||||
slug: category,
|
||||
})) ?? [],
|
||||
tags:
|
||||
post.tags?.map((tag) => ({
|
||||
id: tag,
|
||||
name: tag,
|
||||
slug: tag,
|
||||
})) ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
5
packages/cms/contentlayer/src/content-renderer.tsx
Normal file
5
packages/cms/contentlayer/src/content-renderer.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Mdx } from './mdx/mdx-renderer';
|
||||
|
||||
export function ContentRenderer(props: { content: string }) {
|
||||
return <Mdx code={props.content} />;
|
||||
}
|
||||
3
packages/cms/contentlayer/src/index.ts
Normal file
3
packages/cms/contentlayer/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './client';
|
||||
export * from './mdx/mdx-renderer';
|
||||
export * from './content-renderer';
|
||||
97
packages/cms/contentlayer/src/mdx/mdx-renderer.module.css
Normal file
97
packages/cms/contentlayer/src/mdx/mdx-renderer.module.css
Normal file
@@ -0,0 +1,97 @@
|
||||
.MDX h1 {
|
||||
@apply mt-14 text-4xl font-bold;
|
||||
}
|
||||
|
||||
.MDX h2 {
|
||||
@apply mb-4 mt-12 text-2xl font-semibold lg:text-3xl;
|
||||
}
|
||||
|
||||
.MDX h3 {
|
||||
@apply mt-10 text-2xl font-bold;
|
||||
}
|
||||
|
||||
.MDX h4 {
|
||||
@apply mt-8 text-xl font-bold;
|
||||
}
|
||||
|
||||
.MDX h5 {
|
||||
@apply mt-6 text-lg font-semibold;
|
||||
}
|
||||
|
||||
.MDX h6 {
|
||||
@apply mt-2 text-base font-medium;
|
||||
}
|
||||
|
||||
/**
|
||||
Tailwind "dark" variants do not work with CSS Modules
|
||||
We work it around using :global(.dark)
|
||||
For more info: https://github.com/tailwindlabs/tailwindcss/issues/3258#issuecomment-770215347
|
||||
*/
|
||||
:global(.dark) .MDX h1,
|
||||
:global(.dark) .MDX h2,
|
||||
:global(.dark) .MDX h3,
|
||||
:global(.dark) .MDX h4,
|
||||
:global(.dark) .MDX h5,
|
||||
:global(.dark) .MDX h6 {
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
.MDX p {
|
||||
@apply mb-4 mt-2 text-base leading-7;
|
||||
}
|
||||
|
||||
.MDX li {
|
||||
@apply relative my-1.5 text-base leading-7;
|
||||
}
|
||||
|
||||
.MDX ul > li:before {
|
||||
content: '-';
|
||||
|
||||
@apply mr-2;
|
||||
}
|
||||
|
||||
.MDX ol > li:before {
|
||||
@apply inline-flex font-medium;
|
||||
|
||||
content: counters(counts, '.') '. ';
|
||||
font-feature-settings: 'tnum';
|
||||
}
|
||||
|
||||
.MDX b,
|
||||
.MDX strong {
|
||||
@apply font-bold;
|
||||
}
|
||||
|
||||
:global(.dark) .MDX b,
|
||||
:global(.dark) .MDX strong {
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
.MDX img,
|
||||
.MDX video {
|
||||
@apply rounded-md;
|
||||
}
|
||||
|
||||
.MDX ul,
|
||||
.MDX ol {
|
||||
@apply pl-1;
|
||||
}
|
||||
|
||||
.MDX ol > li {
|
||||
counter-increment: counts;
|
||||
}
|
||||
|
||||
.MDX ol > li:before {
|
||||
@apply mr-2 inline-flex font-semibold;
|
||||
|
||||
content: counters(counts, '.') '. ';
|
||||
font-feature-settings: 'tnum';
|
||||
}
|
||||
|
||||
.MDX blockquote {
|
||||
@apply my-4 border-l-4 border-primary bg-muted px-6 py-4 text-lg font-medium text-gray-600;
|
||||
}
|
||||
|
||||
.MDX pre {
|
||||
@apply my-6 text-sm text-current;
|
||||
}
|
||||
21
packages/cms/contentlayer/src/mdx/mdx-renderer.tsx
Normal file
21
packages/cms/contentlayer/src/mdx/mdx-renderer.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { MDXComponents as MDXComponentsType } from 'mdx/types';
|
||||
import { getMDXComponent } from 'next-contentlayer/hooks';
|
||||
|
||||
import { MDXComponents } from '@kit/ui/mdx-components';
|
||||
|
||||
// @ts-ignore: ignore weird error
|
||||
import styles from './mdx-renderer.module.css';
|
||||
|
||||
export function Mdx({
|
||||
code,
|
||||
}: React.PropsWithChildren<{
|
||||
code: string;
|
||||
}>) {
|
||||
const Component = getMDXComponent(code);
|
||||
|
||||
return (
|
||||
<div className={styles.MDX}>
|
||||
<Component components={MDXComponents as unknown as MDXComponentsType} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user