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:
giancarlo
2024-04-01 19:47:51 +08:00
parent d6004f2f7e
commit 6b72206b00
62 changed files with 1313 additions and 690 deletions

View File

@@ -0,0 +1,9 @@
# CMS/Wordpress - @kit/wordpress
Implementation of the CMS layer using the [Wordpress](https://wordpress.org) library. [WIP - not yet working]
This implementation is used when the host app's environment variable is set as:
```
CMS_TYPE=wordpress
```

View File

@@ -0,0 +1,41 @@
{
"name": "@kit/wordpress",
"private": true,
"version": "0.1.0",
"scripts": {
"clean": "git clean -xdf .turbo node_modules",
"format": "prettier --check \"**/*.{ts,tsx}\"",
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"build": "contentlayer build"
},
"prettier": "@kit/prettier-config",
"exports": {
".": "./src/index.ts"
},
"dependencies": {},
"peerDependencies": {
"@kit/cms": "workspace:^",
"@kit/ui": "workspace:^"
},
"devDependencies": {
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"wp-types": "^3.64.0"
},
"eslintConfig": {
"root": true,
"extends": [
"@kit/eslint-config/base",
"@kit/eslint-config/react"
]
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
}
}

View File

@@ -0,0 +1 @@
export * from './wp-client';

View File

@@ -0,0 +1,284 @@
import type {
WP_REST_API_Category,
WP_REST_API_Post,
WP_REST_API_Tag,
} from 'wp-types';
import { Cms, CmsClient } from '@kit/cms';
import GetTagsOptions = Cms.GetTagsOptions;
/**
* @name WordpressClient
* @description Represents a client for interacting with a Wordpress CMS.
* Implements the CmsClient interface.
*/
export class WordpressClient implements CmsClient {
private readonly apiUrl: string;
constructor(apiUrl = process.env.WORDPRESS_API_URL as string) {
this.apiUrl = apiUrl;
}
async getContentItems(options?: Cms.GetContentItemsOptions) {
let endpoint: string;
switch (options?.type) {
case 'post':
endpoint = '/wp-json/wp/v2/posts';
break;
case 'page':
endpoint = '/wp-json/wp/v2/pages';
break;
default:
endpoint = '/wp-json/wp/v2/posts';
}
const url = new URL(this.apiUrl + endpoint);
if (options?.limit) {
url.searchParams.append('per_page', options.limit.toString());
}
if (options?.offset) {
url.searchParams.append('offset', options.offset.toString());
}
if (options?.categories) {
url.searchParams.append('categories', options.categories.join(','));
}
if (options?.tags) {
url.searchParams.append('tags', options.tags.join(','));
}
const response = await fetch(url.toString());
const data = (await response.json()) as WP_REST_API_Post[];
return Promise.all(
data.map(async (item) => {
// Fetch author, categories, and tags as before...
let parentId: string | undefined;
if (item.parent) {
parentId = item.parent.toString();
}
let children: Cms.ContentItem[] = [];
const embeddedChildren = (
item._embedded ? item._embedded['wp:children'] : []
) as WP_REST_API_Post[];
if (options?.depth && options.depth > 0) {
children = await Promise.all(
embeddedChildren.map(async (child) => {
const childAuthor = await this.getAuthor(child.author);
const childCategories = await this.getCategoriesByIds(
child.categories ?? [],
);
const childTags = await this.getTagsByIds(child.tags ?? []);
return {
id: child.id.toString(),
title: child.title.rendered,
type: child.type as Cms.ContentType,
image: child.featured_media,
description: child.excerpt.rendered,
url: child.link,
content: child.content.rendered,
slug: child.slug,
publishedAt: new Date(child.date),
author: childAuthor?.name,
categories: childCategories.map((category) => category.name),
tags: childTags.map((tag) => tag.name),
parentId: child.parent?.toString(),
};
}),
);
}
const author = await this.getAuthor(item.author);
const categories = await this.getCategoriesByIds(item.categories ?? []);
const tags = await this.getTagsByIds(item.tags ?? []);
return {
id: item.id.toString(),
title: item.title.rendered,
content: item.content.rendered,
description: item.excerpt.rendered,
image: item.featured_media,
url: item.link,
slug: item.slug,
publishedAt: new Date(item.date),
author: author?.name,
categories: categories.map((category) => category.name),
tags: tags.map((tag) => tag.name),
type: item.type as Cms.ContentType,
parentId,
children,
};
}),
);
}
async getContentItemById(slug: string) {
const url = `${this.apiUrl}/wp-json/wp/v2/posts?slug=${slug}`;
const response = await fetch(url);
const data = (await response.json()) as WP_REST_API_Post[];
const item = data[0];
if (!item) {
return;
}
const author = await this.getAuthor(item.author);
const categories = await this.getCategoriesByIds(item.categories ?? []);
const tags = await this.getTagsByIds(item.tags ?? []);
return {
id: item.id,
image: item.featured_media,
url: item.link,
description: item.excerpt.rendered,
type: item.type as Cms.ContentType,
children: [],
title: item.title.rendered,
content: item.content.rendered,
slug: item.slug,
publishedAt: new Date(item.date),
author: author?.name,
categories: categories.map((category) => category.name),
tags: tags.map((tag) => tag.name),
};
}
async getCategoryBySlug(slug: string) {
const url = `${this.apiUrl}/wp-json/wp/v2/categories?slug=${slug}`;
const response = await fetch(url);
const data = await response.json();
if (data.length === 0) {
return;
}
const item = data[0] as WP_REST_API_Category;
return {
id: item.id.toString(),
name: item.name,
slug: item.slug,
};
}
async getTagBySlug(slug: string) {
const url = `${this.apiUrl}/wp-json/wp/v2/tags?slug=${slug}`;
const response = await fetch(url);
const data = await response.json();
if (data.length === 0) {
return;
}
const item = data[0] as WP_REST_API_Tag;
return {
id: item.id.toString(),
name: item.name,
slug: item.slug,
};
}
async getCategories(options?: Cms.GetCategoriesOptions) {
const queryParams = new URLSearchParams();
if (options?.limit) {
queryParams.append('per_page', options.limit.toString());
}
if (options?.offset) {
queryParams.append('offset', options.offset.toString());
}
const response = await fetch(
`${this.apiUrl}/wp-json/wp/v2/categories?${queryParams.toString()}`,
);
const data = (await response.json()) as WP_REST_API_Category[];
return data.map((item) => ({
id: item.id.toString(),
name: item.name,
slug: item.slug,
}));
}
async getTags(options: GetTagsOptions) {
const queryParams = new URLSearchParams();
if (options?.limit) {
queryParams.append('per_page', options.limit.toString());
}
if (options?.offset) {
queryParams.append('offset', options.offset.toString());
}
const response = await fetch(
`${this.apiUrl}/wp-json/wp/v2/tags?${queryParams.toString()}`,
);
const data = (await response.json()) as WP_REST_API_Tag[];
return data.map((item) => ({
id: item.id.toString(),
name: item.name,
slug: item.slug,
}));
}
private async getTagsByIds(ids: number[]) {
const promises = ids.map((id) =>
fetch(`${this.apiUrl}/wp-json/wp/v2/tags/${id}`),
);
const responses = await Promise.all(promises);
const data = (await Promise.all(
responses.map((response) => response.json()),
)) as WP_REST_API_Tag[];
return data.map((item) => ({ id: item.id, name: item.name }));
}
private async getCategoriesByIds(ids: number[]) {
const promises = ids.map((id) =>
fetch(`${this.apiUrl}/wp-json/wp/v2/categories/${id}`),
);
const responses = await Promise.all(promises);
const data = (await Promise.all(
responses.map((response) => response.json()),
)) as WP_REST_API_Category[];
return data.map((item) => ({ id: item.id, name: item.name }));
}
private async getAuthor(id: number) {
const response = await fetch(`${this.apiUrl}/wp-json/wp/v2/users/${id}`);
if (!response.ok) {
return undefined;
}
const data = await response.json();
return { name: data.name };
}
}

View File

@@ -0,0 +1,8 @@
{
"extends": "@kit/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["src"],
"exclude": ["node_modules"]
}