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

@@ -7,6 +7,9 @@ NEXT_PUBLIC_DEFAULT_THEME_MODE=light
NEXT_PUBLIC_THEME_COLOR="#ffffff"
NEXT_PUBLIC_THEME_COLOR_DARK="#0a0a0a"
# CMS
CMS_CLIENT=contentlayer
# AUTH
NEXT_PUBLIC_AUTH_PASSWORD=true
NEXT_PUBLIC_AUTH_MAGIC_LINK=false

View File

@@ -1,12 +1,10 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import Script from 'next/script';
import { allPosts } from 'contentlayer/generated';
import { createCmsClient } from '@kit/cms';
import Post from '~/(marketing)/blog/_components/post';
import appConfig from '~/config/app.config';
import { withI18n } from '~/lib/i18n/with-i18n';
export async function generateMetadata({
@@ -14,14 +12,14 @@ export async function generateMetadata({
}: {
params: { slug: string };
}): Promise<Metadata | undefined> {
const post = allPosts.find((post) => post.slug === params.slug);
const cms = await createCmsClient();
const post = await cms.getContentItemById(params.slug);
if (!post) {
notFound();
}
const { title, date, description, image, slug } = post;
const url = [appConfig.url, 'blog', slug].join('/');
const { title, publishedAt, description, image } = post;
return Promise.resolve({
title,
@@ -30,8 +28,8 @@ export async function generateMetadata({
title,
description,
type: 'article',
publishedTime: date,
url,
publishedTime: publishedAt.toDateString(),
url: post.url,
images: image
? [
{
@@ -49,8 +47,9 @@ export async function generateMetadata({
});
}
function BlogPost({ params }: { params: { slug: string } }) {
const post = allPosts.find((post) => post.slug === params.slug);
async function BlogPost({ params }: { params: { slug: string } }) {
const cms = await createCmsClient();
const post = await cms.getContentItemById(params.slug);
if (!post) {
notFound();
@@ -58,11 +57,7 @@ function BlogPost({ params }: { params: { slug: string } }) {
return (
<div className={'container mx-auto'}>
<Script id={'ld-json'} type="application/ld+json">
{JSON.stringify(post.structuredData)}
</Script>
<Post post={post} content={post.body.code} />
<Post post={post} content={post.content} />
</div>
);
}

View File

@@ -1,5 +1,4 @@
import type { Post } from 'contentlayer/generated';
import { Cms } from '@kit/cms';
import { Heading } from '@kit/ui/heading';
import { If } from '@kit/ui/if';
@@ -7,9 +6,9 @@ import { CoverImage } from '~/(marketing)/blog/_components/cover-image';
import { DateFormatter } from '~/(marketing)/blog/_components/date-formatter';
export const PostHeader: React.FC<{
post: Post;
post: Cms.ContentItem;
}> = ({ post }) => {
const { title, date, readingTime, description, image } = post;
const { title, publishedAt, description, image } = post;
// NB: change this to display the post's image
const displayImage = true;
@@ -30,11 +29,8 @@ export const PostHeader: React.FC<{
<div className="flex">
<div className="flex flex-row items-center space-x-2 text-sm text-gray-600 dark:text-gray-400">
<div>
<DateFormatter dateString={date} />
<DateFormatter dateString={publishedAt.toISOString()} />
</div>
<span>·</span>
<span>{readingTime} minutes reading</span>
</div>
</div>

View File

@@ -1,14 +1,13 @@
import Link from 'next/link';
import type { Post } from 'contentlayer/generated';
import { Cms } from '@kit/cms';
import { If } from '@kit/ui/if';
import { CoverImage } from '~/(marketing)/blog/_components/cover-image';
import { DateFormatter } from '~/(marketing)/blog/_components/date-formatter';
type Props = {
post: Post;
post: Cms.ContentItem;
preloadImage?: boolean;
imageHeight?: string | number;
};
@@ -20,15 +19,16 @@ export function PostPreview({
preloadImage,
imageHeight,
}: React.PropsWithChildren<Props>) {
const { title, image, date, readingTime, description } = post;
const { title, image, publishedAt, description } = post;
const height = imageHeight ?? DEFAULT_IMAGE_HEIGHT;
const url = post.url;
return (
<div className="rounded-xl transition-shadow duration-500 dark:text-gray-800">
<div className="rounded-xl transition-shadow duration-500">
<If condition={image}>
{(imageUrl) => (
<div className="relative mb-2 w-full" style={{ height }}>
<Link href={post.url}>
<Link href={url}>
<CoverImage
preloadImage={preloadImage}
title={title}
@@ -40,27 +40,21 @@ export function PostPreview({
</If>
<div className={'px-1'}>
<div className="flex flex-col space-y-1 px-1 py-2">
<h3 className="px-1 text-2xl font-bold leading-snug dark:text-white">
<Link href={post.url} className="hover:underline">
<div className="flex flex-col space-y-1 py-2">
<h3 className="text-2xl font-bold leading-snug dark:text-white">
<Link href={url} className="hover:underline">
{title}
</Link>
</h3>
</div>
<div className="mb-2 flex flex-row items-center space-x-2 px-1 text-sm">
<div className="text-gray-600 dark:text-gray-300">
<DateFormatter dateString={date} />
<div className="mb-2 flex flex-row items-center space-x-2 text-sm">
<div className="text-muted-foreground">
<DateFormatter dateString={publishedAt.toISOString()} />
</div>
<span className="text-gray-600 dark:text-gray-300">·</span>
<span className="text-gray-600 dark:text-gray-300">
{readingTime} mins reading
</span>
</div>
<p className="mb-4 px-1 text-sm leading-relaxed dark:text-gray-300">
<p className="mb-4 text-sm leading-relaxed text-muted-foreground">
{description}
</p>
</div>

View File

@@ -1,15 +1,10 @@
import dynamic from 'next/dynamic';
import type { Post as PostType } from 'contentlayer/generated';
import type { Cms } from '@kit/cms';
import { ContentRenderer } from '@kit/cms';
import { PostHeader } from './post-header';
const Mdx = dynamic(() =>
import('@kit/ui/mdx').then((mod) => ({ default: mod.Mdx })),
);
export const Post: React.FC<{
post: PostType;
post: Cms.ContentItem;
content: string;
}> = ({ post, content }) => {
return (
@@ -17,7 +12,7 @@ export const Post: React.FC<{
<PostHeader post={post} />
<article className={'mx-auto flex justify-center'}>
<Mdx code={content} />
<ContentRenderer content={content} />
</article>
</div>
);

View File

@@ -1,6 +1,6 @@
import type { Metadata } from 'next';
import { allPosts } from 'contentlayer/generated';
import { createCmsClient } from '@kit/cms';
import { GridList } from '~/(marketing)/_components/grid-list';
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
@@ -13,11 +13,11 @@ export const metadata: Metadata = {
description: `Tutorials, Guides and Updates from our team`,
};
function BlogPage() {
const livePosts = allPosts.filter((post) => {
const isProduction = appConfig.production;
async function BlogPage() {
const cms = await createCmsClient();
return isProduction ? post.live : true;
const posts = await cms.getContentItems({
type: 'post',
});
return (
@@ -29,7 +29,7 @@ function BlogPage() {
/>
<GridList>
{livePosts.map((post, idx) => {
{posts.map((post, idx) => {
return <PostPreview key={idx} post={post} />;
})}
</GridList>

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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',
}}
/>
);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,
};
});
},
);

View File

@@ -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);
});

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -1,17 +1,26 @@
import { invariant } from '@epic-web/invariant';
import { allDocumentationPages, allPosts } from 'contentlayer/generated';
import { getServerSideSitemap } from 'next-sitemap';
import { createCmsClient } from '@kit/cms';
import appConfig from '~/config/app.config';
invariant(appConfig.url, 'No NEXT_PUBLIC_SITE_URL environment variable found');
export async function GET() {
const urls = getSiteUrls();
const posts = getPostsSitemap();
const docs = getDocsSitemap();
const client = await createCmsClient();
const contentItems = await client.getContentItems();
return getServerSideSitemap([...urls, ...posts, ...docs]);
return getServerSideSitemap([
...urls,
...contentItems.map((item) => {
return {
loc: new URL(item.url, appConfig.url).href,
lastmod: new Date().toISOString(),
};
}),
]);
}
function getSiteUrls() {
@@ -24,21 +33,3 @@ function getSiteUrls() {
};
});
}
function getPostsSitemap() {
return allPosts.map((post) => {
return {
loc: new URL(post.url, appConfig.url).href,
lastmod: new Date().toISOString(),
};
});
}
function getDocsSitemap() {
return allDocumentationPages.map((page) => {
return {
loc: new URL(page.url, appConfig.url).href,
lastmod: new Date().toISOString(),
};
});
}

View File

@@ -1,17 +0,0 @@
---
title: Running the Next.js Server
label: Next.js
description: Learn how to run the Next.js server on your local machine.
---
First, we can run the Next.js Server by running the following command:
```bash
npm run dev
```
If everything goes well, your server should be running at
[http://localhost:3000](http://localhost:3000).
With the server running, we can now set up our Supabase containers using
Docker. Jump to the next section to learn how to do that.

View File

@@ -1,88 +0,0 @@
---
title: Running the Supabase Containers
label: Supabase
description: Running the Supabase containers locally for development
---
Before we can run the Supabase local environment, we need to run Docker, as Supabase uses it for running its local environment.
You can use Docker Desktop, Colima, OrbStack, or any other Docker-compatible solution.
### Running the Supabase Environment
First, let's run the Supabase environment, which will spin up a local
instance using Docker. We can do this by running the following command:
```bash
npm run supabase:start
```
Additionally, it imports the default seed data. We use it this data to
populate the database with some initial data and execute the E2E tests.
After running the command above, you will be able to access the Supabase
Studio UI at [http://localhost:54323/](http://localhost:54323/).
### Adding the Supabase Keys to the Environment Variables
If this is the first time you run this command, we will need to get the
Supabase keys and add them to our local environment variables configuration
file `.env`.
When running the command, we will see a message like this:
```bash
> supabase start
Applying migration 20221215192558_schema.sql...
Seeding data supabase/seed.sql...
Started supabase local development setup.
API URL: http://localhost:54321
DB URL: postgresql://postgres:postgres@localhost:54322/postgres
Studio URL: http://localhost:54323
Inbucket URL: http://localhost:54324
JWT secret: super-secret-jwt-token-with-at-least-32-characters-long
anon key: ****************************************************
service_role key: ****************************************************
```
Now, we need to copy the `anon key` and `service_role key` values and add
them to the `.env` file:
```
NEXT_PUBLIC_SUPABASE_ANON_KEY=****************************************************
SUPABASE_SERVICE_ROLE_KEY=****************************************************
```
### Running the Stripe CLI
Run the Stripe CLI with the following command:
```bash
npm run stripe:listen
```
#### Add the Stripe Webhooks Key to your environment file
If this is the first time you run this command, you will need to copy the Webhooks key printed on the console and add it to your development environment variables file:
```bash title=".env.development"
STRIPE_WEBHOOKS_KEY=<PASTE_KEY_HERE>
```
#### Signing In for the first time
You should now be able to sign in. To quickly get started, use the following credentials:
```
email = test@makerkit.dev
password = testingpassword
```
#### Email Confirmations
When signing up, Supabase sends an email confirmation to a testing account. You can access the InBucket testing emails [using the following link](http://localhost:54324/monitor), and can follow the links to complete the sign up process.

View File

@@ -1,19 +0,0 @@
---
title: Running the Stripe CLI for Webhooks
label: Stripe
description: How to run the Stripe CLI for Webhooks in a local development environment
---
Run the Stripe CLI with the following command:
```bash
npm run stripe:listen
```
#### Add the Stripe Webhooks Key to your environment file
If this is the first time you run this command, you will need to copy the Webhooks key printed on the console and add it to your development environment variables file:
```bash title=".env.development"
STRIPE_WEBHOOKS_KEY=<PASTE_KEY_HERE>
```

View File

@@ -1,20 +0,0 @@
---
title: Running the Application
label: Running the Application
description: How to run the application in development mode
---
After installing the modules, we can finally run the
application in development mode.
We need to execute two commands (and an optional one for Stripe):
1. **Next.js Server**: the first command is for running the Next.js server
2. **Supabase Environment**: the second command is for running the Supabase
environment with Docker
3. **Stripe CLI**: finally, the Stripe CLI is needed to dispatch webhooks to
our local server (optional, only needed when interacting with Stripe)
## About this Documentation
This documentation complements the Supabase one and is not meant to be a replacement. We recommend reading the Supabase documentation to get a better understanding of the Supabase concepts and how to use it.

View File

@@ -16,7 +16,8 @@ const INTERNAL_PACKAGES = [
'@kit/billing-gateway',
'@kit/stripe',
'@kit/email-templates',
'@kit/database-webhooks'
'@kit/database-webhooks',
'@kit/cms'
];
/** @type {import('next').NextConfig} */

View File

@@ -30,6 +30,7 @@
"@kit/supabase": "workspace:^",
"@kit/team-accounts": "workspace:^",
"@kit/ui": "workspace:^",
"@kit/cms": "workspace:^",
"@next/mdx": "^14.1.4",
"@radix-ui/react-icons": "^1.3.0",
"@supabase/ssr": "^0.1.0",
@@ -37,13 +38,11 @@
"@tanstack/react-query": "5.28.6",
"@tanstack/react-query-next-experimental": "^5.28.9",
"@tanstack/react-table": "^8.15.0",
"contentlayer": "0.3.4",
"date-fns": "^3.6.0",
"edge-csrf": "^1.0.9",
"i18next": "^23.10.1",
"i18next-resources-to-backend": "^1.2.0",
"next": "v14.2.0-canary.49",
"next-contentlayer": "0.3.4",
"next": "v14.2.0-canary.50",
"next-sitemap": "^4.2.3",
"next-themes": "0.3.0",
"react": "18.2.0",
@@ -51,8 +50,6 @@
"react-hook-form": "^7.51.2",
"react-i18next": "^14.1.0",
"recharts": "^2.12.3",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"sonner": "^1.4.41",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.22.4"

View File

@@ -6,8 +6,7 @@
"~/*": ["./app/*"],
"~/config/*": ["./config/*"],
"~/components/*": ["./components/*"],
"~/lib/*": ["./lib/*"],
"contentlayer/generated": ["./.contentlayer/generated"]
"~/lib/*": ["./lib/*"]
},
"plugins": [
{
@@ -22,7 +21,7 @@
"*.ts",
"*.tsx",
"*.mjs",
"config/**/*.ts",
"./config/**/*.ts",
"components/**/*.{tsx|ts}",
"lib/**/*.ts",
"app"

View File

@@ -28,6 +28,7 @@
"apps/*",
"packages/*",
"packages/features/*",
"packages/cms/*",
"tooling/*",
"supabase"
],

View File

@@ -70,7 +70,7 @@ export function CurrentPlanCard({
</CardDescription>
</CardHeader>
<CardContent className={'space-y-2.5 text-sm'}>
<CardContent className={'space-y-3 text-sm'}>
<div className={'flex flex-col space-y-1'}>
<div className={'flex items-center space-x-2 text-lg font-semibold'}>
<BadgeCheck
@@ -85,85 +85,58 @@ export function CurrentPlanCard({
</div>
</div>
<div>
<CurrentPlanAlert status={subscription.status} />
</div>
{/*
Only show the alert if the subscription requires action
(e.g. trial ending soon, subscription canceled, etc.)
*/}
<If condition={!subscription.active}>
<div>
<CurrentPlanAlert status={subscription.status} />
</div>
</If>
<div>
<Accordion type="single" collapsible>
<AccordionItem value="features">
<AccordionTrigger>
<Trans i18nKey="billing:planDetails" />
</AccordionTrigger>
<If condition={subscription.status === 'trialing'}>
<div className="flex flex-col space-y-0.5">
<span className="font-semibold">
<Trans i18nKey="billing:trialEndsOn" />
</span>
<AccordionContent className="space-y-2.5">
<If condition={subscription.status === 'trialing'}>
<div className="flex flex-col">
<span className="font-medium">
<Trans i18nKey="billing:trialEndsOn" />
</span>
<div className={'text-muted-foreground'}>
<span>
{subscription.trial_ends_at
? formatDate(subscription.trial_ends_at, 'P')
: ''}
</span>
</div>
</div>
</If>
<div className={'text-muted-foreground'}>
<span>
{subscription.trial_ends_at
? formatDate(subscription.trial_ends_at, 'P')
: ''}
</span>
</div>
</div>
</If>
<If condition={subscription.cancel_at_period_end}>
<div className="flex flex-col space-y-0.5">
<span className="font-semibold">
<Trans i18nKey="billing:cancelSubscriptionDate" />
</span>
<If condition={subscription.cancel_at_period_end}>
<div className="flex flex-col">
<span className="font-medium">
<Trans i18nKey="billing:cancelSubscriptionDate" />
</span>
<div className={'text-muted-foreground'}>
<span>
{formatDate(subscription.period_ends_at ?? '', 'P')}
</span>
</div>
</div>
</If>
<div className={'text-muted-foreground'}>
<span>
{formatDate(subscription.period_ends_at ?? '', 'P')}
</span>
</div>
</div>
</If>
<div className="flex flex-col space-y-0.5">
<span className="font-semibold">
<Trans i18nKey="billing:detailsLabel" />
</span>
<div className="flex flex-col space-y-1">
<span className="font-semibold">
<Trans i18nKey="billing:detailsLabel" />
</span>
<LineItemDetails
lineItems={productLineItems}
currency={subscription.currency}
selectedInterval={firstLineItem.interval}
/>
</div>
<div className="flex flex-col space-y-1">
<span className="font-semibold">
<Trans i18nKey="billing:featuresLabel" />
</span>
<ul className={'flex flex-col space-y-0.5'}>
{product.features.map((item) => {
return (
<li
key={item}
className="flex items-center space-x-0.5"
>
<CheckCircle2 className="h-4 text-green-500" />
<span className={'text-muted-foreground'}>
<Trans i18nKey={item} defaults={item} />
</span>
</li>
);
})}
</ul>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<LineItemDetails
lineItems={productLineItems}
currency={subscription.currency}
selectedInterval={firstLineItem.interval}
/>
</div>
</div>
</CardContent>
</Card>

View File

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

View File

@@ -4,6 +4,9 @@ date: 2021-12-24
live: false
description: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
image: /assets/images/posts/lorem-ipsum.webp
author: John Doe
categories:
- posts
---
## Fecerat avis invenio mentis

View File

@@ -19,6 +19,11 @@ export const Post = defineDocumentType(() => ({
description: 'The date of the post',
required: true,
},
author: {
type: 'string',
description: 'The author of the post',
required: true,
},
live: {
type: 'boolean',
description: 'Whether the post is live or not',
@@ -33,12 +38,22 @@ export const Post = defineDocumentType(() => ({
type: 'string',
description: 'The description of the post',
},
tags: {
type: 'list',
required: false,
of: {
type: 'string',
},
},
categories: {
type: 'list',
required: false,
of: {
type: 'string',
},
},
},
computedFields: {
url: {
type: 'string',
resolve: (post) => `/blog/${getSlug(post._raw.sourceFileName)}`,
},
readingTime: {
type: 'number',
resolve: (post) => calculateReadingTime(post.body.raw),
@@ -47,6 +62,10 @@ export const Post = defineDocumentType(() => ({
type: 'string',
resolve: (post) => getSlug(post._raw.sourceFileName),
},
url: {
type: 'string',
resolve: (post) => `/blog/${getSlug(post._raw.sourceFileName)}`,
},
structuredData: {
type: 'object',
resolve: (doc) => ({
@@ -57,7 +76,6 @@ export const Post = defineDocumentType(() => ({
dateModified: doc.date,
description: doc.description,
image: [siteUrl, doc.image].join(''),
url: [siteUrl, 'blog', doc._raw.flattenedPath].join('/'),
author: {
'@type': 'Organization',
name: `Makerkit`,
@@ -82,43 +100,30 @@ export const DocumentationPage = defineDocumentType(() => ({
description: 'The label of the page in the sidebar',
required: true,
},
cardCTA: {
type: 'string',
description: 'The label of the CTA link on the card',
required: false,
},
description: {
type: 'string',
description: 'The description of the post',
},
show_child_cards: {
type: 'boolean',
default: false,
},
collapsible: {
type: 'boolean',
tags: {
type: 'list',
required: false,
default: false,
of: {
type: 'string',
},
},
collapsed: {
type: 'boolean',
categories: {
type: 'list',
required: false,
default: false,
of: {
type: 'string',
},
},
},
computedFields: {
url: {
type: 'string',
resolve: (post) => `/blog/${getSlug(post._raw.sourceFileName)}`,
},
readingTime: {
type: 'number',
resolve: (post) => calculateReadingTime(post.body.raw),
},
slug: {
type: 'string',
resolve: (post) => getSlug(post._raw.sourceFileName),
},
structuredData: {
type: 'object',
resolve: (doc) => ({
@@ -150,13 +155,24 @@ export const DocumentationPage = defineDocumentType(() => ({
type: 'json',
resolve: (doc) => getPathSegments(doc).map(getMetaFromFolderName),
},
resolvedPath: {
slug: {
type: 'string',
resolve: (doc) => {
return getPathSegments(doc)
resolve: (doc) =>
getPathSegments(doc)
.map(getMetaFromFolderName)
.map(({ pathName }) => pathName)
.join('/');
.join('/'),
},
url: {
type: 'string',
resolve: (doc) => {
return (
'/docs/' +
getPathSegments(doc)
.map(getMetaFromFolderName)
.map(({ pathName }) => pathName)
.join('/')
);
},
},
},

View File

@@ -0,0 +1,45 @@
{
"name": "@kit/contentlayer",
"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": {
"contentlayer": "0.3.4",
"next-contentlayer": "0.3.4",
"rehype-slug": "^6.0.0",
"rehype-autolink-headings": "^6.0.0"
},
"peerDependencies": {
"@kit/cms": "workspace:^",
"@kit/ui": "workspace:^"
},
"devDependencies": {
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*"
},
"eslintConfig": {
"root": true,
"extends": [
"@kit/eslint-config/base",
"@kit/eslint-config/react"
]
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
}
}

View 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,
})) ?? [],
};
}
}

View File

@@ -0,0 +1,5 @@
import { Mdx } from './mdx/mdx-renderer';
export function ContentRenderer(props: { content: string }) {
return <Mdx code={props.content} />;
}

View File

@@ -0,0 +1,3 @@
export * from './client';
export * from './mdx/mdx-renderer';
export * from './content-renderer';

View File

@@ -1,7 +1,8 @@
import type { MDXComponents } from 'mdx/types';
import type { MDXComponents as MDXComponentsType } from 'mdx/types';
import { getMDXComponent } from 'next-contentlayer/hooks';
import Components from './mdx-components';
import { MDXComponents } from '@kit/ui/mdx-components';
// @ts-ignore: ignore weird error
import styles from './mdx-renderer.module.css';
@@ -14,7 +15,7 @@ export function Mdx({
return (
<div className={styles.MDX}>
<Component components={Components as unknown as MDXComponents} />
<Component components={MDXComponents as unknown as MDXComponentsType} />
</div>
);
}

View File

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

View File

@@ -0,0 +1,3 @@
# CMS - @kit/cms
CMS abstraction layer for the Makerkit framework.

View File

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

View File

@@ -0,0 +1,72 @@
// 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;
author: string;
publishedAt: Date;
image: string | undefined;
slug: string;
categories: Category[];
tags: Tag[];
parentId?: string;
children?: ContentItem[];
}
export interface Category {
id: string;
name: string;
slug: string;
}
export interface Tag {
id: string;
name: string;
slug: string;
}
export interface GetContentItemsOptions {
type?: ContentType;
limit?: number;
offset?: number;
categories?: string[];
tags?: string[];
depth?: number;
}
export interface GetCategoriesOptions {
type?: ContentType;
limit?: number;
offset?: number;
}
export interface GetTagsOptions {
type?: ContentType;
limit?: number;
offset?: number;
}
}
export abstract class CmsClient {
abstract getContentItems(
options?: Cms.GetContentItemsOptions,
): Promise<Cms.ContentItem[]>;
abstract getContentItemById(id: string): Promise<Cms.ContentItem | undefined>;
abstract getCategories(
options?: Cms.GetCategoriesOptions,
): Promise<Cms.Category[]>;
abstract getCategoryBySlug(slug: string): Promise<Cms.Category | undefined>;
abstract getTags(options?: Cms.GetTagsOptions): Promise<Cms.Tag[]>;
abstract getTagBySlug(slug: string): Promise<Cms.Tag | undefined>;
}

View File

@@ -0,0 +1,3 @@
// we can add more types here if we have more CMSs
// ex. export type CmsType = 'contentlayer' | 'other-cms';
export type CmsType = 'contentlayer' | 'wordpress';

View File

@@ -0,0 +1,17 @@
import { CmsType } from './cms.type';
export async function ContentRenderer({
content,
type = process.env.CMS_CLIENT as CmsType,
}: {
content: string;
type?: CmsType;
}) {
switch (type) {
case 'contentlayer': {
const { ContentRenderer } = await import('@kit/contentlayer');
return ContentRenderer({ content });
}
}
}

View File

@@ -0,0 +1,36 @@
import { CmsClient } from './cms-client';
import { CmsType } from './cms.type';
/**
* Creates a CMS client based on the specified type.
*
* @param {CmsType} type - The type of CMS client to create. Defaults to the value of the CMS_CLIENT environment variable.
* @returns {Promise<CmsClient>} A Promise that resolves to the created CMS client.
* @throws {Error} If the specified CMS type is unknown.
*/
export async function createCmsClient(
type: CmsType = process.env.CMS_CLIENT as CmsType,
): Promise<CmsClient> {
switch (type) {
case 'contentlayer':
return getContentLayerClient();
case 'wordpress':
return getWordpressClient();
default:
throw new Error(`Unknown CMS type: ${type}`);
}
}
async function getContentLayerClient() {
const { ContentlayerClient } = await import('@kit/contentlayer');
return new ContentlayerClient();
}
async function getWordpressClient() {
const { WordpressClient } = await import('@kit/wordpress');
return new WordpressClient();
}

View File

@@ -0,0 +1,4 @@
export * from './cms-client';
export * from './create-cms-client';
export * from './cms.type';
export * from './content-renderer';

View File

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

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"]
}

View File

@@ -119,7 +119,7 @@
"./auth-change-listener": "./src/makerkit/auth-change-listener.tsx",
"./loading-overlay": "./src/makerkit/loading-overlay.tsx",
"./profile-avatar": "./src/makerkit/profile-avatar.tsx",
"./mdx": "./src/makerkit/mdx/mdx-renderer.tsx",
"./mdx-components": "./src/makerkit/mdx-components.tsx",
"./mode-toggle": "./src/makerkit/mode-toggle.tsx"
},
"typesVersions": {

View File

@@ -2,8 +2,8 @@ import { forwardRef } from 'react';
import Image from 'next/image';
import { cn } from '../../utils';
import { LazyRender } from '../lazy-render';
import { cn } from '../utils';
import { LazyRender } from './lazy-render';
const NextImage: React.FC<{
width: number;
@@ -72,11 +72,9 @@ const Video: React.FC<{
);
};
const Components = {
export const MDXComponents = {
img: NextImage,
a: ExternalLink,
Video,
Image: NextImage,
};
export default Components;

386
pnpm-lock.yaml generated
View File

@@ -53,6 +53,9 @@ importers:
'@kit/billing-gateway':
specifier: workspace:^
version: link:../../packages/billing-gateway
'@kit/cms':
specifier: workspace:^
version: link:../../packages/cms/core
'@kit/database-webhooks':
specifier: workspace:^
version: link:../../packages/database-webhooks
@@ -94,19 +97,16 @@ importers:
version: 5.28.6(react@18.2.0)
'@tanstack/react-query-next-experimental':
specifier: ^5.28.9
version: 5.28.9(@tanstack/react-query@5.28.6)(next@14.2.0-canary.49)(react@18.2.0)
version: 5.28.9(@tanstack/react-query@5.28.6)(next@14.2.0-canary.50)(react@18.2.0)
'@tanstack/react-table':
specifier: ^8.15.0
version: 8.15.0(react-dom@18.2.0)(react@18.2.0)
contentlayer:
specifier: 0.3.4
version: 0.3.4(esbuild@0.20.2)
date-fns:
specifier: ^3.6.0
version: 3.6.0
edge-csrf:
specifier: ^1.0.9
version: 1.0.9(next@14.2.0-canary.49)
version: 1.0.9(next@14.2.0-canary.50)
i18next:
specifier: ^23.10.1
version: 23.10.1
@@ -114,14 +114,11 @@ importers:
specifier: ^1.2.0
version: 1.2.0
next:
specifier: v14.2.0-canary.49
version: 14.2.0-canary.49(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0)
next-contentlayer:
specifier: 0.3.4
version: 0.3.4(contentlayer@0.3.4)(esbuild@0.20.2)(next@14.2.0-canary.49)(react-dom@18.2.0)(react@18.2.0)
specifier: v14.2.0-canary.50
version: 14.2.0-canary.50(react-dom@18.2.0)(react@18.2.0)
next-sitemap:
specifier: ^4.2.3
version: 4.2.3(next@14.2.0-canary.49)
version: 4.2.3(next@14.2.0-canary.50)
next-themes:
specifier: 0.3.0
version: 0.3.0(react-dom@18.2.0)(react@18.2.0)
@@ -140,12 +137,6 @@ importers:
recharts:
specifier: ^2.12.3
version: 2.12.3(react-dom@18.2.0)(react@18.2.0)
rehype-autolink-headings:
specifier: ^7.1.0
version: 7.1.0
rehype-slug:
specifier: ^6.0.0
version: 6.0.0
sonner:
specifier: ^1.4.41
version: 1.4.41(react-dom@18.2.0)(react@18.2.0)
@@ -268,6 +259,78 @@ importers:
specifier: ^3.22.4
version: 3.22.4
packages/cms/contentlayer:
dependencies:
'@kit/cms':
specifier: workspace:^
version: link:../core
'@kit/ui':
specifier: workspace:^
version: link:../../ui
contentlayer:
specifier: 0.3.4
version: 0.3.4(esbuild@0.20.2)
next-contentlayer:
specifier: 0.3.4
version: 0.3.4(contentlayer@0.3.4)(esbuild@0.20.2)(next@13.5.6)(react-dom@18.2.0)(react@18.2.0)
rehype-autolink-headings:
specifier: ^6.0.0
version: 6.1.1
rehype-slug:
specifier: ^6.0.0
version: 6.0.0
devDependencies:
'@kit/eslint-config':
specifier: workspace:*
version: link:../../../tooling/eslint
'@kit/prettier-config':
specifier: workspace:*
version: link:../../../tooling/prettier
'@kit/tsconfig':
specifier: workspace:*
version: link:../../../tooling/typescript
packages/cms/core:
dependencies:
'@kit/contentlayer':
specifier: workspace:*
version: link:../contentlayer
'@kit/wordpress':
specifier: workspace:*
version: link:../wordpress
devDependencies:
'@kit/eslint-config':
specifier: workspace:*
version: link:../../../tooling/eslint
'@kit/prettier-config':
specifier: workspace:*
version: link:../../../tooling/prettier
'@kit/tsconfig':
specifier: workspace:*
version: link:../../../tooling/typescript
packages/cms/wordpress:
dependencies:
'@kit/cms':
specifier: workspace:^
version: link:../core
'@kit/ui':
specifier: workspace:^
version: link:../../ui
devDependencies:
'@kit/eslint-config':
specifier: workspace:*
version: link:../../../tooling/eslint
'@kit/prettier-config':
specifier: workspace:*
version: link:../../../tooling/prettier
'@kit/tsconfig':
specifier: workspace:*
version: link:../../../tooling/typescript
wp-types:
specifier: ^3.64.0
version: 3.64.0
packages/database-webhooks:
dependencies:
'@kit/billing-gateway':
@@ -2074,8 +2137,8 @@ packages:
resolution: {integrity: sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==}
dev: false
/@next/env@14.2.0-canary.49:
resolution: {integrity: sha512-rQaBRv0PRO3+4lx90zB9eBL0xk230G+6avgCyBL272hckH4XsGgXY6adtBBmZJF1QuDI+pS+DqppXSJvfexsdw==}
/@next/env@14.2.0-canary.50:
resolution: {integrity: sha512-COLktqbQGmSANtTTKVs4heykkT4YSLM+GU1CbHKpSXnyEP98yrWcfMTMeTwcEZCgilvI1gPT5zVO/ISU1o/X5A==}
dev: false
/@next/eslint-plugin-next@14.1.4:
@@ -2098,6 +2161,15 @@ packages:
source-map: 0.7.4
dev: false
/@next/swc-darwin-arm64@13.5.6:
resolution: {integrity: sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@next/swc-darwin-arm64@14.1.0:
resolution: {integrity: sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==}
engines: {node: '>= 10'}
@@ -2107,8 +2179,8 @@ packages:
dev: false
optional: true
/@next/swc-darwin-arm64@14.2.0-canary.49:
resolution: {integrity: sha512-tFFCgRJOk28rIiEGjz2bafqp3G5lV7hXyYjZ7d+gt/MjpLRrtTwu+lRBv/W1VFdTkPv8+k2hvXZNNTHO1n57Ow==}
/@next/swc-darwin-arm64@14.2.0-canary.50:
resolution: {integrity: sha512-el2drGIjRNuLqqahuCoKou50pEqacrcGvhOphiU8wQPWOku3d762sN9pTyunyLVshjLNOI/gDxh1Ja2dDcmXzg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@@ -2116,6 +2188,15 @@ packages:
dev: false
optional: true
/@next/swc-darwin-x64@13.5.6:
resolution: {integrity: sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@next/swc-darwin-x64@14.1.0:
resolution: {integrity: sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==}
engines: {node: '>= 10'}
@@ -2125,8 +2206,8 @@ packages:
dev: false
optional: true
/@next/swc-darwin-x64@14.2.0-canary.49:
resolution: {integrity: sha512-NR4Meb67q8M2pNP5a8Tp3Zfar2Ao8ChHWcD3wEBgICcgJ4ZyCQCWXdM+VBsf8a3yuAoXmu1/cwOwWu1KXVC96A==}
/@next/swc-darwin-x64@14.2.0-canary.50:
resolution: {integrity: sha512-gulXuO14RZODSB3hU+Rb+CHWymH7kGAcvsP7SA95wUXx2CAuugFfI90sHL4ieQib5N058DoIQPvYILiqP9RpxQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@@ -2134,6 +2215,15 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-gnu@13.5.6:
resolution: {integrity: sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-arm64-gnu@14.1.0:
resolution: {integrity: sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==}
engines: {node: '>= 10'}
@@ -2143,8 +2233,17 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-gnu@14.2.0-canary.49:
resolution: {integrity: sha512-2bFQUNYnz6L7xOAzvejMj09iqmWwkjFyguGEfmNiFN0kPgJ4viSCKZvoiuG/MPh3VoDSz5N2qx1tehSCy7KbFA==}
/@next/swc-linux-arm64-gnu@14.2.0-canary.50:
resolution: {integrity: sha512-6ZXM32VGQU1liB9+r3AHIsUZBbBQGMBpWdGnRzvCQ+AUVhL01x2P77AXCjhxeLUgrUisl3Cw+UDc+WvuLcygjQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-arm64-musl@13.5.6:
resolution: {integrity: sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -2161,8 +2260,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-musl@14.2.0-canary.49:
resolution: {integrity: sha512-68PjCGC1JghA2tuznu+ExeSP+L6qpf6afblB4wFhDRniP+0hRrZB+1E3jJ3PmBgHtitJJMaplTFeKYQ8xbF8xw==}
/@next/swc-linux-arm64-musl@14.2.0-canary.50:
resolution: {integrity: sha512-Is7FNrgY1ifBMKs9Y7fx6OJp7OjwfMMl8BhlN+UzbkMtZF9R45qLnRSWOu0gkQLqfqR0wx//Bmkr/d25qqZxjg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -2170,6 +2269,15 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-gnu@13.5.6:
resolution: {integrity: sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-x64-gnu@14.1.0:
resolution: {integrity: sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==}
engines: {node: '>= 10'}
@@ -2179,8 +2287,17 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-gnu@14.2.0-canary.49:
resolution: {integrity: sha512-eiDvo0bnYCI59UhaZrNV1k7wZPFHyQ2uJ7/MUH9yvZZcSKBxRDtNc3FmCAZjKiNx/SclMFRAtENLOlDzceRp5g==}
/@next/swc-linux-x64-gnu@14.2.0-canary.50:
resolution: {integrity: sha512-rKcciKNtCVrcj9zZ+JBK1AgIbeISHZz2OcTa/i1O3l+VwNDN25YAPaVDL0aPX6e9N0SR5W33b+bSQMHOc1FGhA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-x64-musl@13.5.6:
resolution: {integrity: sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -2197,8 +2314,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-musl@14.2.0-canary.49:
resolution: {integrity: sha512-XgwiLB/WkRjuhWoKZmlRsZl1b8C7dsYlRD3zqHPkrgWhERyyn3AoeRjIa/eHR6nxj7oTu2KHET1oSJoYobH70g==}
/@next/swc-linux-x64-musl@14.2.0-canary.50:
resolution: {integrity: sha512-tVFgS5lOa/h6h5//4p9mhcV7XThMAzMJQoC+j7y+yhnGnb17t4pQPR3FXAonncgm9OyCkA2N0O0hqwsnj6oCLA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -2206,6 +2323,15 @@ packages:
dev: false
optional: true
/@next/swc-win32-arm64-msvc@13.5.6:
resolution: {integrity: sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@next/swc-win32-arm64-msvc@14.1.0:
resolution: {integrity: sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==}
engines: {node: '>= 10'}
@@ -2215,8 +2341,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-arm64-msvc@14.2.0-canary.49:
resolution: {integrity: sha512-jqC5vhFOAewsGdWriuQqR2aalQ8dHJ1WkSl1psluTxpo5UgICBk+H0wQ93a0CEfD0Rj+8QjUFh+U1oYTqE4YIg==}
/@next/swc-win32-arm64-msvc@14.2.0-canary.50:
resolution: {integrity: sha512-1FmGWELLW7XdrNmJQca7vbBUIVOd84LGZQCO6gRIvmAw3Oh7S3UwP2rAsZ9K24Ox44TnK2xV4C4t9BV8PnHzMQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -2224,6 +2350,15 @@ packages:
dev: false
optional: true
/@next/swc-win32-ia32-msvc@13.5.6:
resolution: {integrity: sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@next/swc-win32-ia32-msvc@14.1.0:
resolution: {integrity: sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==}
engines: {node: '>= 10'}
@@ -2233,8 +2368,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-ia32-msvc@14.2.0-canary.49:
resolution: {integrity: sha512-Zcfe1+FuFtMCtG0L7F9yh0yRhmLM2gGAUHW41FYN+Rtbi/JFS8qhs/M7pOPkqhEWWKqo3at64q7z8KQh+21VsQ==}
/@next/swc-win32-ia32-msvc@14.2.0-canary.50:
resolution: {integrity: sha512-GmIQ0VdGEExzZSh00wCjAILfdqR4dzSFnnXvjAnNBehS8uadHhlLY7fpsVOtNh7byd5gxgbt+dFz7Y4GrrCRbA==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
@@ -2242,6 +2377,15 @@ packages:
dev: false
optional: true
/@next/swc-win32-x64-msvc@13.5.6:
resolution: {integrity: sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@next/swc-win32-x64-msvc@14.1.0:
resolution: {integrity: sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==}
engines: {node: '>= 10'}
@@ -2251,8 +2395,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-x64-msvc@14.2.0-canary.49:
resolution: {integrity: sha512-yeCjnmqMmI9aNbRk3DTrKvCuImUWXU+Kl0XC9KFo8iLpOztpCQrMA+pf5s3GRqv1HRzbRoHsj+1VCPXzTmZrLA==}
/@next/swc-win32-x64-msvc@14.2.0-canary.50:
resolution: {integrity: sha512-kNqwVNRCoujVBe2C4YdtwfrF8103nMmsV3B/IvMnxB3pAotvYLzUTboflT2Wx5AMFTNY1KGYY8GGFjotX5/GRQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -4230,7 +4374,7 @@ packages:
/@tanstack/query-core@5.28.6:
resolution: {integrity: sha512-hnhotV+DnQtvtR3jPvbQMPNMW4KEK0J4k7c609zJ8muiNknm+yoDyMHmxTWM5ZnlZpsz0zOxYFr+mzRJNHWJsA==}
/@tanstack/react-query-next-experimental@5.28.9(@tanstack/react-query@5.28.6)(next@14.2.0-canary.49)(react@18.2.0):
/@tanstack/react-query-next-experimental@5.28.9(@tanstack/react-query@5.28.6)(next@14.2.0-canary.50)(react@18.2.0):
resolution: {integrity: sha512-cihvqAme8nX6O5jeWtk19fnMsgXTX5puHwj6ya2Gf6FZIKhcFTrXQ9npH3ACcbinmVYPcQrShk/D3XAGKR/AUg==}
peerDependencies:
'@tanstack/react-query': ^5.28.9
@@ -4238,7 +4382,7 @@ packages:
react: ^18.0.0
dependencies:
'@tanstack/react-query': 5.28.6(react@18.2.0)
next: 14.2.0-canary.49(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0)
next: 14.2.0-canary.50(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
dev: false
@@ -5887,12 +6031,6 @@ packages:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
dev: false
/devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
dependencies:
dequal: 2.0.3
dev: false
/didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
@@ -6001,12 +6139,12 @@ packages:
/eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
/edge-csrf@1.0.9(next@14.2.0-canary.49):
/edge-csrf@1.0.9(next@14.2.0-canary.50):
resolution: {integrity: sha512-3F89YTh42UDdISr3s9AEcgJDLi4ysgjGfnybzF0LuZGaG2W31h1ZwgWwEQBLMj04lAklcP4XHZYi7vk9o8zcbg==}
peerDependencies:
next: ^13.0.0 || ^14.0.0
dependencies:
next: 14.2.0-canary.49(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0)
next: 14.2.0-canary.50(react-dom@18.2.0)(react@18.2.0)
dev: false
/editorconfig@1.0.4:
@@ -7147,16 +7285,27 @@ packages:
web-namespaces: 2.0.1
dev: false
/hast-util-has-property@2.0.1:
resolution: {integrity: sha512-X2+RwZIMTMKpXUzlotatPzWj8bspCymtXH3cfG3iQKV+wPF53Vgaqxi/eLqGck0wKq1kS9nvoB1wchbCPEL8sg==}
dev: false
/hast-util-heading-rank@2.1.1:
resolution: {integrity: sha512-iAuRp+ESgJoRFJbSyaqsfvJDY6zzmFoEnL1gtz1+U8gKtGGj1p0CVlysuUAUjq95qlZESHINLThwJzNGmgGZxA==}
dependencies:
'@types/hast': 2.3.10
dev: false
/hast-util-heading-rank@3.0.0:
resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==}
dependencies:
'@types/hast': 3.0.4
dev: false
/hast-util-is-element@3.0.0:
resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
/hast-util-is-element@2.1.3:
resolution: {integrity: sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==}
dependencies:
'@types/hast': 3.0.4
'@types/hast': 2.3.10
'@types/unist': 2.0.10
dev: false
/hast-util-parse-selector@3.1.1:
@@ -8655,7 +8804,7 @@ packages:
engines: {node: '>= 0.4.0'}
dev: false
/next-contentlayer@0.3.4(contentlayer@0.3.4)(esbuild@0.20.2)(next@14.2.0-canary.49)(react-dom@18.2.0)(react@18.2.0):
/next-contentlayer@0.3.4(contentlayer@0.3.4)(esbuild@0.20.2)(next@13.5.6)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-UtUCwgAl159KwfhNaOwyiI7Lg6sdioyKMeh+E7jxx0CJ29JuXGxBEYmCI6+72NxFGIFZKx8lvttbbQhbnYWYSw==}
peerDependencies:
contentlayer: 0.3.4
@@ -8666,7 +8815,7 @@ packages:
'@contentlayer/core': 0.3.4(esbuild@0.20.2)
'@contentlayer/utils': 0.3.4
contentlayer: 0.3.4(esbuild@0.20.2)
next: 14.2.0-canary.49(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0)
next: 13.5.6(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
transitivePeerDependencies:
@@ -8676,7 +8825,7 @@ packages:
- supports-color
dev: false
/next-sitemap@4.2.3(next@14.2.0-canary.49):
/next-sitemap@4.2.3(next@14.2.0-canary.50):
resolution: {integrity: sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==}
engines: {node: '>=14.18'}
hasBin: true
@@ -8687,7 +8836,7 @@ packages:
'@next/env': 13.5.6
fast-glob: 3.3.2
minimist: 1.2.8
next: 14.2.0-canary.49(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0)
next: 14.2.0-canary.50(react-dom@18.2.0)(react@18.2.0)
dev: false
/next-themes@0.3.0(react-dom@18.2.0)(react@18.2.0):
@@ -8699,6 +8848,46 @@ packages:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
/next@13.5.6(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==}
engines: {node: '>=16.14.0'}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
react: ^18.2.0
react-dom: ^18.2.0
sass: ^1.3.0
peerDependenciesMeta:
'@opentelemetry/api':
optional: true
sass:
optional: true
dependencies:
'@next/env': 13.5.6
'@opentelemetry/api': 1.8.0
'@swc/helpers': 0.5.2
busboy: 1.6.0
caniuse-lite: 1.0.30001600
postcss: 8.4.31
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
styled-jsx: 5.1.1(react@18.2.0)
watchpack: 2.4.0
optionalDependencies:
'@next/swc-darwin-arm64': 13.5.6
'@next/swc-darwin-x64': 13.5.6
'@next/swc-linux-arm64-gnu': 13.5.6
'@next/swc-linux-arm64-musl': 13.5.6
'@next/swc-linux-x64-gnu': 13.5.6
'@next/swc-linux-x64-musl': 13.5.6
'@next/swc-win32-arm64-msvc': 13.5.6
'@next/swc-win32-ia32-msvc': 13.5.6
'@next/swc-win32-x64-msvc': 13.5.6
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
dev: false
/next@14.1.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==}
engines: {node: '>=18.17.0'}
@@ -8738,8 +8927,8 @@ packages:
- babel-plugin-macros
dev: false
/next@14.2.0-canary.49(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-sfryWP84xmqUOYAilbiojczTpTGCRTMch3w+EVppzPj0DS6gOWv9vPUHp/6uMWWZ+YX+n3GkYhwRK80Q+FG+kg==}
/next@14.2.0-canary.50(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-7uNL5MrCx7YXJO1B/H3619HkLQhlXdAWIsgMHzetrz7ffE3isZoy6u5aXkkITfyKBfbvMbyhUcd2MH7HCdivfg==}
engines: {node: '>=18.17.0'}
hasBin: true
peerDependencies:
@@ -8756,8 +8945,7 @@ packages:
sass:
optional: true
dependencies:
'@next/env': 14.2.0-canary.49
'@opentelemetry/api': 1.8.0
'@next/env': 14.2.0-canary.50
'@swc/helpers': 0.5.5
busboy: 1.6.0
caniuse-lite: 1.0.30001600
@@ -8767,15 +8955,15 @@ packages:
react-dom: 18.2.0(react@18.2.0)
styled-jsx: 5.1.1(react@18.2.0)
optionalDependencies:
'@next/swc-darwin-arm64': 14.2.0-canary.49
'@next/swc-darwin-x64': 14.2.0-canary.49
'@next/swc-linux-arm64-gnu': 14.2.0-canary.49
'@next/swc-linux-arm64-musl': 14.2.0-canary.49
'@next/swc-linux-x64-gnu': 14.2.0-canary.49
'@next/swc-linux-x64-musl': 14.2.0-canary.49
'@next/swc-win32-arm64-msvc': 14.2.0-canary.49
'@next/swc-win32-ia32-msvc': 14.2.0-canary.49
'@next/swc-win32-x64-msvc': 14.2.0-canary.49
'@next/swc-darwin-arm64': 14.2.0-canary.50
'@next/swc-darwin-x64': 14.2.0-canary.50
'@next/swc-linux-arm64-gnu': 14.2.0-canary.50
'@next/swc-linux-arm64-musl': 14.2.0-canary.50
'@next/swc-linux-x64-gnu': 14.2.0-canary.50
'@next/swc-linux-x64-musl': 14.2.0-canary.50
'@next/swc-win32-arm64-msvc': 14.2.0-canary.50
'@next/swc-win32-ia32-msvc': 14.2.0-canary.50
'@next/swc-win32-x64-msvc': 14.2.0-canary.50
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
@@ -9920,15 +10108,16 @@ packages:
rc: 1.2.8
dev: false
/rehype-autolink-headings@7.1.0:
resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==}
/rehype-autolink-headings@6.1.1:
resolution: {integrity: sha512-NMYzZIsHM3sA14nC5rAFuUPIOfg+DFmf9EY1YMhaNlB7+3kK/ZlE6kqPfuxr1tsJ1XWkTrMtMoyHosU70d35mA==}
dependencies:
'@types/hast': 3.0.4
'@ungap/structured-clone': 1.2.0
hast-util-heading-rank: 3.0.0
hast-util-is-element: 3.0.0
unified: 11.0.4
unist-util-visit: 5.0.0
'@types/hast': 2.3.10
extend: 3.0.2
hast-util-has-property: 2.0.1
hast-util-heading-rank: 2.1.1
hast-util-is-element: 2.1.3
unified: 10.1.2
unist-util-visit: 4.1.2
dev: false
/rehype-slug@6.0.0:
@@ -11096,18 +11285,6 @@ packages:
vfile: 5.3.7
dev: false
/unified@11.0.4:
resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==}
dependencies:
'@types/unist': 3.0.2
bail: 2.0.2
devlop: 1.1.0
extend: 3.0.2
is-plain-obj: 4.1.0
trough: 2.2.0
vfile: 6.0.1
dev: false
/unist-util-generated@2.0.1:
resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==}
dev: false
@@ -11149,12 +11326,6 @@ packages:
'@types/unist': 2.0.10
dev: false
/unist-util-stringify-position@4.0.0:
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
dependencies:
'@types/unist': 3.0.2
dev: false
/unist-util-visit-parents@5.1.3:
resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==}
dependencies:
@@ -11313,13 +11484,6 @@ packages:
unist-util-stringify-position: 3.0.3
dev: false
/vfile-message@4.0.2:
resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==}
dependencies:
'@types/unist': 3.0.2
unist-util-stringify-position: 4.0.0
dev: false
/vfile@5.3.7:
resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==}
dependencies:
@@ -11329,14 +11493,6 @@ packages:
vfile-message: 3.1.4
dev: false
/vfile@6.0.1:
resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==}
dependencies:
'@types/unist': 3.0.2
unist-util-stringify-position: 4.0.0
vfile-message: 4.0.2
dev: false
/victory-vendor@36.9.2:
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
dependencies:
@@ -11360,6 +11516,14 @@ packages:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
/watchpack@2.4.0:
resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==}
engines: {node: '>=10.13.0'}
dependencies:
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
dev: false
/watchpack@2.4.1:
resolution: {integrity: sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==}
engines: {node: '>=10.13.0'}
@@ -11527,6 +11691,12 @@ packages:
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
dev: false
/wp-types@3.64.0:
resolution: {integrity: sha512-8rwHUQFxI18jezvObymV0eKEhnU2xaSci5ra6YG+dV6EDZLfNR2z3NZA82mFkOzZRWpl/Dlj+a4u8aqk08UPGQ==}
dependencies:
typescript: 5.4.3
dev: true
/wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}

View File

@@ -1,6 +1,7 @@
packages:
- apps/*
- packages/*
- tooling/*
- packages/features/*
- packages/cms/*
- tooling/*
- supabase

View File

@@ -37,6 +37,7 @@ const config = {
'**/.eslintrc.cjs',
'**/*.config.js',
'**/*.config.cjs',
'**/node_modules',
'.next',
'dist',
'pnpm-lock.yaml',

View File

@@ -19,7 +19,7 @@ const config = {
'^@supabase/supabase-js$',
'^@supabase/gotrue-js$',
'<THIRD_PARTY_MODULES>',
'^@kit/(.*)$',
'^@kit/(.*)$', // package imports
'^~/(.*)$', // app-specific imports
'^[./]', // relative imports
],

View File

@@ -6,7 +6,8 @@ export default {
content: [
'../../packages/**/*.tsx',
'../../apps/**/*.tsx',
'!**/node_modules',
'!../../packages/**/node_modules',
'!../../apps/**/node_modules',
],
theme: {
container: {