Replaced contentlayer with keystatic
This commit is contained in:
@@ -8,7 +8,10 @@ NEXT_PUBLIC_THEME_COLOR="#ffffff"
|
||||
NEXT_PUBLIC_THEME_COLOR_DARK="#0a0a0a"
|
||||
|
||||
# CMS
|
||||
CMS_CLIENT=contentlayer
|
||||
CMS_CLIENT=keystatic
|
||||
|
||||
# KEYSTATIC
|
||||
NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH=./content
|
||||
|
||||
# AUTH
|
||||
NEXT_PUBLIC_AUTH_PASSWORD=true
|
||||
|
||||
@@ -4,9 +4,10 @@ import { notFound } from 'next/navigation';
|
||||
|
||||
import { createCmsClient } from '@kit/cms';
|
||||
|
||||
import { Post } from '~/(marketing)/blog/_components/post';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { Post } from '../../blog/_components/post';
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
|
||||
@@ -21,6 +21,7 @@ export function PostPreview({
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const { title, image, publishedAt, description } = post;
|
||||
const height = imageHeight ?? DEFAULT_IMAGE_HEIGHT;
|
||||
|
||||
const slug = `/blog/${post.slug}`;
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PostHeader } from './post-header';
|
||||
|
||||
export const Post: React.FC<{
|
||||
post: Cms.ContentItem;
|
||||
content: string;
|
||||
content: unknown;
|
||||
}> = ({ post, content }) => {
|
||||
return (
|
||||
<div>
|
||||
|
||||
8
apps/web/content/posts/my-post.mdoc
Normal file
8
apps/web/content/posts/my-post.mdoc
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: hello
|
||||
description: hello
|
||||
categories: ['blog']
|
||||
tags: []
|
||||
---
|
||||
|
||||
ddfdsfjsdnfjdks
|
||||
@@ -5,10 +5,12 @@
|
||||
export async function register() {
|
||||
// only run in nodejs runtime
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
const { registerInstrumentation } = await import('@kit/monitoring');
|
||||
const { registerMonitoringInstrumentation } = await import(
|
||||
'@kit/monitoring'
|
||||
);
|
||||
|
||||
// Register monitoring instrumentation based on the
|
||||
// MONITORING_INSTRUMENTATION_PROVIDER environment variable.
|
||||
return registerInstrumentation();
|
||||
return registerMonitoringInstrumentation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"@kit/tsconfig": "workspace:^",
|
||||
"@next/bundle-analyzer": "14.2.0-canary.54",
|
||||
"@types/mdx": "^2.0.12",
|
||||
"@types/node": "^20.12.3",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/react": "^18.2.75",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"autoprefixer": "^10.4.19",
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# 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_CLIENT=contentlayer
|
||||
```
|
||||
@@ -1,87 +0,0 @@
|
||||
---
|
||||
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
|
||||
repositories built by the MakerKit team. In this document, we will learn how
|
||||
to fetch and install the codebase.
|
||||
|
||||
### Requirements
|
||||
|
||||
To get started with the Next.js and Supabase SaaS template, we need to ensure
|
||||
you install the required software.
|
||||
|
||||
- Node.js
|
||||
- Git
|
||||
- Docker
|
||||
|
||||
### Getting Started with MakerKit
|
||||
|
||||
You have two choices for cloning the repository:
|
||||
|
||||
1. forking the original repository and cloning it from your fork
|
||||
2. cloning it manually from the original repository
|
||||
|
||||
#### Clone the repository
|
||||
|
||||
To get the codebase on your local machine using the original repository, clone the repository with the
|
||||
following command:
|
||||
|
||||
```
|
||||
git clone --depth=1 git@github.com:makerkit/next-supabase-saas-kit-lite.git my-saas
|
||||
```
|
||||
|
||||
The command above clones the repository in the folder `my-saas` which
|
||||
you can rename it with the name of your project.
|
||||
|
||||
If you forked the repository, point it to your fork instead of the original.
|
||||
|
||||
#### Initializing Git
|
||||
|
||||
Now, run the following commands for:
|
||||
|
||||
1. Moving into the folder
|
||||
2. Reinitialize your git repository
|
||||
|
||||
Personally I re-initialize the Git repository, but it's not required.
|
||||
|
||||
```
|
||||
cd my-saas
|
||||
rm -rf .git
|
||||
git init
|
||||
```
|
||||
|
||||
### Setting the Upstream repository, and fetching updates
|
||||
|
||||
Now, we can add the original Makerkit repository as "upstream" so we can fetch updates from the main repository:
|
||||
|
||||
```
|
||||
git remote add upstream git@github.com:makerkit/next-supabase-saas-kit-lite.git
|
||||
git add .
|
||||
git commit -a -m "Initial Commit"
|
||||
```
|
||||
|
||||
In this way, to fetch updates (after committing your files), simply run:
|
||||
|
||||
```
|
||||
git pull upstream main --allow-unrelated-histories
|
||||
```
|
||||
|
||||
You'll likely run into conflicts when running this command, so carefully choose the changes (sorry!).
|
||||
|
||||
### Installing the Node dependencies
|
||||
|
||||
Finally, we can install the NodeJS dependencies with `npm`:
|
||||
|
||||
```
|
||||
npm i
|
||||
```
|
||||
|
||||
While the application code is fully working, we now need to set up your Supabase
|
||||
project.
|
||||
|
||||
So let's jump on to the next step!
|
||||
@@ -1,87 +0,0 @@
|
||||
---
|
||||
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
|
||||
repositories built by the MakerKit team. In this document, we will learn how
|
||||
to fetch and install the codebase.
|
||||
|
||||
### Requirements
|
||||
|
||||
To get started with the Next.js and Supabase SaaS template, we need to ensure
|
||||
you install the required software.
|
||||
|
||||
- Node.js
|
||||
- Git
|
||||
- Docker
|
||||
|
||||
### Getting Started with MakerKit
|
||||
|
||||
You have two choices for cloning the repository:
|
||||
|
||||
1. forking the original repository and cloning it from your fork
|
||||
2. cloning it manually from the original repository
|
||||
|
||||
#### Clone the repository
|
||||
|
||||
To get the codebase on your local machine using the original repository, clone the repository with the
|
||||
following command:
|
||||
|
||||
```
|
||||
git clone --depth=1 git@github.com:makerkit/next-supabase-saas-kit-lite.git my-saas
|
||||
```
|
||||
|
||||
The command above clones the repository in the folder `my-saas` which
|
||||
you can rename it with the name of your project.
|
||||
|
||||
If you forked the repository, point it to your fork instead of the original.
|
||||
|
||||
#### Initializing Git
|
||||
|
||||
Now, run the following commands for:
|
||||
|
||||
1. Moving into the folder
|
||||
2. Reinitialize your git repository
|
||||
|
||||
Personally I re-initialize the Git repository, but it's not required.
|
||||
|
||||
```
|
||||
cd my-saas
|
||||
rm -rf .git
|
||||
git init
|
||||
```
|
||||
|
||||
### Setting the Upstream repository, and fetching updates
|
||||
|
||||
Now, we can add the original Makerkit repository as "upstream" so we can fetch updates from the main repository:
|
||||
|
||||
```
|
||||
git remote add upstream git@github.com:makerkit/next-supabase-saas-kit-lite.git
|
||||
git add .
|
||||
git commit -a -m "Initial Commit"
|
||||
```
|
||||
|
||||
In this way, to fetch updates (after committing your files), simply run:
|
||||
|
||||
```
|
||||
git pull upstream main --allow-unrelated-histories
|
||||
```
|
||||
|
||||
You'll likely run into conflicts when running this command, so carefully choose the changes (sorry!).
|
||||
|
||||
### Installing the Node dependencies
|
||||
|
||||
Finally, we can install the NodeJS dependencies with `npm`:
|
||||
|
||||
```
|
||||
npm i
|
||||
```
|
||||
|
||||
While the application code is fully working, we now need to set up your Supabase
|
||||
project.
|
||||
|
||||
So let's jump on to the next step!
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
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.
|
||||
|
||||
In this section, we learn how to install and run the SaaS kit on your local
|
||||
machine.
|
||||
|
||||
Buckle up and let's get started!
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
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.
|
||||
|
||||
If you open the global configuration at `src/configuration.ts`, you'll find
|
||||
the `auth` object:
|
||||
|
||||
```tsx title="configuration.ts"
|
||||
import type { Provider } from '@supabase/gotrue-js/src/lib/types';
|
||||
|
||||
auth: {
|
||||
requireEmailConfirmation: false,
|
||||
providers: {
|
||||
emailPassword: true,
|
||||
phoneNumber: false,
|
||||
emailLink: false,
|
||||
oAuth: ['google'] as Provider[],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, the `providers` object can be configured to only display the
|
||||
auth methods we want to use.
|
||||
|
||||
1. For example, by setting both `phoneNumber` and `emailLink` to `true`, the
|
||||
authentication pages will display the `Email Link` authentication
|
||||
and the `Phone Number` authentication forms.
|
||||
2. Instead, by setting `emailPassword` to `false`, we will remove the
|
||||
`email/password` form from the authentication and user profile pages.
|
||||
|
||||
## Requiring Email Verification
|
||||
|
||||
This setting needs to match what you have set up in Supabase. If you require email confirmation before your users can sign in, you will have to flip the following flag in your configuration:
|
||||
|
||||
```ts
|
||||
auth: {
|
||||
requireEmailConfirmation: false,
|
||||
}
|
||||
```
|
||||
|
||||
When the flag is set to `true`, the user will not be redirected to the onboarding flow, but will instead see a successful alert asking them to confirm their email. After confirmation, they will be able to sign in.
|
||||
|
||||
When the flag is set to `false`, the application will redirect them directly to the onboarding flow.
|
||||
|
||||
## Emails sent by Supabase
|
||||
|
||||
Supabase spins up an [InBucket](http://localhost:54324/) instance where all the emails are sent: this is where you can find emails related to password reset, sign-in links, and email verifications.
|
||||
|
||||
To access the InBucket instance, you can go to the following URL: [http://localhost:54324/](http://localhost:54324/). Save this URL, you will use it very often.
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
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.
|
||||
|
||||
## Authentication URLs
|
||||
|
||||
The first thing you need to do is to set the authentication URLs in the Supabase Dashboard. These URLs are used to redirect users to the correct page after they have logged in or signed up.
|
||||
|
||||
1. Go to the [Supabase Dashboard](https://app.supabase.io/).
|
||||
2. Click on the project you want to use.
|
||||
3. Go to the **Authentication** tab.
|
||||
4. Click on **URL Configuration**.
|
||||
5. Add your Site URL to the **Site URL** field. This is the URL of your MakerKit site (e.g. `https://my-site.com`).
|
||||
6. Add your Redirect URLs to the **Redirect URLs** field. You need to add at least two URLs: This is the URL of your MakerKit site with `/auth/callback` appended to it (e.g. `https://my-site.com/auth/callback`) and another for redirecting users to their password reset page (e.g. `https://my-site.com/settings/profile/password`).
|
||||
|
||||
## Custom SMTP (optional)
|
||||
|
||||
If you want to send emails from your own domain, you can configure your SMTP settings in the Supabase Dashboard.
|
||||
|
||||
This is optional, but recommended if you want to send emails from your own domain.
|
||||
|
||||
1. Go to the [Supabase Dashboard](https://app.supabase.io/).
|
||||
2. Click on the project you want to use.
|
||||
3. Go to the **Project Settings** tab.
|
||||
4. Click on **Auth**.
|
||||
5. Tweak the `SMTP Settings` settings to your liking according to your provider's documentation.
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
title: Authentication
|
||||
label: Authentication
|
||||
description: Learn everything about Authentication in Makerkit
|
||||
categories:
|
||||
- documentation
|
||||
---
|
||||
|
||||
MakerKit uses Supabase to manage authentication within your application.
|
||||
|
||||
By default, every kit comes with the following built-in authentication methods:
|
||||
- **Email/Password** - we added, by default, the traditional way of signing in
|
||||
- **Third Party Providers** - we also added by default Google Auth sign-in
|
||||
- **Email Links**
|
||||
- **Phone Number**
|
||||
|
||||
You're free to add (or remove) any of the methods supported by Supabase's
|
||||
Authentication: we will see how.
|
||||
|
||||
This documentation will help you with the following:
|
||||
- **Setup** - setting up your Supabase project
|
||||
- **SSR** - use SSR to persist your users' authentication, adding new
|
||||
providers
|
||||
- **Customization** - an overview of how MakerKit works so that you can adapt
|
||||
it to your own application's needs
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
title: Lorem Ipsum
|
||||
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:
|
||||
- blog
|
||||
---
|
||||
|
||||
## Fecerat avis invenio mentis
|
||||
|
||||
Lorem markdownum signis vidi quisquis saepe inani inter animam laceras nequiquam
|
||||
castos cumque dum poste pede, **soliti et** eras. Cornua utendum. Ne dignus
|
||||
opacae? Moles percussis, redimitus equi quercus haurit perque *aras*; humo!
|
||||
Digessit Famemque membris vestrum [sua adveniens](http://erit.net/luctus.php)
|
||||
deserta, et me cum cum dicuntur et ignes.
|
||||
|
||||
var xmp_duplex = boolean + alu_unmount_tween - newline;
|
||||
var matrix_http_plain = facebook;
|
||||
rom = risc_flops + market + 2 + reimage_c_mca;
|
||||
|
||||
## Parilesque duae meritis
|
||||
|
||||
Suum spes medio faciunt miserarum artisque. Honor amplectique crescunt saepius
|
||||
cavis esse saepe laetabile modo. Fontis relinquit titulum est victa mundi!
|
||||
|
||||
## Orbem dare
|
||||
|
||||
More cingo concipit cumque armenta. Secuta quare profundi damni erigimur effugit
|
||||
facta ipsa, videt videt. Conantem et **campos animos usquam** ut munus erat
|
||||
audito, e!
|
||||
|
||||
> Talia ponit corpora Philemon. Volant pone dicta, fugerent hanc; trahunt plus
|
||||
> **cinxit agendo**, Sedit, animum, molli spargere. Perterrita et bella Tiresia
|
||||
> tanta auctor, colatur **nigro** externa. Stamina nunc bis *longeque* cornua.
|
||||
|
||||
## Cantat sequentes et illi opertos Cycno
|
||||
|
||||
Per inque Pallade cuspide errabat. Est dolor excitus ultorem avertere, numero et
|
||||
Minos pater flamine, ictu tune Phylius.
|
||||
|
||||
Ditem est qui Scythicis erubuit suae, qui nunc tacito aequare auras. Suas erat
|
||||
cubitoque esse, volventem quae, fera tinxi villo ita. Addit hic sine at orbe ut
|
||||
sanesque intravit pudore nullisque Canens, ait aut. Parabat Pittheia Pan cibus
|
||||
et perque inquit grave suae coniunx cura matris undaeque et stagni et.
|
||||
@@ -1,271 +0,0 @@
|
||||
import { defineDocumentType, makeSource } from 'contentlayer/source-files';
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
||||
import rehypeSlug from 'rehype-slug';
|
||||
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
||||
|
||||
export const Post = defineDocumentType(() => ({
|
||||
name: 'Post',
|
||||
filePathPattern: `posts/*.mdx`,
|
||||
contentType: 'mdx',
|
||||
fields: {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'The title of the post',
|
||||
required: true,
|
||||
},
|
||||
date: {
|
||||
type: 'date',
|
||||
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',
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
image: {
|
||||
type: 'string',
|
||||
description: 'The path to the cover image',
|
||||
},
|
||||
description: {
|
||||
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: {
|
||||
readingTime: {
|
||||
type: 'number',
|
||||
resolve: (post) => calculateReadingTime(post.body.raw),
|
||||
},
|
||||
slug: {
|
||||
type: 'string',
|
||||
resolve: (post) => getSlug(post._raw.sourceFileName),
|
||||
},
|
||||
url: {
|
||||
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) => ({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
headline: doc.title,
|
||||
datePublished: doc.date,
|
||||
dateModified: doc.date,
|
||||
description: doc.description,
|
||||
image: [siteUrl, doc.image].join(''),
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
name: `Makerkit`,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export const DocumentationPage = defineDocumentType(() => ({
|
||||
name: 'DocumentationPage',
|
||||
filePathPattern: `docs/**/*.mdx`,
|
||||
contentType: 'mdx',
|
||||
fields: {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'The title of the post',
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: 'string',
|
||||
description: 'The label of the page in the sidebar',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
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: {
|
||||
readingTime: {
|
||||
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) => ({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'LearningResource',
|
||||
headline: doc.title,
|
||||
datePublished: doc.date,
|
||||
dateModified: doc.date,
|
||||
description: doc.description,
|
||||
image: [siteUrl, doc.image].join(''),
|
||||
url: [siteUrl, 'blog', doc._raw.flattenedPath].join('/'),
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
name: `Makerkit`,
|
||||
},
|
||||
}),
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
resolve: (doc) => {
|
||||
if (doc._id.startsWith('docs/index.md')) {
|
||||
return '/docs';
|
||||
}
|
||||
|
||||
return urlFromFilePath(doc);
|
||||
},
|
||||
},
|
||||
pathSegments: {
|
||||
type: 'json',
|
||||
resolve: (doc) => getPathSegments(doc).map(getMetaFromFolderName),
|
||||
},
|
||||
slug: {
|
||||
type: 'string',
|
||||
resolve: (doc) =>
|
||||
getPathSegments(doc)
|
||||
.map(getMetaFromFolderName)
|
||||
.map(({ pathName }) => pathName)
|
||||
.join('/'),
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
resolve: (doc) => {
|
||||
return (
|
||||
'/docs/' +
|
||||
getPathSegments(doc)
|
||||
.map(getMetaFromFolderName)
|
||||
.map(({ pathName }) => pathName)
|
||||
.join('/')
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export default makeSource({
|
||||
contentDirPath: './content',
|
||||
documentTypes: [Post, DocumentationPage],
|
||||
mdx: {
|
||||
remarkPlugins: [],
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
[
|
||||
rehypeAutolinkHeadings,
|
||||
{
|
||||
properties: {
|
||||
className: ['anchor'],
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
function calculateReadingTime(content) {
|
||||
const wordsPerMinute = 235;
|
||||
const numberOfWords = content.split(/\s/g).length;
|
||||
const minutes = numberOfWords / wordsPerMinute;
|
||||
|
||||
return Math.ceil(minutes);
|
||||
}
|
||||
|
||||
function getSlug(fileName) {
|
||||
return fileName.replace('.mdx', '');
|
||||
}
|
||||
|
||||
function urlFromFilePath(doc) {
|
||||
let urlPath = doc._raw.flattenedPath.replace(/^app\/?/, '/');
|
||||
|
||||
if (!urlPath.startsWith('/')) {
|
||||
urlPath = `/${urlPath}`;
|
||||
}
|
||||
|
||||
return urlPath;
|
||||
}
|
||||
|
||||
function getMetaFromFolderName(dirName) {
|
||||
const re = /^((\d+)-)?(.*)$/;
|
||||
const [, , orderStr, pathName] = dirName.match(re) ?? [];
|
||||
const order = orderStr ? parseInt(orderStr) : 0;
|
||||
|
||||
return { order, pathName };
|
||||
}
|
||||
|
||||
function getPathSegments(doc) {
|
||||
return (
|
||||
urlFromFilePath(doc)
|
||||
.split('/')
|
||||
// skip `/docs` prefix
|
||||
.slice(2)
|
||||
);
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
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, ...allDocumentationPages];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 matchesParentIds = options?.parentIds
|
||||
? options.parentIds.includes(item.parentId ?? '')
|
||||
: true;
|
||||
|
||||
return tagMatch && categoryMatch && matchesParentIds;
|
||||
})
|
||||
.map((post) => this.mapPost(post))
|
||||
.slice(startOffset, endOffset);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return Promise.resolve(post ? this.mapPost(post) : 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
|
||||
.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
|
||||
.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 {
|
||||
return {
|
||||
id: post.slug,
|
||||
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,
|
||||
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,
|
||||
})) ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Mdx } from './mdx-renderer';
|
||||
|
||||
export function MDXContentRenderer(props: { content: string }) {
|
||||
return <Mdx code={props.content} />;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { getMDXComponent } from 'next-contentlayer/hooks';
|
||||
|
||||
export function Mdx({ code }: { code: string }) {
|
||||
const Component = getMDXComponent(code);
|
||||
|
||||
return <Component />;
|
||||
}
|
||||
@@ -16,7 +16,8 @@
|
||||
"devDependencies": {
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*"
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@types/node": "^20.12.7"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
|
||||
@@ -5,7 +5,7 @@ export namespace Cms {
|
||||
title: string;
|
||||
url: string;
|
||||
description: string | undefined;
|
||||
content: string;
|
||||
content: unknown;
|
||||
author: string;
|
||||
publishedAt: Date;
|
||||
image: string | undefined;
|
||||
|
||||
@@ -1,3 +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';
|
||||
export type CmsType = 'wordpress' | 'keystatic';
|
||||
|
||||
@@ -4,16 +4,16 @@ export async function ContentRenderer({
|
||||
content,
|
||||
type = process.env.CMS_CLIENT as CmsType,
|
||||
}: {
|
||||
content: string;
|
||||
content: unknown;
|
||||
type?: CmsType;
|
||||
}) {
|
||||
switch (type) {
|
||||
case 'contentlayer': {
|
||||
const { MDXContentRenderer } = await import(
|
||||
'../../contentlayer/src/content-renderer'
|
||||
case 'keystatic': {
|
||||
const { KeystaticDocumentRenderer } = await import(
|
||||
'../../keystatic/src/content-renderer'
|
||||
);
|
||||
|
||||
return MDXContentRenderer({ content });
|
||||
return KeystaticDocumentRenderer({ content });
|
||||
}
|
||||
|
||||
case 'wordpress': {
|
||||
|
||||
@@ -16,25 +16,25 @@ export async function createCmsClient(
|
||||
|
||||
async function cmsClientFactory(type: CmsType) {
|
||||
switch (type) {
|
||||
case 'contentlayer':
|
||||
return getContentLayerClient();
|
||||
|
||||
case 'wordpress':
|
||||
return getWordpressClient();
|
||||
|
||||
case 'keystatic':
|
||||
return getKeystaticClient();
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown CMS type`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getContentLayerClient() {
|
||||
const { ContentlayerClient } = await import('../../contentlayer/src/client');
|
||||
|
||||
return new ContentlayerClient();
|
||||
}
|
||||
|
||||
async function getWordpressClient() {
|
||||
const { WordpressClient } = await import('../../wordpress/src/wp-client');
|
||||
|
||||
return new WordpressClient();
|
||||
}
|
||||
|
||||
async function getKeystaticClient() {
|
||||
const { KeystaticClient } = await import('../../keystatic/src/client');
|
||||
|
||||
return new KeystaticClient();
|
||||
}
|
||||
|
||||
9
packages/cms/keystatic/README.md
Normal file
9
packages/cms/keystatic/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# CMS/Keystatic - @kit/keystatic
|
||||
|
||||
Implementation of the CMS layer using the Keystatic library.
|
||||
|
||||
This implementation is used when the host app's environment variable is set as:
|
||||
|
||||
```
|
||||
CMS_CLIENT=keystatic
|
||||
```
|
||||
@@ -1,27 +1,27 @@
|
||||
{
|
||||
"name": "@kit/contentlayer",
|
||||
"name": "@kit/keystatic",
|
||||
"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"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
".": "./src/index.ts",
|
||||
"./admin": "./src/keystatic-admin.tsx",
|
||||
"./route-handler": "./src/keystatic-route-handler.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"contentlayer": "0.3.4",
|
||||
"next-contentlayer": "0.3.4",
|
||||
"rehype-autolink-headings": "^6.0.0",
|
||||
"rehype-slug": "^6.0.0"
|
||||
"@keystatic/core": "0.5.11",
|
||||
"@keystatic/next": "5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@kit/cms": "workspace:^",
|
||||
"@kit/ui": "workspace:^"
|
||||
"@kit/ui": "workspace:^",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/cms": "*",
|
||||
@@ -29,7 +29,8 @@
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "*",
|
||||
"mdx": "0.3.1"
|
||||
"@types/node": "^20.12.7",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
124
packages/cms/keystatic/src/client.ts
Normal file
124
packages/cms/keystatic/src/client.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { Entry, createReader } from '@keystatic/core/reader';
|
||||
|
||||
import { Cms, CmsClient } from '@kit/cms';
|
||||
|
||||
import config from './keystatic.config';
|
||||
|
||||
const reader = createReader('.', config);
|
||||
|
||||
type EntryProps = Entry<(typeof config)['collections']['posts']>;
|
||||
|
||||
export class KeystaticClient implements CmsClient {
|
||||
async getContentItems(options?: Cms.GetContentItemsOptions) {
|
||||
const docs = await reader.collections.posts.all();
|
||||
|
||||
const startOffset = options?.offset ?? 0;
|
||||
const endOffset = startOffset + (options?.limit ?? 10);
|
||||
|
||||
return Promise.all(
|
||||
docs
|
||||
.filter((item) => {
|
||||
const categoryMatch = options?.categories
|
||||
? options.categories.find((category) =>
|
||||
item.entry.categories.includes(category),
|
||||
)
|
||||
: true;
|
||||
|
||||
if (!categoryMatch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tagMatch = options?.tags
|
||||
? options.tags.find((tag) => item.entry.tags.includes(tag))
|
||||
: true;
|
||||
|
||||
if (!tagMatch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.slice(startOffset, endOffset)
|
||||
.map(async (item) => {
|
||||
const children = docs.filter(
|
||||
(item) => item.entry.parent === item.slug,
|
||||
);
|
||||
|
||||
console.log(item);
|
||||
|
||||
return this.mapPost(item, children);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getContentItemById(id: string) {
|
||||
const doc = await reader.collections.posts.read(id);
|
||||
|
||||
if (!doc) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return this.mapPost({ entry: doc, slug: id }, []);
|
||||
}
|
||||
|
||||
async getCategories() {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
async getTags() {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
async getTagBySlug() {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
async getCategoryBySlug() {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
private async mapPost<
|
||||
Type extends {
|
||||
entry: EntryProps;
|
||||
slug: string;
|
||||
},
|
||||
>(item: Type, children: Type[] = []): Promise<Cms.ContentItem> {
|
||||
const publishedAt = item.entry.publishedAt
|
||||
? new Date(item.entry.publishedAt)
|
||||
: new Date();
|
||||
|
||||
const content = await item.entry.content();
|
||||
|
||||
return {
|
||||
id: item.slug,
|
||||
title: item.entry.title,
|
||||
url: item.slug,
|
||||
slug: item.slug,
|
||||
description: item.entry.description,
|
||||
publishedAt,
|
||||
author: item.entry.author,
|
||||
content,
|
||||
image: item.entry.image ?? undefined,
|
||||
categories:
|
||||
item.entry.categories.map((item) => {
|
||||
return {
|
||||
id: item,
|
||||
name: item,
|
||||
slug: item,
|
||||
};
|
||||
}) ?? [],
|
||||
tags: item.entry.tags.map((item) => {
|
||||
return {
|
||||
id: item,
|
||||
name: item,
|
||||
slug: item,
|
||||
};
|
||||
}),
|
||||
parentId: item.entry.parent ?? undefined,
|
||||
order: item.entry.order ?? 1,
|
||||
children: await Promise.all(
|
||||
children.map(async (child) => this.mapPost(child, [])),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
6
packages/cms/keystatic/src/content-renderer.tsx
Normal file
6
packages/cms/keystatic/src/content-renderer.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { DocumentElement } from '@keystatic/core';
|
||||
import { DocumentRenderer } from '@keystatic/core/renderer';
|
||||
|
||||
export function KeystaticDocumentRenderer(props: { content: unknown }) {
|
||||
return <DocumentRenderer document={props.content as DocumentElement[]} />;
|
||||
}
|
||||
7
packages/cms/keystatic/src/keystatic-admin.tsx
Normal file
7
packages/cms/keystatic/src/keystatic-admin.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { makePage } from '@keystatic/next/ui/app';
|
||||
|
||||
import config from './keystatic.config';
|
||||
|
||||
export default makePage(config);
|
||||
7
packages/cms/keystatic/src/keystatic-route-handler.tsx
Normal file
7
packages/cms/keystatic/src/keystatic-route-handler.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { makeRouteHandler } from '@keystatic/next/route-handler';
|
||||
|
||||
import keystaticConfig from './keystatic.config';
|
||||
|
||||
export const { POST, GET } = makeRouteHandler({
|
||||
config: keystaticConfig,
|
||||
});
|
||||
57
packages/cms/keystatic/src/keystatic.config.ts
Normal file
57
packages/cms/keystatic/src/keystatic.config.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { collection, config, fields } from '@keystatic/core';
|
||||
import { z } from 'zod';
|
||||
|
||||
const path = z.string().parse(process.env.NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH);
|
||||
|
||||
export default createKeyStaticConfig(path);
|
||||
|
||||
function createKeyStaticConfig(path: string) {
|
||||
return config({
|
||||
storage: {
|
||||
kind: 'local',
|
||||
},
|
||||
collections: {
|
||||
posts: collection({
|
||||
label: 'Posts',
|
||||
slugField: 'title',
|
||||
path: `${path}/posts/*`,
|
||||
format: { contentField: 'content' },
|
||||
schema: {
|
||||
title: fields.slug({ name: { label: 'Title' } }),
|
||||
image: fields.image({
|
||||
label: 'Image',
|
||||
directory: 'public/site/images',
|
||||
publicPath: '/site/images',
|
||||
}),
|
||||
categories: fields.array(fields.text({ label: 'Category' })),
|
||||
tags: fields.array(fields.text({ label: 'Tag' })),
|
||||
description: fields.text({ label: 'Description' }),
|
||||
publishedAt: fields.date({ label: 'Published At' }),
|
||||
author: fields.text({ label: 'Author' }),
|
||||
parent: fields.relationship({
|
||||
label: 'Parent',
|
||||
collection: 'posts',
|
||||
}),
|
||||
order: fields.number({ label: 'Order' }),
|
||||
content: fields.document({
|
||||
label: 'Content',
|
||||
formatting: true,
|
||||
dividers: true,
|
||||
links: true,
|
||||
images: {
|
||||
directory: 'public/site/images',
|
||||
publicPath: '/site/images',
|
||||
schema: {
|
||||
title: fields.text({
|
||||
label: 'Caption',
|
||||
description:
|
||||
'The text to display under the image in a caption.',
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "*",
|
||||
"@types/node": "^20.12.7",
|
||||
"wp-types": "^3.64.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export function WordpressContentRenderer(props: { content: string }) {
|
||||
return <div dangerouslySetInnerHTML={{ __html: props.content }} />;
|
||||
export function WordpressContentRenderer(props: { content: unknown }) {
|
||||
return <div dangerouslySetInnerHTML={{ __html: props.content as string }} />;
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ const DEFAULT_INSTRUMENTATION_PROVIDER = process.env
|
||||
.MONITORING_INSTRUMENTATION_PROVIDER as InstrumentationProvider | undefined;
|
||||
|
||||
/**
|
||||
* @name registerInstrumentation
|
||||
* @name registerMonitoringInstrumentation
|
||||
* @description Register monitoring instrumentation based on the MONITORING_INSTRUMENTATION_PROVIDER environment variable.
|
||||
*
|
||||
* Please set the MONITORING_INSTRUMENTATION_PROVIDER environment variable to register the monitoring instrumentation provider.
|
||||
*/
|
||||
export async function registerInstrumentation() {
|
||||
export async function registerMonitoringInstrumentation() {
|
||||
if (!DEFAULT_INSTRUMENTATION_PROVIDER) {
|
||||
console.info(`No instrumentation provider specified. Skipping...`);
|
||||
|
||||
|
||||
4529
pnpm-lock.yaml
generated
4529
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user