Update content categorization and handle hierarchical documentation
Enhancements were implemented to support hierarchical documentation. Local CMS now respects parent ID and order attributes of content items, and content can be categories as 'blog' or 'documentation'. Changes were also made to the wordpress integration supporting these new categorizations. Introduced working with nested documentation pages.
This commit is contained in:
@@ -10,8 +10,6 @@ export const PostHeader: React.FC<{
|
|||||||
}> = ({ post }) => {
|
}> = ({ post }) => {
|
||||||
const { title, publishedAt, description, image } = post;
|
const { title, publishedAt, description, image } = post;
|
||||||
|
|
||||||
console.log(post);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-col space-y-8'}>
|
<div className={'flex flex-col space-y-8'}>
|
||||||
<div className={'flex flex-col space-y-2'}>
|
<div className={'flex flex-col space-y-2'}>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ async function BlogPage() {
|
|||||||
const cms = await createCmsClient();
|
const cms = await createCmsClient();
|
||||||
|
|
||||||
const posts = await cms.getContentItems({
|
const posts = await cms.getContentItems({
|
||||||
type: 'post',
|
categories: ['blog'],
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,17 +14,17 @@ import styles from '../../blog/_components/html-renderer.module.css';
|
|||||||
const getPageBySlug = cache(async (slug: string) => {
|
const getPageBySlug = cache(async (slug: string) => {
|
||||||
const client = await createCmsClient();
|
const client = await createCmsClient();
|
||||||
|
|
||||||
return client.getContentItemById(slug, 'pages');
|
return client.getContentItemById(slug);
|
||||||
});
|
});
|
||||||
|
|
||||||
interface PageParams {
|
interface PageParams {
|
||||||
params: {
|
params: {
|
||||||
slug: string;
|
slug: string[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateMetadata = async ({ params }: PageParams) => {
|
export const generateMetadata = async ({ params }: PageParams) => {
|
||||||
const page = await getPageBySlug(params.slug);
|
const page = await getPageBySlug(params.slug.join('/'));
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -39,7 +39,7 @@ export const generateMetadata = async ({ params }: PageParams) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function DocumentationPage({ params }: PageParams) {
|
async function DocumentationPage({ params }: PageParams) {
|
||||||
const page = await getPageBySlug(params.slug);
|
const page = await getPageBySlug(params.slug.join('/'));
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -61,7 +61,7 @@ async function DocumentationPage({ params }: PageParams) {
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
<If condition={page.children}>
|
<If condition={page.children}>
|
||||||
<DocsCards pages={page.children ?? []} />
|
<DocsCards cards={page.children ?? []} />
|
||||||
</If>
|
</If>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2,10 +2,10 @@ import { Cms } from '@kit/cms';
|
|||||||
|
|
||||||
import { DocsCard } from './docs-card';
|
import { DocsCard } from './docs-card';
|
||||||
|
|
||||||
export function DocsCards({ pages }: { pages: Cms.ContentItem[] }) {
|
export function DocsCards({ cards }: { cards: Cms.ContentItem[] }) {
|
||||||
return (
|
return (
|
||||||
<div className={'grid grid-cols-1 gap-8 lg:grid-cols-2'}>
|
<div className={'grid grid-cols-1 gap-8 lg:grid-cols-2'}>
|
||||||
{pages.map((item) => {
|
{cards.map((item) => {
|
||||||
return (
|
return (
|
||||||
<DocsCard
|
<DocsCard
|
||||||
key={item.title}
|
key={item.title}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createCmsClient } from '@kit/cms';
|
import { Cms, createCmsClient } from '@kit/cms';
|
||||||
|
|
||||||
import { DocsNavigation } from '~/(marketing)/docs/_components/docs-navigation';
|
import { DocsNavigation } from '~/(marketing)/docs/_components/docs-navigation';
|
||||||
|
|
||||||
@@ -6,17 +6,13 @@ async function DocsLayout({ children }: React.PropsWithChildren) {
|
|||||||
const cms = await createCmsClient();
|
const cms = await createCmsClient();
|
||||||
|
|
||||||
const pages = await cms.getContentItems({
|
const pages = await cms.getContentItems({
|
||||||
type: 'page',
|
|
||||||
categories: ['documentation'],
|
categories: ['documentation'],
|
||||||
depth: 1,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(pages);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'container mx-auto'}>
|
<div className={'container mx-auto'}>
|
||||||
<div className={'flex'}>
|
<div className={'flex'}>
|
||||||
<DocsNavigation pages={pages} />
|
<DocsNavigation pages={buildDocumentationTree(pages)} />
|
||||||
|
|
||||||
<div className={'flex w-full flex-col items-center'}>{children}</div>
|
<div className={'flex w-full flex-col items-center'}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,3 +21,37 @@ async function DocsLayout({ children }: React.PropsWithChildren) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default DocsLayout;
|
export default DocsLayout;
|
||||||
|
|
||||||
|
// we want to place all the children under their parent
|
||||||
|
// based on the property parentId
|
||||||
|
function buildDocumentationTree(pages: Cms.ContentItem[]) {
|
||||||
|
const tree: Cms.ContentItem[] = [];
|
||||||
|
const map: Record<string, Cms.ContentItem> = {};
|
||||||
|
|
||||||
|
pages.forEach((page) => {
|
||||||
|
map[page.id] = page;
|
||||||
|
});
|
||||||
|
|
||||||
|
pages.forEach((page) => {
|
||||||
|
if (page.parentId) {
|
||||||
|
const parent = map[page.parentId];
|
||||||
|
|
||||||
|
if (!parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parent.children) {
|
||||||
|
parent.children = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.children.push(page);
|
||||||
|
|
||||||
|
// sort children by order
|
||||||
|
parent.children.sort((a, b) => a.order - b.order);
|
||||||
|
} else {
|
||||||
|
tree.push(page);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return tree.sort((a, b) => a.order - b.order);
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,11 +19,12 @@ async function DocsPage() {
|
|||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
const docs = await client.getContentItems({
|
const docs = await client.getContentItems({
|
||||||
type: 'page',
|
|
||||||
categories: ['documentation'],
|
categories: ['documentation'],
|
||||||
depth: 1,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Filter out any docs that have a parentId, as these are children of other docs
|
||||||
|
const cards = docs.filter((item) => !item.parentId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'my-8 flex flex-col space-y-16'}>
|
<div className={'my-8 flex flex-col space-y-16'}>
|
||||||
<SitePageHeader
|
<SitePageHeader
|
||||||
@@ -32,7 +33,7 @@ async function DocsPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<DocsCards pages={docs} />
|
<DocsCards cards={cards} />
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
title: Installing Makerkit
|
title: Installing Makerkit
|
||||||
label: Installing Makerkit
|
label: Installing Makerkit
|
||||||
description: Learn how to install Makerkit on your local machine
|
description: Learn how to install Makerkit on your local machine
|
||||||
|
categories:
|
||||||
|
- documentation
|
||||||
---
|
---
|
||||||
|
|
||||||
If you have bought a license for MakerKit, you have access to all the
|
If you have bought a license for MakerKit, you have access to all the
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
title: Clone the MakerKit SaaS boilerplate repository
|
title: Clone the MakerKit SaaS boilerplate repository
|
||||||
label: Clone the repository
|
label: Clone the repository
|
||||||
description: Learn how to clone the MakerKit repository and install the NodeJS dependencies.
|
description: Learn how to clone the MakerKit repository and install the NodeJS dependencies.
|
||||||
|
categories:
|
||||||
|
- documentation
|
||||||
---
|
---
|
||||||
|
|
||||||
If you have bought a license for MakerKit, you have access to all the
|
If you have bought a license for MakerKit, you have access to all the
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
title: Getting Started
|
title: Getting Started
|
||||||
label: Getting Started
|
label: Getting Started
|
||||||
description: Getting started with the Makerkit Kit
|
description: Getting started with the Makerkit Kit
|
||||||
|
categories:
|
||||||
|
- documentation
|
||||||
---
|
---
|
||||||
|
|
||||||
Makerkit is a Next.js/Remix SaaS Starter that helps you build your own SaaS in minutes. It comes with a fully integrated Stripe billing system, a landing page, and a dashboard.
|
Makerkit is a Next.js/Remix SaaS Starter that helps you build your own SaaS in minutes. It comes with a fully integrated Stripe billing system, a landing page, and a dashboard.
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
title: Authentication Overview
|
title: Authentication Overview
|
||||||
label: Overview
|
label: Overview
|
||||||
description: Learn how authentication works in MakerKit and how to configure it.
|
description: Learn how authentication works in MakerKit and how to configure it.
|
||||||
|
categories:
|
||||||
|
- documentation
|
||||||
---
|
---
|
||||||
|
|
||||||
The way you want your users to authenticate can be driven via configuration.
|
The way you want your users to authenticate can be driven via configuration.
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
title: Supabase Setup
|
title: Supabase Setup
|
||||||
label: Supabase Setup
|
label: Supabase Setup
|
||||||
description: How to setup authentication in MakerKit using Supabase.
|
description: How to setup authentication in MakerKit using Supabase.
|
||||||
|
categories:
|
||||||
|
- documentation
|
||||||
---
|
---
|
||||||
|
|
||||||
Supabase needs a few settings to be configured in their Dashboard to work correctly. This guide will walk you through the steps to get your Supabase authentication setup.
|
Supabase needs a few settings to be configured in their Dashboard to work correctly. This guide will walk you through the steps to get your Supabase authentication setup.
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
title: Authentication
|
title: Authentication
|
||||||
label: Authentication
|
label: Authentication
|
||||||
description: Learn everything about Authentication in Makerkit
|
description: Learn everything about Authentication in Makerkit
|
||||||
|
categories:
|
||||||
|
- documentation
|
||||||
---
|
---
|
||||||
|
|
||||||
MakerKit uses Supabase to manage authentication within your application.
|
MakerKit uses Supabase to manage authentication within your application.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiu
|
|||||||
image: /assets/images/posts/lorem-ipsum.webp
|
image: /assets/images/posts/lorem-ipsum.webp
|
||||||
author: John Doe
|
author: John Doe
|
||||||
categories:
|
categories:
|
||||||
- posts
|
- blog
|
||||||
---
|
---
|
||||||
|
|
||||||
## Fecerat avis invenio mentis
|
## Fecerat avis invenio mentis
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ export const Post = defineDocumentType(() => ({
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
resolve: (post) => `/blog/${getSlug(post._raw.sourceFileName)}`,
|
resolve: (post) => `/blog/${getSlug(post._raw.sourceFileName)}`,
|
||||||
},
|
},
|
||||||
|
parentId: {
|
||||||
|
type: 'string',
|
||||||
|
resolve: (doc) => {
|
||||||
|
const segments = getPathSegments(doc);
|
||||||
|
return segments.length > 1 ? segments.slice(0, -1).join('/') : 'blog';
|
||||||
|
},
|
||||||
|
},
|
||||||
structuredData: {
|
structuredData: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
resolve: (doc) => ({
|
resolve: (doc) => ({
|
||||||
@@ -124,6 +131,33 @@ export const DocumentationPage = defineDocumentType(() => ({
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
resolve: (post) => calculateReadingTime(post.body.raw),
|
resolve: (post) => calculateReadingTime(post.body.raw),
|
||||||
},
|
},
|
||||||
|
parentId: {
|
||||||
|
type: 'string',
|
||||||
|
resolve: (doc) => {
|
||||||
|
const segments = getPathSegments(doc);
|
||||||
|
|
||||||
|
if (segments.length > 1) {
|
||||||
|
const { pathName } = getMetaFromFolderName(segments[0]);
|
||||||
|
|
||||||
|
if (pathName === 'index') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: 'number',
|
||||||
|
resolve: (doc) => {
|
||||||
|
const segments = getPathSegments(doc);
|
||||||
|
const { order } = getMetaFromFolderName(segments[segments.length - 1]);
|
||||||
|
|
||||||
|
return order;
|
||||||
|
},
|
||||||
|
},
|
||||||
structuredData: {
|
structuredData: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
resolve: (doc) => ({
|
resolve: (doc) => ({
|
||||||
|
|||||||
@@ -7,14 +7,7 @@ async function getAllContentItems() {
|
|||||||
'../.contentlayer/generated'
|
'../.contentlayer/generated'
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [...allPosts, ...allDocumentationPages];
|
||||||
...allPosts.map((item) => {
|
|
||||||
return { ...item, type: 'post' };
|
|
||||||
}),
|
|
||||||
...allDocumentationPages.map((item) => {
|
|
||||||
return { ...item, type: 'page', categories: ['documentation'] };
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,29 +34,14 @@ export class ContentlayerClient implements CmsClient {
|
|||||||
)
|
)
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
const typeMatch = options?.type ? item.type === options.type : true;
|
const matchesParentIds = options?.parentIds
|
||||||
const path = item._raw.flattenedPath;
|
? options.parentIds.includes(item.parentId ?? '')
|
||||||
const splitPath = path.split('/');
|
: true;
|
||||||
|
|
||||||
const depthMatch =
|
return tagMatch && categoryMatch && matchesParentIds;
|
||||||
options?.depth !== undefined
|
|
||||||
? splitPath.length - 1 === options.depth
|
|
||||||
: true;
|
|
||||||
|
|
||||||
return tagMatch && categoryMatch && typeMatch && depthMatch;
|
|
||||||
})
|
})
|
||||||
.slice(startOffset, endOffset)
|
.map((post) => this.mapPost(post))
|
||||||
.map((post) => {
|
.slice(startOffset, endOffset);
|
||||||
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);
|
return Promise.resolve(promise);
|
||||||
}
|
}
|
||||||
@@ -76,15 +54,7 @@ export class ContentlayerClient implements CmsClient {
|
|||||||
return Promise.resolve(undefined);
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const children: Cms.ContentItem[] = [];
|
return Promise.resolve(post ? this.mapPost(post) : undefined);
|
||||||
|
|
||||||
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) {
|
async getCategoryBySlug(slug: string) {
|
||||||
@@ -108,13 +78,6 @@ export class ContentlayerClient implements CmsClient {
|
|||||||
const allContentItems = await getAllContentItems();
|
const allContentItems = await getAllContentItems();
|
||||||
|
|
||||||
const categories = allContentItems
|
const categories = allContentItems
|
||||||
.filter((item) => {
|
|
||||||
if (options?.type) {
|
|
||||||
return item.type === options.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.slice(startOffset, endOffset)
|
.slice(startOffset, endOffset)
|
||||||
.flatMap((post) => post.categories)
|
.flatMap((post) => post.categories)
|
||||||
.filter((category): category is string => !!category)
|
.filter((category): category is string => !!category)
|
||||||
@@ -132,13 +95,6 @@ export class ContentlayerClient implements CmsClient {
|
|||||||
const allContentItems = await getAllContentItems();
|
const allContentItems = await getAllContentItems();
|
||||||
|
|
||||||
const tags = allContentItems
|
const tags = allContentItems
|
||||||
.filter((item) => {
|
|
||||||
if (options?.type) {
|
|
||||||
return item.type === options.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.slice(startOffset, endOffset)
|
.slice(startOffset, endOffset)
|
||||||
.flatMap((post) => post.tags)
|
.flatMap((post) => post.tags)
|
||||||
.filter((tag): tag is string => !!tag)
|
.filter((tag): tag is string => !!tag)
|
||||||
@@ -167,9 +123,10 @@ export class ContentlayerClient implements CmsClient {
|
|||||||
title: post.title,
|
title: post.title,
|
||||||
description: post.description ?? '',
|
description: post.description ?? '',
|
||||||
content: post.body?.code,
|
content: post.body?.code,
|
||||||
|
order: 'order' in post ? post.order : 0,
|
||||||
image: 'image' in post ? post.image : undefined,
|
image: 'image' in post ? post.image : undefined,
|
||||||
publishedAt: 'date' in post ? new Date(post.date) : new Date(),
|
publishedAt: 'date' in post ? new Date(post.date) : new Date(),
|
||||||
parentId: 'parentId' in post ? post.parentId : undefined,
|
parentId: 'parentId' in post ? (post.parentId as string) : undefined,
|
||||||
url: post.url,
|
url: post.url,
|
||||||
slug: post.slug,
|
slug: post.slug,
|
||||||
author: 'author' in post ? post.author : '',
|
author: 'author' in post ? post.author : '',
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
export namespace Cms {
|
export namespace Cms {
|
||||||
export type ContentType = 'post' | 'page';
|
|
||||||
|
|
||||||
export interface ContentItem {
|
export interface ContentItem {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
type: ContentType;
|
|
||||||
url: string;
|
url: string;
|
||||||
description: string | undefined;
|
description: string | undefined;
|
||||||
content: string;
|
content: string;
|
||||||
@@ -15,8 +12,9 @@ export namespace Cms {
|
|||||||
slug: string;
|
slug: string;
|
||||||
categories: Category[];
|
categories: Category[];
|
||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
parentId?: string;
|
order: number;
|
||||||
children?: ContentItem[];
|
children: ContentItem[];
|
||||||
|
parentId: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
@@ -32,44 +30,73 @@ export namespace Cms {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GetContentItemsOptions {
|
export interface GetContentItemsOptions {
|
||||||
type?: ContentType;
|
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
depth?: number;
|
parentIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetCategoriesOptions {
|
export interface GetCategoriesOptions {
|
||||||
type?: ContentType;
|
slugs?: string[];
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetTagsOptions {
|
export interface GetTagsOptions {
|
||||||
type?: ContentType;
|
slugs?: string[];
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class representing a CMS client.
|
||||||
|
*/
|
||||||
export abstract class CmsClient {
|
export abstract class CmsClient {
|
||||||
|
/**
|
||||||
|
* Retrieves content items based on the provided options.
|
||||||
|
* @param options - Options for filtering and pagination.
|
||||||
|
* @returns A promise that resolves to an array of content items.
|
||||||
|
*/
|
||||||
abstract getContentItems(
|
abstract getContentItems(
|
||||||
options?: Cms.GetContentItemsOptions,
|
options?: Cms.GetContentItemsOptions,
|
||||||
): Promise<Cms.ContentItem[]>;
|
): Promise<Cms.ContentItem[]>;
|
||||||
|
|
||||||
abstract getContentItemById(
|
/**
|
||||||
id: string,
|
* Retrieves a content item by its ID and type.
|
||||||
type?: string,
|
* @param id - The ID of the content item.
|
||||||
): Promise<Cms.ContentItem | undefined>;
|
* @returns A promise that resolves to the content item, or undefined if not found.
|
||||||
|
*/
|
||||||
|
abstract getContentItemById(id: string): Promise<Cms.ContentItem | undefined>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves categories based on the provided options.
|
||||||
|
* @param options - Options for filtering and pagination.
|
||||||
|
* @returns A promise that resolves to an array of categories.
|
||||||
|
*/
|
||||||
abstract getCategories(
|
abstract getCategories(
|
||||||
options?: Cms.GetCategoriesOptions,
|
options?: Cms.GetCategoriesOptions,
|
||||||
): Promise<Cms.Category[]>;
|
): Promise<Cms.Category[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a category by its slug.
|
||||||
|
* @param slug - The slug of the category.
|
||||||
|
* @returns A promise that resolves to the category, or undefined if not found.
|
||||||
|
*/
|
||||||
abstract getCategoryBySlug(slug: string): Promise<Cms.Category | undefined>;
|
abstract getCategoryBySlug(slug: string): Promise<Cms.Category | undefined>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves tags based on the provided options.
|
||||||
|
* @param options - Options for filtering and pagination.
|
||||||
|
* @returns A promise that resolves to an array of tags.
|
||||||
|
*/
|
||||||
abstract getTags(options?: Cms.GetTagsOptions): Promise<Cms.Tag[]>;
|
abstract getTags(options?: Cms.GetTagsOptions): Promise<Cms.Tag[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a tag by its slug.
|
||||||
|
* @param slug - The slug of the tag.
|
||||||
|
* @returns A promise that resolves to the tag, or undefined if not found.
|
||||||
|
*/
|
||||||
abstract getTagBySlug(slug: string): Promise<Cms.Tag | undefined>;
|
abstract getTagBySlug(slug: string): Promise<Cms.Tag | undefined>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,4 +41,23 @@ You will be asked to set up the Wordpress instance when you visit `http://localh
|
|||||||
|
|
||||||
## Note for Wordpress REST API
|
## Note for Wordpress REST API
|
||||||
|
|
||||||
To make the REST API in your Wordpress instance work, please change the permalink structure to `/%post%/` from the Wordpress admin panel.
|
To make the REST API in your Wordpress instance work, please change the permalink structure to `/%post%/` from the Wordpress admin panel.
|
||||||
|
|
||||||
|
## Blog
|
||||||
|
|
||||||
|
To include Blog Posts from Wordpress - please create a **post** with category named `blog` and add posts to it.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
To include Documentation from Wordpress - please create a **page** with category named `documentation` and add posts to it.
|
||||||
|
|
||||||
|
This involves enabling categories for pages. To do this, add the following code to your theme's `functions.php` file:
|
||||||
|
|
||||||
|
```php
|
||||||
|
function add_categories_to_pages() {
|
||||||
|
register_taxonomy_for_object_type('category', 'page');
|
||||||
|
}
|
||||||
|
add_action('init', 'add_categories_to_pages');
|
||||||
|
```
|
||||||
|
|
||||||
|
Please refer to `wp-content/themes/twentytwentyfour/functions.php` for an example of a theme that includes this code.
|
||||||
@@ -21,10 +21,28 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 8080:80
|
- 8080:80
|
||||||
restart: always
|
restart: always
|
||||||
|
working_dir: /var/www/html
|
||||||
|
volumes:
|
||||||
|
- ./wp-content:/var/www/html/wp-content
|
||||||
environment:
|
environment:
|
||||||
- WORDPRESS_DB_HOST=db
|
- WORDPRESS_DB_HOST=db
|
||||||
- WORDPRESS_DB_USER=wordpress
|
- WORDPRESS_DB_USER=wordpress
|
||||||
- WORDPRESS_DB_PASSWORD=wordpress
|
- WORDPRESS_DB_PASSWORD=wordpress
|
||||||
- WORDPRESS_DB_NAME=wordpress
|
- WORDPRESS_DB_NAME=wordpress
|
||||||
|
- WORDPRESS_DEBUG=1
|
||||||
|
- WORDPRESS_CONFIG_EXTRA = |
|
||||||
|
define('FS_METHOD', 'direct');
|
||||||
|
/** disable wp core auto update */
|
||||||
|
define('WP_AUTO_UPDATE_CORE', false);
|
||||||
|
|
||||||
|
/** local environment settings */
|
||||||
|
define('WP_CACHE', false);
|
||||||
|
define('ENVIRONMENT', 'local');
|
||||||
|
|
||||||
|
/** force site home url */
|
||||||
|
if(!defined('WP_HOME')) {
|
||||||
|
define('WP_HOME', 'http://localhost');
|
||||||
|
define('WP_SITEURL', WP_HOME);
|
||||||
|
}
|
||||||
volumes:
|
volumes:
|
||||||
db_data:
|
db_data:
|
||||||
@@ -20,93 +20,83 @@ export class WordpressClient implements CmsClient {
|
|||||||
this.apiUrl = apiUrl;
|
this.apiUrl = apiUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves content items from a CMS based on the provided options.
|
||||||
|
*
|
||||||
|
* @param {Cms.GetContentItemsOptions} options - The options to customize the retrieval of content items.
|
||||||
|
*/
|
||||||
async getContentItems(options?: Cms.GetContentItemsOptions) {
|
async getContentItems(options?: Cms.GetContentItemsOptions) {
|
||||||
let endpoint: string;
|
const queryParams = new URLSearchParams({
|
||||||
|
_embed: 'true',
|
||||||
switch (options?.type) {
|
});
|
||||||
case 'post':
|
|
||||||
endpoint = '/wp-json/wp/v2/posts?_embed';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'page':
|
|
||||||
endpoint = '/wp-json/wp/v2/pages?_embed';
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
endpoint = '/wp-json/wp/v2/posts?_embed';
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(this.apiUrl + endpoint);
|
|
||||||
|
|
||||||
if (options?.limit) {
|
if (options?.limit) {
|
||||||
url.searchParams.append('per_page', options.limit.toString());
|
queryParams.append('per_page', options.limit.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.offset) {
|
if (options?.offset) {
|
||||||
url.searchParams.append('offset', options.offset.toString());
|
queryParams.append('offset', options.offset.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.categories) {
|
if (options?.categories) {
|
||||||
url.searchParams.append('categories', options.categories.join(','));
|
const ids = await this.getCategories({
|
||||||
|
slugs: options.categories,
|
||||||
|
}).then((categories) => categories.map((category) => category.id));
|
||||||
|
|
||||||
|
if (ids.length) {
|
||||||
|
queryParams.append('categories', ids.join(','));
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
'No categories found for the provided slugs',
|
||||||
|
options.categories,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.tags) {
|
if (options?.tags) {
|
||||||
url.searchParams.append('tags', options.tags.join(','));
|
const ids = await this.getCategories({
|
||||||
|
slugs: options.tags,
|
||||||
|
}).then((tags) => tags.map((tag) => tag.id));
|
||||||
|
|
||||||
|
if (ids.length) {
|
||||||
|
queryParams.append('tags', ids.join(','));
|
||||||
|
} else {
|
||||||
|
console.warn('No tags found for the provided slugs', options.tags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(url.toString());
|
if (options?.parentIds && options.parentIds.length > 0) {
|
||||||
const data = (await response.json()) as WP_REST_API_Post[];
|
queryParams.append('parent', options.parentIds.join(','));
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.all(
|
const endpoints = [
|
||||||
data.map(async (item) => {
|
`/wp-json/wp/v2/pages?${queryParams.toString()}`,
|
||||||
// Fetch author, categories, and tags as before...
|
`/wp-json/wp/v2/posts?${queryParams.toString()}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const urls = endpoints.map((endpoint) => `${this.apiUrl}${endpoint}`);
|
||||||
|
|
||||||
|
const responses = await Promise.all(
|
||||||
|
urls.map((url) => fetch(url).then((value) => value.json())),
|
||||||
|
).then((values) => values.flat().filter(Boolean));
|
||||||
|
|
||||||
|
return await Promise.all(
|
||||||
|
responses.map(async (item: WP_REST_API_Post) => {
|
||||||
let parentId: string | undefined;
|
let parentId: string | undefined;
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
throw new Error('Failed to fetch content items');
|
||||||
|
}
|
||||||
|
|
||||||
if (item.parent) {
|
if (item.parent) {
|
||||||
parentId = item.parent.toString();
|
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: this.getFeaturedMedia(child),
|
|
||||||
description: child.excerpt.rendered,
|
|
||||||
url: child.link,
|
|
||||||
content: child.content.rendered,
|
|
||||||
slug: child.slug,
|
|
||||||
publishedAt: new Date(child.date),
|
|
||||||
author: childAuthor?.name,
|
|
||||||
categories: childCategories,
|
|
||||||
tags: childTags,
|
|
||||||
parentId: child.parent?.toString(),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const author = await this.getAuthor(item.author);
|
const author = await this.getAuthor(item.author);
|
||||||
const categories = await this.getCategoriesByIds(item.categories ?? []);
|
const categories = await this.getCategoriesByIds(item.categories ?? []);
|
||||||
const tags = await this.getTagsByIds(item.tags ?? []);
|
const tags = await this.getTagsByIds(item.tags ?? []);
|
||||||
const image = item.featured_media ? this.getFeaturedMedia(item) : '';
|
const image = item.featured_media ? this.getFeaturedMedia(item) : '';
|
||||||
|
const order = item.menu_order ?? 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: item.id.toString(),
|
id: item.id.toString(),
|
||||||
@@ -120,19 +110,34 @@ export class WordpressClient implements CmsClient {
|
|||||||
author: author?.name,
|
author: author?.name,
|
||||||
categories: categories,
|
categories: categories,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
type: item.type as Cms.ContentType,
|
|
||||||
parentId,
|
parentId,
|
||||||
children,
|
order,
|
||||||
|
children: [],
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getContentItemById(slug: string, type: 'posts' | 'pages' = 'posts') {
|
async getContentItemById(slug: string) {
|
||||||
const url = `${this.apiUrl}/wp-json/wp/v2/${type}?slug=${slug}&_embed`;
|
const searchParams = new URLSearchParams({
|
||||||
const response = await fetch(url);
|
_embed: 'true',
|
||||||
const data = (await response.json()) as WP_REST_API_Post[];
|
slug,
|
||||||
const item = data[0];
|
});
|
||||||
|
|
||||||
|
const endpoints = [
|
||||||
|
`/wp-json/wp/v2/pages?${searchParams.toString()}`,
|
||||||
|
`/wp-json/wp/v2/posts?${searchParams.toString()}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const promises = endpoints.map((endpoint) =>
|
||||||
|
fetch(this.apiUrl + endpoint).then((res) => res.json()),
|
||||||
|
);
|
||||||
|
|
||||||
|
const responses = await Promise.all(promises).then((values) =>
|
||||||
|
values.filter(Boolean),
|
||||||
|
);
|
||||||
|
|
||||||
|
const item = responses[0][0] as WP_REST_API_Post;
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return;
|
return;
|
||||||
@@ -146,9 +151,9 @@ export class WordpressClient implements CmsClient {
|
|||||||
return {
|
return {
|
||||||
id: item.id.toString(),
|
id: item.id.toString(),
|
||||||
image,
|
image,
|
||||||
|
order: item.menu_order ?? 0,
|
||||||
url: item.link,
|
url: item.link,
|
||||||
description: item.excerpt.rendered,
|
description: item.excerpt.rendered,
|
||||||
type: item.type as Cms.ContentType,
|
|
||||||
children: [],
|
children: [],
|
||||||
title: item.title.rendered,
|
title: item.title.rendered,
|
||||||
content: item.content.rendered,
|
content: item.content.rendered,
|
||||||
@@ -157,6 +162,7 @@ export class WordpressClient implements CmsClient {
|
|||||||
author: author?.name,
|
author: author?.name,
|
||||||
categories,
|
categories,
|
||||||
tags,
|
tags,
|
||||||
|
parentId: item.parent?.toString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,10 +213,22 @@ export class WordpressClient implements CmsClient {
|
|||||||
queryParams.append('offset', options.offset.toString());
|
queryParams.append('offset', options.offset.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.slugs) {
|
||||||
|
const slugs = options.slugs.join(',');
|
||||||
|
|
||||||
|
queryParams.append('slug', slugs);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${this.apiUrl}/wp-json/wp/v2/categories?${queryParams.toString()}`,
|
`${this.apiUrl}/wp-json/wp/v2/categories?${queryParams.toString()}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Failed to fetch categories', await response.json());
|
||||||
|
|
||||||
|
throw new Error('Failed to fetch categories');
|
||||||
|
}
|
||||||
|
|
||||||
const data = (await response.json()) as WP_REST_API_Category[];
|
const data = (await response.json()) as WP_REST_API_Category[];
|
||||||
|
|
||||||
return data.map((item) => ({
|
return data.map((item) => ({
|
||||||
@@ -231,10 +249,21 @@ export class WordpressClient implements CmsClient {
|
|||||||
queryParams.append('offset', options.offset.toString());
|
queryParams.append('offset', options.offset.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.slugs) {
|
||||||
|
const slugs = options.slugs.join(',');
|
||||||
|
queryParams.append('slug', slugs);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${this.apiUrl}/wp-json/wp/v2/tags?${queryParams.toString()}`,
|
`${this.apiUrl}/wp-json/wp/v2/tags?${queryParams.toString()}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Failed to fetch tags', await response.json());
|
||||||
|
|
||||||
|
throw new Error('Failed to fetch tags');
|
||||||
|
}
|
||||||
|
|
||||||
const data = (await response.json()) as WP_REST_API_Tag[];
|
const data = (await response.json()) as WP_REST_API_Tag[];
|
||||||
|
|
||||||
return data.map((item) => ({
|
return data.map((item) => ({
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
</footer>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
function register_category_and_tag_with_pages(){
|
||||||
|
/*add categories and tags to pages*/
|
||||||
|
register_taxonomy_for_object_type('category', 'page');
|
||||||
|
register_taxonomy_for_object_type('post_tag', 'page');
|
||||||
|
}
|
||||||
|
add_action( 'init', 'register_category_and_tag_with_pages');
|
||||||
|
|
||||||
|
function register_pre_get_category_and_tag_with_pages( $query ) {
|
||||||
|
|
||||||
|
if ( is_admin() || ! $query->is_main_query() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/*view categories and tags archive pages */
|
||||||
|
if($query->is_category && $query->is_main_query()){
|
||||||
|
$query->set('post_type', array( 'post', 'page'));
|
||||||
|
}
|
||||||
|
if($query->is_tag && $query->is_main_query()){
|
||||||
|
$query->set('post_type', array( 'post', 'page'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_action( 'pre_get_posts', 'register_pre_get_category_and_tag_with_pages');
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html <?php language_attributes(); ?>>
|
||||||
|
<head>
|
||||||
|
<meta charset="<?php bloginfo( 'charset' ); ?>">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<?php wp_head(); ?>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body <?php body_class(); ?>>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<div id="primary" class="content-area">
|
||||||
|
<main id="main" class="site-main" role="main">
|
||||||
|
|
||||||
|
<?php
|
||||||
|
if (have_posts()) :
|
||||||
|
/* Start the Loop */
|
||||||
|
while (have_posts()) : the_post();
|
||||||
|
?>
|
||||||
|
<div>
|
||||||
|
<a href="<?php the_permalink(); ?>"><h3><?php the_title(); ?></h3></a>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
endwhile;
|
||||||
|
/* End the Loop */
|
||||||
|
else :
|
||||||
|
// Nothing
|
||||||
|
endif;
|
||||||
|
?>
|
||||||
|
|
||||||
|
</main><!-- #main -->
|
||||||
|
</div><!-- #primary -->
|
||||||
|
</div><!-- .wrap -->
|
||||||
|
|
||||||
|
<?php get_footer(); ?>
|
||||||
|
<?php wp_footer(); ?>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user