diff --git a/apps/web/app/(marketing)/blog/_components/post-header.tsx b/apps/web/app/(marketing)/blog/_components/post-header.tsx
index 4d490386f..2bcbd02a1 100644
--- a/apps/web/app/(marketing)/blog/_components/post-header.tsx
+++ b/apps/web/app/(marketing)/blog/_components/post-header.tsx
@@ -10,8 +10,6 @@ export const PostHeader: React.FC<{
}> = ({ post }) => {
const { title, publishedAt, description, image } = post;
- console.log(post);
-
return (
diff --git a/apps/web/app/(marketing)/blog/page.tsx b/apps/web/app/(marketing)/blog/page.tsx
index e57b5025c..69ae75ac7 100644
--- a/apps/web/app/(marketing)/blog/page.tsx
+++ b/apps/web/app/(marketing)/blog/page.tsx
@@ -20,7 +20,7 @@ async function BlogPage() {
const cms = await createCmsClient();
const posts = await cms.getContentItems({
- type: 'post',
+ categories: ['blog'],
});
return (
diff --git a/apps/web/app/(marketing)/docs/[slug]/page.tsx b/apps/web/app/(marketing)/docs/[...slug]/page.tsx
similarity index 86%
rename from apps/web/app/(marketing)/docs/[slug]/page.tsx
rename to apps/web/app/(marketing)/docs/[...slug]/page.tsx
index 5d263e490..b4581df1e 100644
--- a/apps/web/app/(marketing)/docs/[slug]/page.tsx
+++ b/apps/web/app/(marketing)/docs/[...slug]/page.tsx
@@ -14,17 +14,17 @@ import styles from '../../blog/_components/html-renderer.module.css';
const getPageBySlug = cache(async (slug: string) => {
const client = await createCmsClient();
- return client.getContentItemById(slug, 'pages');
+ return client.getContentItemById(slug);
});
interface PageParams {
params: {
- slug: string;
+ slug: string[];
};
}
export const generateMetadata = async ({ params }: PageParams) => {
- const page = await getPageBySlug(params.slug);
+ const page = await getPageBySlug(params.slug.join('/'));
if (!page) {
notFound();
@@ -39,7 +39,7 @@ export const generateMetadata = async ({ params }: PageParams) => {
};
async function DocumentationPage({ params }: PageParams) {
- const page = await getPageBySlug(params.slug);
+ const page = await getPageBySlug(params.slug.join('/'));
if (!page) {
notFound();
@@ -61,7 +61,7 @@ async function DocumentationPage({ params }: PageParams) {
-
+
diff --git a/apps/web/app/(marketing)/docs/_components/docs-cards.tsx b/apps/web/app/(marketing)/docs/_components/docs-cards.tsx
index 490f45340..568fef941 100644
--- a/apps/web/app/(marketing)/docs/_components/docs-cards.tsx
+++ b/apps/web/app/(marketing)/docs/_components/docs-cards.tsx
@@ -2,10 +2,10 @@ import { Cms } from '@kit/cms';
import { DocsCard } from './docs-card';
-export function DocsCards({ pages }: { pages: Cms.ContentItem[] }) {
+export function DocsCards({ cards }: { cards: Cms.ContentItem[] }) {
return (
- {pages.map((item) => {
+ {cards.map((item) => {
return (
@@ -25,3 +21,37 @@ async function DocsLayout({ children }: React.PropsWithChildren) {
}
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 = {};
+
+ 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);
+}
diff --git a/apps/web/app/(marketing)/docs/page.tsx b/apps/web/app/(marketing)/docs/page.tsx
index 31783b43b..aaf4f01ff 100644
--- a/apps/web/app/(marketing)/docs/page.tsx
+++ b/apps/web/app/(marketing)/docs/page.tsx
@@ -19,11 +19,12 @@ async function DocsPage() {
const { t } = await createI18nServerInstance();
const docs = await client.getContentItems({
- type: 'page',
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 (
);
diff --git a/packages/cms/contentlayer/content/docs/001-getting_started/001-getting_started.mdx b/packages/cms/contentlayer/content/docs/001-getting_started/001-getting_started.mdx
index ab4f3fe61..6c06a58b7 100644
--- a/packages/cms/contentlayer/content/docs/001-getting_started/001-getting_started.mdx
+++ b/packages/cms/contentlayer/content/docs/001-getting_started/001-getting_started.mdx
@@ -2,6 +2,8 @@
title: Installing Makerkit
label: Installing Makerkit
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
diff --git a/packages/cms/contentlayer/content/docs/001-getting_started/002-clone-repository.mdx b/packages/cms/contentlayer/content/docs/001-getting_started/002-clone-repository.mdx
index 4fe33013d..18643ac29 100644
--- a/packages/cms/contentlayer/content/docs/001-getting_started/002-clone-repository.mdx
+++ b/packages/cms/contentlayer/content/docs/001-getting_started/002-clone-repository.mdx
@@ -2,6 +2,8 @@
title: Clone the MakerKit SaaS boilerplate repository
label: Clone the repository
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
diff --git a/packages/cms/contentlayer/content/docs/001-getting_started/index.mdx b/packages/cms/contentlayer/content/docs/001-getting_started/index.mdx
index a84d8d0c1..3217485c5 100644
--- a/packages/cms/contentlayer/content/docs/001-getting_started/index.mdx
+++ b/packages/cms/contentlayer/content/docs/001-getting_started/index.mdx
@@ -2,6 +2,8 @@
title: Getting Started
label: Getting Started
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.
diff --git a/packages/cms/contentlayer/content/docs/002-authentication/001-configuration.mdx b/packages/cms/contentlayer/content/docs/002-authentication/001-configuration.mdx
index 5cd72c211..da623923b 100644
--- a/packages/cms/contentlayer/content/docs/002-authentication/001-configuration.mdx
+++ b/packages/cms/contentlayer/content/docs/002-authentication/001-configuration.mdx
@@ -2,6 +2,8 @@
title: Authentication Overview
label: Overview
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.
diff --git a/packages/cms/contentlayer/content/docs/002-authentication/002-setup.mdx b/packages/cms/contentlayer/content/docs/002-authentication/002-setup.mdx
index 0baf6e48e..d7f7b17ab 100644
--- a/packages/cms/contentlayer/content/docs/002-authentication/002-setup.mdx
+++ b/packages/cms/contentlayer/content/docs/002-authentication/002-setup.mdx
@@ -2,6 +2,8 @@
title: Supabase Setup
label: Supabase Setup
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.
diff --git a/packages/cms/contentlayer/content/docs/002-authentication/index.mdx b/packages/cms/contentlayer/content/docs/002-authentication/index.mdx
index 7496890e2..bc7f24cda 100644
--- a/packages/cms/contentlayer/content/docs/002-authentication/index.mdx
+++ b/packages/cms/contentlayer/content/docs/002-authentication/index.mdx
@@ -2,6 +2,8 @@
title: Authentication
label: Authentication
description: Learn everything about Authentication in Makerkit
+categories:
+ - documentation
---
MakerKit uses Supabase to manage authentication within your application.
diff --git a/packages/cms/contentlayer/content/posts/post-01.mdx b/packages/cms/contentlayer/content/posts/post-01.mdx
index 32630c3c0..31a1a856f 100644
--- a/packages/cms/contentlayer/content/posts/post-01.mdx
+++ b/packages/cms/contentlayer/content/posts/post-01.mdx
@@ -6,7 +6,7 @@ description: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiu
image: /assets/images/posts/lorem-ipsum.webp
author: John Doe
categories:
- - posts
+ - blog
---
## Fecerat avis invenio mentis
diff --git a/packages/cms/contentlayer/contentlayer.config.js b/packages/cms/contentlayer/contentlayer.config.js
index 2251a15cc..e3e8215d4 100644
--- a/packages/cms/contentlayer/contentlayer.config.js
+++ b/packages/cms/contentlayer/contentlayer.config.js
@@ -66,6 +66,13 @@ export const Post = defineDocumentType(() => ({
type: 'string',
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: {
type: 'object',
resolve: (doc) => ({
@@ -124,6 +131,33 @@ export const DocumentationPage = defineDocumentType(() => ({
type: 'number',
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: {
type: 'object',
resolve: (doc) => ({
diff --git a/packages/cms/contentlayer/src/client.ts b/packages/cms/contentlayer/src/client.ts
index cbfd4bddb..a249719a5 100644
--- a/packages/cms/contentlayer/src/client.ts
+++ b/packages/cms/contentlayer/src/client.ts
@@ -7,14 +7,7 @@ async function getAllContentItems() {
'../.contentlayer/generated'
);
- return [
- ...allPosts.map((item) => {
- return { ...item, type: 'post' };
- }),
- ...allDocumentationPages.map((item) => {
- return { ...item, type: 'page', categories: ['documentation'] };
- }),
- ];
+ return [...allPosts, ...allDocumentationPages];
}
/**
@@ -41,29 +34,14 @@ export class ContentlayerClient implements CmsClient {
)
: true;
- const typeMatch = options?.type ? item.type === options.type : true;
- const path = item._raw.flattenedPath;
- const splitPath = path.split('/');
+ const matchesParentIds = options?.parentIds
+ ? options.parentIds.includes(item.parentId ?? '')
+ : true;
- const depthMatch =
- options?.depth !== undefined
- ? splitPath.length - 1 === options.depth
- : true;
-
- return tagMatch && categoryMatch && typeMatch && depthMatch;
+ return tagMatch && categoryMatch && matchesParentIds;
})
- .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);
- });
+ .map((post) => this.mapPost(post))
+ .slice(startOffset, endOffset);
return Promise.resolve(promise);
}
@@ -76,15 +54,7 @@ export class ContentlayerClient implements CmsClient {
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);
+ return Promise.resolve(post ? this.mapPost(post) : undefined);
}
async getCategoryBySlug(slug: string) {
@@ -108,13 +78,6 @@ export class ContentlayerClient implements CmsClient {
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)
@@ -132,13 +95,6 @@ export class ContentlayerClient implements CmsClient {
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)
@@ -167,9 +123,10 @@ export class ContentlayerClient implements CmsClient {
title: post.title,
description: post.description ?? '',
content: post.body?.code,
+ order: 'order' in post ? post.order : 0,
image: 'image' in post ? post.image : undefined,
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,
slug: post.slug,
author: 'author' in post ? post.author : '',
diff --git a/packages/cms/core/src/cms-client.ts b/packages/cms/core/src/cms-client.ts
index c5f5d2737..5b6191664 100644
--- a/packages/cms/core/src/cms-client.ts
+++ b/packages/cms/core/src/cms-client.ts
@@ -1,11 +1,8 @@
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Cms {
- export type ContentType = 'post' | 'page';
-
export interface ContentItem {
id: string;
title: string;
- type: ContentType;
url: string;
description: string | undefined;
content: string;
@@ -15,8 +12,9 @@ export namespace Cms {
slug: string;
categories: Category[];
tags: Tag[];
- parentId?: string;
- children?: ContentItem[];
+ order: number;
+ children: ContentItem[];
+ parentId: string | undefined;
}
export interface Category {
@@ -32,44 +30,73 @@ export namespace Cms {
}
export interface GetContentItemsOptions {
- type?: ContentType;
limit?: number;
offset?: number;
categories?: string[];
tags?: string[];
- depth?: number;
+ parentIds?: string[];
}
export interface GetCategoriesOptions {
- type?: ContentType;
+ slugs?: string[];
limit?: number;
offset?: number;
}
export interface GetTagsOptions {
- type?: ContentType;
+ slugs?: string[];
limit?: number;
offset?: number;
}
}
+/**
+ * Abstract class representing a CMS client.
+ */
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(
options?: Cms.GetContentItemsOptions,
): Promise;
- abstract getContentItemById(
- id: string,
- type?: string,
- ): Promise;
+ /**
+ * Retrieves a content item by its ID and type.
+ * @param id - The ID of the content item.
+ * @returns A promise that resolves to the content item, or undefined if not found.
+ */
+ abstract getContentItemById(id: string): Promise;
+ /**
+ * 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(
options?: Cms.GetCategoriesOptions,
): Promise;
+ /**
+ * 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;
+ /**
+ * 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;
+ /**
+ * 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;
}
diff --git a/packages/cms/wordpress/README.md b/packages/cms/wordpress/README.md
index a99083de0..3735ffc74 100644
--- a/packages/cms/wordpress/README.md
+++ b/packages/cms/wordpress/README.md
@@ -41,4 +41,23 @@ You will be asked to set up the Wordpress instance when you visit `http://localh
## 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.
\ No newline at end of file
+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.
\ No newline at end of file
diff --git a/packages/cms/wordpress/docker-compose.yml b/packages/cms/wordpress/docker-compose.yml
index 0bc568b51..ebfbe68ca 100644
--- a/packages/cms/wordpress/docker-compose.yml
+++ b/packages/cms/wordpress/docker-compose.yml
@@ -21,10 +21,28 @@ services:
ports:
- 8080:80
restart: always
+ working_dir: /var/www/html
+ volumes:
+ - ./wp-content:/var/www/html/wp-content
environment:
- WORDPRESS_DB_HOST=db
- WORDPRESS_DB_USER=wordpress
- WORDPRESS_DB_PASSWORD=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:
db_data:
\ No newline at end of file
diff --git a/packages/cms/wordpress/src/wp-client.ts b/packages/cms/wordpress/src/wp-client.ts
index 2f6df186a..5fc764fd6 100644
--- a/packages/cms/wordpress/src/wp-client.ts
+++ b/packages/cms/wordpress/src/wp-client.ts
@@ -20,93 +20,83 @@ export class WordpressClient implements CmsClient {
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) {
- let endpoint: string;
-
- 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);
+ const queryParams = new URLSearchParams({
+ _embed: 'true',
+ });
if (options?.limit) {
- url.searchParams.append('per_page', options.limit.toString());
+ queryParams.append('per_page', options.limit.toString());
}
if (options?.offset) {
- url.searchParams.append('offset', options.offset.toString());
+ queryParams.append('offset', options.offset.toString());
}
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) {
- 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());
- const data = (await response.json()) as WP_REST_API_Post[];
+ if (options?.parentIds && options.parentIds.length > 0) {
+ queryParams.append('parent', options.parentIds.join(','));
+ }
- return Promise.all(
- data.map(async (item) => {
- // Fetch author, categories, and tags as before...
+ const endpoints = [
+ `/wp-json/wp/v2/pages?${queryParams.toString()}`,
+ `/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;
+ if (!item) {
+ throw new Error('Failed to fetch content items');
+ }
+
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: 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 categories = await this.getCategoriesByIds(item.categories ?? []);
const tags = await this.getTagsByIds(item.tags ?? []);
const image = item.featured_media ? this.getFeaturedMedia(item) : '';
+ const order = item.menu_order ?? 0;
return {
id: item.id.toString(),
@@ -120,19 +110,34 @@ export class WordpressClient implements CmsClient {
author: author?.name,
categories: categories,
tags: tags,
- type: item.type as Cms.ContentType,
parentId,
- children,
+ order,
+ children: [],
};
}),
);
}
- async getContentItemById(slug: string, type: 'posts' | 'pages' = 'posts') {
- const url = `${this.apiUrl}/wp-json/wp/v2/${type}?slug=${slug}&_embed`;
- const response = await fetch(url);
- const data = (await response.json()) as WP_REST_API_Post[];
- const item = data[0];
+ async getContentItemById(slug: string) {
+ const searchParams = new URLSearchParams({
+ _embed: 'true',
+ slug,
+ });
+
+ 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) {
return;
@@ -146,9 +151,9 @@ export class WordpressClient implements CmsClient {
return {
id: item.id.toString(),
image,
+ order: item.menu_order ?? 0,
url: item.link,
description: item.excerpt.rendered,
- type: item.type as Cms.ContentType,
children: [],
title: item.title.rendered,
content: item.content.rendered,
@@ -157,6 +162,7 @@ export class WordpressClient implements CmsClient {
author: author?.name,
categories,
tags,
+ parentId: item.parent?.toString(),
};
}
@@ -207,10 +213,22 @@ export class WordpressClient implements CmsClient {
queryParams.append('offset', options.offset.toString());
}
+ if (options?.slugs) {
+ const slugs = options.slugs.join(',');
+
+ queryParams.append('slug', slugs);
+ }
+
const response = await fetch(
`${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[];
return data.map((item) => ({
@@ -231,10 +249,21 @@ export class WordpressClient implements CmsClient {
queryParams.append('offset', options.offset.toString());
}
+ if (options?.slugs) {
+ const slugs = options.slugs.join(',');
+ queryParams.append('slug', slugs);
+ }
+
const response = await fetch(
`${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[];
return data.map((item) => ({
diff --git a/packages/cms/wordpress/wp-content/themes/twentytwentyfour/footer.php b/packages/cms/wordpress/wp-content/themes/twentytwentyfour/footer.php
new file mode 100644
index 000000000..4c4600a74
--- /dev/null
+++ b/packages/cms/wordpress/wp-content/themes/twentytwentyfour/footer.php
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/packages/cms/wordpress/wp-content/themes/twentytwentyfour/functions.php b/packages/cms/wordpress/wp-content/themes/twentytwentyfour/functions.php
new file mode 100644
index 000000000..65cf44243
--- /dev/null
+++ b/packages/cms/wordpress/wp-content/themes/twentytwentyfour/functions.php
@@ -0,0 +1,23 @@
+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');
\ No newline at end of file
diff --git a/packages/cms/wordpress/wp-content/themes/twentytwentyfour/index.php b/packages/cms/wordpress/wp-content/themes/twentytwentyfour/index.php
new file mode 100644
index 000000000..e882eb0ee
--- /dev/null
+++ b/packages/cms/wordpress/wp-content/themes/twentytwentyfour/index.php
@@ -0,0 +1,42 @@
+
+>
+
+
+
+
+
+
+
+>
+
+
+
+
+
+
+
+
\ No newline at end of file