Upgrade librabries and rebranding kontent.ai (#45260)
Co-authored-by: JJ Kasper <jj@jjsweb.site>
|
@ -1,3 +1,3 @@
|
|||
KONTENT_PROJECT_ID=
|
||||
KONTENT_PREVIEW_SECRET=
|
||||
KONTENT_PROJECT_ID=
|
||||
KONTENT_PREVIEW_SECRET=
|
||||
KONTENT_PREVIEW_API_KEY=
|
|
@ -1,16 +1,16 @@
|
|||
# A statically generated blog example using Next.js and Kontent
|
||||
# A statically generated blog example using Next.js and Kontent.ai
|
||||
|
||||
This example showcases Next.js's [Static Generation](https://nextjs.org/docs/basic-features/pages) feature using [Kentico Kontent](https://kontent.ai) as the data source.
|
||||
This example showcases Next.js's [Static Generation](https://nextjs.org/docs/basic-features/pages) feature using [Kontent.ai](https://kontent.ai) as the data source.
|
||||
|
||||
## Demo
|
||||
|
||||
[https://next-blog-kontent.vercel.app/](https://next-blog-kontent.vercel.app/)
|
||||
[https://next-blog-kontent-ai.vercel.app/](https://next-blog-kontent-ai.vercel.app/)
|
||||
|
||||
## Deploy your own
|
||||
|
||||
Once you have access to [the environment variables you'll need](#step-3-set-up-environment-variables), deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):
|
||||
|
||||
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/cms-kontent&project-name=cms-kontent&repository-name=cms-kontent&env=KONTENT_PROJECT_ID,KONTENT_PREVIEW_API_KEY,KONTENT_PREVIEW_SECRET&envDescription=Required%20to%20connect%20the%20app%20with%20Kontent&envLink=https://github.com/vercel/next.js/tree/canary/examples/cms-kontent%23step-3-set-up-environment-variables)
|
||||
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/cms-kontent-ai&project-name=cms-kontent-ai&repository-name=cms-kontent-ai&env=KONTENT_PROJECT_ID,KONTENT_PREVIEW_API_KEY,KONTENT_PREVIEW_SECRET&envDescription=Required%20to%20connect%20the%20app%20with%20Kontent.ai&envLink=https://github.com/vercel/next.js/tree/canary/examples/cms-kontent-ai)
|
||||
|
||||
### Related examples
|
||||
|
||||
|
@ -51,45 +51,43 @@ pnpm create next-app --example cms-kontent cms-kontent-app
|
|||
|
||||
## Configuration
|
||||
|
||||
### Step 1. Create an account on Kontent
|
||||
### **1. Create an account on Kontent.ai**
|
||||
|
||||
First, [create an account on Kontent.ai](https://app.kontent.ai/sign-up?utm_source=nextjs_docs_example&utm_medium=devrel&utm_campaign=extended_trial).
|
||||
|
||||
> The link above will provide you with the 90-days trial. Once you finish the trial, or even during the trial period, you could switch to the [**developer plan**](https://kontent.ai/developer-plan) which is **free of charge** and offers all the features you'll need to test out the example capabilities.
|
||||
> The link above will provide you with the 30-days trial. Once you finish the trial, or even during the trial period, you can switch to the [**developer plan**](https://kontent.ai/developer-plan) which is **free of charge** and offers all the features you'll need to test out the example capabilities.
|
||||
|
||||
After signing up, [create an empty project](https://docs.kontent.ai/tutorials/set-up-kontent/projects/manage-projects#a-creating-projects).
|
||||
|
||||
### Step 2. Create the content models and fill them with data
|
||||
### **2. Create the content models and fill them with data**
|
||||
|
||||
The [content model](https://docs.kontent.ai/tutorials/set-up-kontent/content-modeling/what-is-content-modeling) defines the data structures of your application/websites. The structures are flexible and you can tailor them to your needs.
|
||||
|
||||
For this example you need to create a content model that defines an `author` and a `post` content type. **You can import these automatically or by doing it manually** to familiarize yourself with the Kontent user interface.
|
||||
For this example, you need to create a content model that defines an `author` and a `post` content type. **You can import these automatically or by doing it manually** to familiarize yourself with the Kontent.ai user interface.
|
||||
|
||||
To import the content models with their data follow the next steps:
|
||||
|
||||
1. Enter [Kontent application](https://app.kontent.ai)
|
||||
1. Go to "Project Settings", select API keys
|
||||
1. Enter [Kontent.ai application](https://app.kontent.ai)
|
||||
1. Go to "Project Settings" and select API keys
|
||||
1. Activate Management API
|
||||
1. Copy `Project ID` and `Management API` key
|
||||
1. Install [Kontent Backup Manager](https://github.com/Kentico/kontent-backup-manager-js) and import data to newly created project from kontent-backup.zip file (place appropriate values for apiKey and projectId arguments):
|
||||
1. Install [Kontent.ai Backup Manager](https://github.com/kontent-ai/backup-manager) and import data to the newly created project from kontent-ai-backup.zip file (don't forget to place appropriate values for apiKey and projectId arguments):
|
||||
|
||||
```sh
|
||||
npm i -g @kentico/kontent-backup-manager
|
||||
kbm --action=restore --apiKey=<Management API key> --projectId=<Project ID> --zipFilename=kontent-backup
|
||||
npm i -g @kontent-ai/backup-manager
|
||||
kbm --action=restore --apiKey=<Management API key> --projectId=<Project ID> --zipFilename=kontent-ai-backup
|
||||
```
|
||||
|
||||
> **💡 Alternatively, you can use the [Template Manager UI](https://kentico.github.io/kontent-template-manager/import) for importing the content.**
|
||||
1. Go to your Kontent.ai project and publish all the imported items.
|
||||
> Note: You can deactivate the Management API key, as it is not necessary anymore.
|
||||
|
||||
1. Go to your Kontent project and publish all the imported items.
|
||||
> You could deactivate Management API key, it is not necessary any more.
|
||||
#### **2.1. (Optional) Create the content models manually**
|
||||
|
||||
### Step 2.1. Optionally create the content models manually
|
||||
|
||||
You can safely ignore this step if you already imported the content models in Step 2.
|
||||
You can ignore this step if you already imported the content models in Step 2.
|
||||
|
||||
#### Create an `Author` content type
|
||||
|
||||
From your Kontent project, go to **Content models** and add a new `Content type`:
|
||||
In your Kontent.ai project, go to the **Content models** and add a new `Content type`:
|
||||
|
||||
> you don't have to modify the element configuration unless specified
|
||||
|
||||
|
@ -98,7 +96,7 @@ From your Kontent project, go to **Content models** and add a new `Content type`
|
|||
- `Name` - **Text** element
|
||||
- `Picture` - **Asset** element - configure to allow to select `At most 1` asset and `Limit file types` only to `Adjustable images`
|
||||
|
||||
Save the content type and continue.
|
||||
Save the content type.
|
||||
|
||||
The content type should look like this:
|
||||
|
||||
|
@ -106,7 +104,7 @@ The content type should look like this:
|
|||
|
||||
#### Create a `Post` content type
|
||||
|
||||
From your Kontent project, go to **Content models** and add a new content type:
|
||||
In your Kontent.ai project, go to **Content models** and add a new content type:
|
||||
|
||||
> you don't have to modify the element configuration unless specified
|
||||
|
||||
|
@ -119,7 +117,7 @@ From your Kontent project, go to **Content models** and add a new content type:
|
|||
- `Cover Image` - **Asset Text** element - configure to allow to select `At most 1` asset and `Limit file types` only to `Adjustable images` - `Content` - `Slug` - **URL slug** element - auto-generated from `Title` element
|
||||
- `Author` - **Linked items** element - configure to accept `Exactly 1` item of type `Author`
|
||||
|
||||
Save the content type and continue.
|
||||
Save the content type.
|
||||
|
||||
The content type should look like this:
|
||||
|
||||
|
@ -135,16 +133,16 @@ Go to `Content & Assets` section in your project and click `Create new` on the `
|
|||
|
||||
Next, create another item based on **Post** content type:
|
||||
|
||||
- It's recommend to create at least **2 post items**.
|
||||
- It's recommended to create at least **2 post items**.
|
||||
- Use dummy data for the text.
|
||||
- For images, you can download them from [Unsplash](https://unsplash.com/).
|
||||
- Pick the **author** you created earlier.
|
||||
|
||||
**Important:** For each item, you need to click on **Publish**. If not, the entry will be in draft workflow step.
|
||||
**Important:** For each item, you need to click on **Publish**. If not, the entry will be in the draft workflow step.
|
||||
|
||||
![Published post item overview](./docs/publish-post-overview.png)
|
||||
|
||||
### Step 3. Set up environment variables
|
||||
### **3. Set up environment variables**
|
||||
|
||||
Copy the `.env.local.example` file in this directory to `.env.local` (which will be ignored by Git):
|
||||
|
||||
|
@ -158,7 +156,7 @@ Then set each variable on `.env.local` using the keys `Project settings` > `API
|
|||
- `KONTENT_PREVIEW_API_KEY` - One of the Preview API keys in `Project settings` > `API keys`.
|
||||
- `KONTENT_PREVIEW_SECRET` - Can be any random string (but avoid spaces), like `MY_SECRET` - this is used for [Preview Mode](https://nextjs.org/docs/advanced-features/preview-mode).
|
||||
|
||||
### Step 4. Run Next.js in development mode
|
||||
### **4. Run Next.js in development mode**
|
||||
|
||||
```bash
|
||||
npm install
|
||||
|
@ -172,9 +170,9 @@ yarn dev
|
|||
|
||||
Your blog should be up and running on [http://localhost:3000](http://localhost:3000)! If it doesn't work, post on [GitHub discussions](https://github.com/vercel/next.js/discussions).
|
||||
|
||||
### Step 5. Try preview mode
|
||||
### **5. Try preview mode**
|
||||
|
||||
In your Kontent project, go to **Project Settings > Preview URLs** and set a new preview URL for the `Post` content type to:
|
||||
In your Kontent.ai project, go to **Project Settings > Preview URLs** and set a new preview URL for the `Post` content type to:
|
||||
|
||||
```plain
|
||||
http://localhost:3000/api/preview?secret=<KONTENT_PREVIEW_SECRET>&slug={URLslug}
|
||||
|
@ -188,21 +186,21 @@ Once saved, go to one of the posts you've created and:
|
|||
|
||||
- Create a new version of the post
|
||||
- **Update the title**. For example, you can add `[Draft]` in front of the title.
|
||||
> Mind the title also regenerates the URL slug, if you want to change any other field that does not influence URL slug, feel free to do so.
|
||||
- **Do not** publish it. By doing this, the post will be in draft workflow step.
|
||||
> Mind the title also regenerates the URL slug, if you want to change any other field that does not influence the URL slug, feel free to do so.
|
||||
- **Do not** publish it. By not publishing it, the post will be in the draft workflow step.
|
||||
- On the menu, you will see the **Preview** button. Click on it!
|
||||
|
||||
![Post preview button](./docs/post-preview-button.png).
|
||||
|
||||
You will now be able to see the updated title. To exit preview mode, you can click on **Click here to exit preview mode** at the top of the page.
|
||||
|
||||
### Step 6. Deploy on Vercel
|
||||
### **6. Deploy on Vercel**
|
||||
|
||||
You can deploy this app to the cloud with [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
|
||||
|
||||
#### Deploy Your Local Project
|
||||
|
||||
To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [import to Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example).
|
||||
To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [import it to Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example).
|
||||
|
||||
**Important**: When you import your project on Vercel, make sure to click on **Environment Variables** and set them to match your `.env.local` file.
|
||||
|
|
@ -2,7 +2,9 @@ import Container from './container'
|
|||
import cn from 'classnames'
|
||||
import { EXAMPLE_PATH } from '../lib/constants'
|
||||
|
||||
export default function Alert({ preview }) {
|
||||
type AlertProps = { preview: boolean }
|
||||
|
||||
export default function Alert({ preview }: AlertProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn('border-b', {
|
|
@ -1,6 +1,11 @@
|
|||
import Image from '../components/image'
|
||||
import Image from './image'
|
||||
|
||||
export default function Avatar({ name, picture }) {
|
||||
type AvatarProps = {
|
||||
name: string
|
||||
picture: string
|
||||
}
|
||||
|
||||
export default function Avatar({ name, picture }: AvatarProps) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="w-12 h-12 relative mr-4">
|
7
examples/cms-kontent-ai/components/container.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
type ContainerProps = {
|
||||
children: JSX.Element[] | JSX.Element
|
||||
}
|
||||
|
||||
export default function Container({ children }: ContainerProps) {
|
||||
return <div className="container mx-auto px-5">{children}</div>
|
||||
}
|
|
@ -1,8 +1,14 @@
|
|||
import cn from 'classnames'
|
||||
import Image from '../components/image'
|
||||
import Image from './image'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function CoverImage({ title, src, slug }) {
|
||||
type CoverImageProps = {
|
||||
title: string
|
||||
src: string
|
||||
slug?: string
|
||||
}
|
||||
|
||||
export default function CoverImage({ title, src, slug }: CoverImageProps) {
|
||||
const image = (
|
||||
<Image
|
||||
width={2000}
|
12
examples/cms-kontent-ai/components/date-formatter.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { parseISO, format } from 'date-fns'
|
||||
|
||||
type DateFormatterProps = { dateString: string | null }
|
||||
|
||||
export default function DateFormatter({ dateString }: DateFormatterProps) {
|
||||
if (dateString === null) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const date = parseISO(dateString)
|
||||
return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>
|
||||
}
|
|
@ -3,6 +3,18 @@ import DateFormatter from '../components/date-formatter'
|
|||
import CoverImage from '../components/cover-image'
|
||||
import Link from 'next/link'
|
||||
|
||||
type HeroPostProps = {
|
||||
title: string
|
||||
coverImage: string
|
||||
date: string | null
|
||||
excerpt: string
|
||||
author: {
|
||||
name: string
|
||||
picture: string
|
||||
}
|
||||
slug: string
|
||||
}
|
||||
|
||||
export default function HeroPost({
|
||||
title,
|
||||
coverImage,
|
||||
|
@ -10,7 +22,7 @@ export default function HeroPost({
|
|||
excerpt,
|
||||
author,
|
||||
slug,
|
||||
}) {
|
||||
}: HeroPostProps) {
|
||||
return (
|
||||
<section>
|
||||
<div className="mb-8 md:mb-16">
|
43
examples/cms-kontent-ai/components/image.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import NextImage, { ImageLoaderProps } from 'next/image'
|
||||
import { transformImageUrl } from '@kontent-ai/delivery-sdk'
|
||||
|
||||
const srcIsKontentAsset = (src: string) => {
|
||||
try {
|
||||
const { hostname } = new URL(src)
|
||||
return hostname.endsWith('.kc-usercontent.com')
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const kontentImageLoader = ({
|
||||
src,
|
||||
width,
|
||||
quality = 75,
|
||||
}: ImageLoaderProps): string => {
|
||||
return transformImageUrl(src)
|
||||
.withWidth(width)
|
||||
.withQuality(quality)
|
||||
.withCompression('lossless')
|
||||
.withAutomaticFormat()
|
||||
.getUrl()
|
||||
}
|
||||
|
||||
const getLoader = (src: string) => {
|
||||
return srcIsKontentAsset(src) ? kontentImageLoader : undefined
|
||||
}
|
||||
|
||||
type ImageType = {
|
||||
width?: number
|
||||
height?: number
|
||||
src: string
|
||||
layout?: string
|
||||
className: string
|
||||
alt: string
|
||||
}
|
||||
|
||||
export default function Image(props: ImageType) {
|
||||
const loader = getLoader(props.src)
|
||||
|
||||
return <NextImage {...props} loader={loader} />
|
||||
}
|
21
examples/cms-kontent-ai/components/layout.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import Alert from './alert'
|
||||
import Footer from './footer'
|
||||
import Meta from './meta'
|
||||
|
||||
type LayoutProps = {
|
||||
preview: boolean
|
||||
children: JSX.Element | JSX.Element[]
|
||||
}
|
||||
|
||||
export default function Layout({ preview, children }: LayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<Meta />
|
||||
<div className="min-h-screen">
|
||||
<Alert preview={preview} />
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
19
examples/cms-kontent-ai/components/more-stories.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Post } from '@/viewmodels/post'
|
||||
import PostPreview from './post-preview'
|
||||
|
||||
type MoreStoriesProps = { posts: Array<Post> }
|
||||
|
||||
export default function MoreStories({ posts }: MoreStoriesProps) {
|
||||
return (
|
||||
<section>
|
||||
<h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight">
|
||||
More Stories
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32">
|
||||
{posts.map((post) => (
|
||||
<PostPreview key={post.slug} post={post} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
import markdownStyles from './markdown-styles.module.css'
|
||||
|
||||
export default function PostBody({ content }) {
|
||||
type PostBodyProps = {
|
||||
content: string
|
||||
}
|
||||
|
||||
export default function PostBody({ content }: PostBodyProps) {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div
|
|
@ -1,12 +1,25 @@
|
|||
import Avatar from '../components/avatar'
|
||||
import DateFormatter from '../components/date-formatter'
|
||||
import CoverImage from '../components/cover-image'
|
||||
import PostTitle from '../components/post-title'
|
||||
import Avatar from './avatar'
|
||||
import DateFormatter from './date-formatter'
|
||||
import CoverImage from './cover-image'
|
||||
import PostTitle from './post-title'
|
||||
import { Author } from '@/viewmodels/author'
|
||||
|
||||
export default function PostHeader({ title, coverImage, date, author }) {
|
||||
type PostHeaderType = {
|
||||
title: string
|
||||
coverImage: string
|
||||
date: string | null
|
||||
author: Author
|
||||
}
|
||||
|
||||
export default function PostHeader({
|
||||
title,
|
||||
coverImage,
|
||||
date,
|
||||
author,
|
||||
}: PostHeaderType) {
|
||||
return (
|
||||
<>
|
||||
<PostTitle>{title}</PostTitle>
|
||||
<PostTitle title={title} />
|
||||
<div className="hidden md:block md:mb-12">
|
||||
<Avatar name={author.name} picture={author.picture} />
|
||||
</div>
|
29
examples/cms-kontent-ai/components/post-preview.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import Avatar from './avatar'
|
||||
import DateFormatter from './date-formatter'
|
||||
import CoverImage from './cover-image'
|
||||
import Link from 'next/link'
|
||||
import { Post } from '@/viewmodels/post'
|
||||
|
||||
type PostPreviewProps = {
|
||||
post: Post
|
||||
}
|
||||
|
||||
export default function PostPreview({ post }: PostPreviewProps) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-5">
|
||||
<CoverImage slug={post.slug} title={post.title} src={post.coverImage} />
|
||||
</div>
|
||||
<h3 className="text-3xl mb-3 leading-snug">
|
||||
<Link href={`/posts/${post.slug}`} className="hover:underline">
|
||||
{post.title}
|
||||
</Link>
|
||||
</h3>
|
||||
<div className="text-lg mb-4">
|
||||
<DateFormatter dateString={post.date} />
|
||||
</div>
|
||||
<p className="text-lg leading-relaxed mb-4">{post.excerpt}</p>
|
||||
<Avatar name={post.author.name} picture={post.author.picture} />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,7 +1,11 @@
|
|||
export default function PostTitle({ children }) {
|
||||
export type PostTitleProps = {
|
||||
title: string
|
||||
}
|
||||
|
||||
export default function PostTitle({ title }: PostTitleProps) {
|
||||
return (
|
||||
<h1 className="text-6xl md:text-7xl lg:text-8xl font-bold tracking-tighter leading-tight md:leading-none mb-12 text-center md:text-left">
|
||||
{children}
|
||||
{title}
|
||||
</h1>
|
||||
)
|
||||
}
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 191 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 193 KiB |
|
@ -1,10 +1,13 @@
|
|||
import { DeliveryClient } from '@kentico/kontent-delivery'
|
||||
import { Author, contentTypes, Post } from '@/models'
|
||||
import { Author as ViewModelAuthor } from '@/viewmodels/author'
|
||||
import { Post as ViewModelPost } from '@/viewmodels/post'
|
||||
import { DeliveryClient } from '@kontent-ai/delivery-sdk'
|
||||
import pkg from '../package.json'
|
||||
|
||||
const sourceTrackingHeaderName = 'X-KC-SOURCE'
|
||||
|
||||
const client = new DeliveryClient({
|
||||
projectId: process.env.KONTENT_PROJECT_ID,
|
||||
projectId: process.env.KONTENT_PROJECT_ID ?? '',
|
||||
previewApiKey: process.env.KONTENT_PREVIEW_API_KEY,
|
||||
globalHeaders: (_queryConfig) => [
|
||||
{
|
||||
|
@ -14,14 +17,14 @@ const client = new DeliveryClient({
|
|||
],
|
||||
})
|
||||
|
||||
function parseAuthor(author) {
|
||||
function parseAuthor(author: Author): ViewModelAuthor {
|
||||
return {
|
||||
name: author.elements.name.value,
|
||||
picture: author.elements.picture.value[0].url,
|
||||
}
|
||||
}
|
||||
|
||||
function parsePost(post) {
|
||||
function parsePost(post: Post): ViewModelPost {
|
||||
return {
|
||||
title: post.elements.title.value,
|
||||
slug: post.elements.slug.value,
|
||||
|
@ -35,8 +38,8 @@ function parsePost(post) {
|
|||
|
||||
export async function getAllPostSlugs() {
|
||||
return await client
|
||||
.items()
|
||||
.type('post')
|
||||
.items<Post>()
|
||||
.type(contentTypes.post.codename)
|
||||
.elementsParameter(['slug'])
|
||||
.toPromise()
|
||||
.then((response) =>
|
||||
|
@ -44,13 +47,13 @@ export async function getAllPostSlugs() {
|
|||
)
|
||||
}
|
||||
|
||||
export async function getMorePostsForSlug(slug, preview) {
|
||||
export async function getMorePostsForSlug(slug: string, preview: boolean) {
|
||||
return client
|
||||
.items()
|
||||
.items<Post>()
|
||||
.type(contentTypes.post.codename)
|
||||
.queryConfig({
|
||||
usePreviewMode: !!preview,
|
||||
})
|
||||
.type('post')
|
||||
.orderByDescending('elements.date')
|
||||
.notEqualsFilter('elements.slug', slug)
|
||||
.limitParameter(2)
|
||||
|
@ -58,25 +61,25 @@ export async function getMorePostsForSlug(slug, preview) {
|
|||
.then((response) => response.data.items.map((post) => parsePost(post)))
|
||||
}
|
||||
|
||||
export async function getPostBySlug(slug, preview) {
|
||||
export async function getPostBySlug(slug: string, preview: boolean) {
|
||||
return await client
|
||||
.items()
|
||||
.items<Post>()
|
||||
.type(contentTypes.post.codename)
|
||||
.queryConfig({
|
||||
usePreviewMode: !!preview,
|
||||
})
|
||||
.type('post')
|
||||
.equalsFilter('elements.slug', slug)
|
||||
.toPromise()
|
||||
.then((response) => parsePost(response.data.items[0]))
|
||||
}
|
||||
|
||||
export async function getAllPosts(preview) {
|
||||
export async function getAllPosts(preview: boolean) {
|
||||
return await client
|
||||
.items()
|
||||
.items<Post>()
|
||||
.type(contentTypes.post.codename)
|
||||
.queryConfig({
|
||||
usePreviewMode: !!preview,
|
||||
usePreviewMode: preview,
|
||||
})
|
||||
.type('post')
|
||||
.orderByDescending('elements.date')
|
||||
.toPromise()
|
||||
.then((response) => response.data.items.map((post) => parsePost(post)))
|
5
examples/cms-kontent-ai/lib/constants.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export const EXAMPLE_PATH = 'cms-kontent-ai'
|
||||
export const CMS_NAME = 'Kontent.ai'
|
||||
export const CMS_URL = 'https://kontent.ai'
|
||||
export const HOME_OG_IMAGE_URL =
|
||||
'https://og-image.vercel.app/Next.js%20Blog%20Example%20with%20**Kontent.ai**.png?theme=light&md=1&fontSize=75px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg&images=https%3A%2F%2Fraw.githubusercontent.com%2Fkontent-ai%2F.github%2Fmain%2Flogos%2Fkai-logo-symbol-color-rgb.png'
|
|
@ -1,7 +1,7 @@
|
|||
import { remark } from 'remark'
|
||||
import html from 'remark-html'
|
||||
|
||||
export default async function markdownToHtml(markdown) {
|
||||
export default async function markdownToHtml(markdown: string) {
|
||||
const result = await remark().use(html).process(markdown)
|
||||
return result.toString()
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export {}
|
25
examples/cms-kontent-ai/models/content-types/author.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { type IContentItem, type Elements } from '@kontent-ai/delivery-sdk'
|
||||
/**
|
||||
* Generated by '@kontent-ai/model-generator@5.9.0'
|
||||
*
|
||||
* Author
|
||||
* Id: c204b17f-d320-5755-b913-7b4caa8902b6
|
||||
* Codename: author
|
||||
*/
|
||||
export type Author = IContentItem<{
|
||||
/**
|
||||
* Name (text)
|
||||
* Required: false
|
||||
* Id: fab7d833-5672-5271-ac00-49e6fcf34754
|
||||
* Codename: name
|
||||
*/
|
||||
name: Elements.TextElement
|
||||
|
||||
/**
|
||||
* Picture (asset)
|
||||
* Required: false
|
||||
* Id: 78b67780-55e4-5b29-903c-de37d3aa263b
|
||||
* Codename: picture
|
||||
*/
|
||||
picture: Elements.AssetsElement
|
||||
}>
|
2
examples/cms-kontent-ai/models/content-types/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './author'
|
||||
export * from './post'
|
67
examples/cms-kontent-ai/models/content-types/post.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { type IContentItem, type Elements } from '@kontent-ai/delivery-sdk'
|
||||
import { type Author } from './author'
|
||||
|
||||
/**
|
||||
* Generated by '@kontent-ai/model-generator@5.9.0'
|
||||
*
|
||||
* Post
|
||||
* Id: b1efdda5-18a3-5445-8ca2-51c252a9ee2d
|
||||
* Codename: post
|
||||
*/
|
||||
export type Post = IContentItem<{
|
||||
/**
|
||||
* Author (modular_content)
|
||||
* Required: false
|
||||
* Id: aa68b9d2-e807-54be-ac82-543ff122b6b2
|
||||
* Codename: author
|
||||
*/
|
||||
author: Elements.LinkedItemsElement<Author>
|
||||
|
||||
/**
|
||||
* Content (rich_text)
|
||||
* Required: false
|
||||
* Id: f8b91d9f-0a7d-557e-9fe8-ca1203ae4a67
|
||||
* Codename: content
|
||||
*/
|
||||
content: Elements.RichTextElement
|
||||
|
||||
/**
|
||||
* Cover Image (asset)
|
||||
* Required: false
|
||||
* Id: 8f1bd2ae-b15a-5100-b8ee-b279a87862fb
|
||||
* Codename: cover_image
|
||||
*/
|
||||
cover_image: Elements.AssetsElement
|
||||
|
||||
/**
|
||||
* Date (date_time)
|
||||
* Required: false
|
||||
* Id: 1739ed56-ccd8-55a3-9cd3-67f09a8073db
|
||||
* Codename: date
|
||||
*/
|
||||
date: Elements.DateTimeElement
|
||||
|
||||
/**
|
||||
* Excerpt (text)
|
||||
* Required: false
|
||||
* Id: e1461965-9004-500e-9441-39a51aa3088b
|
||||
* Codename: excerpt
|
||||
*/
|
||||
excerpt: Elements.TextElement
|
||||
|
||||
/**
|
||||
* Slug (url_slug)
|
||||
* Required: false
|
||||
* Id: f12efb12-764a-512c-82f8-4423d978b62b
|
||||
* Codename: slug
|
||||
*/
|
||||
slug: Elements.UrlSlugElement
|
||||
|
||||
/**
|
||||
* Title (text)
|
||||
* Required: true
|
||||
* Id: 933469d0-fe2b-51fa-8f62-600f103aee8b
|
||||
* Codename: title
|
||||
*/
|
||||
title: Elements.TextElement
|
||||
}>
|
4
examples/cms-kontent-ai/models/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export * from './project'
|
||||
export * from './content-types'
|
||||
export * from './content-type-snippets'
|
||||
export * from './taxonomies'
|
28
examples/cms-kontent-ai/models/project/assetFolders.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Generated by '@kontent-ai/model-generator@5.9.0'
|
||||
*
|
||||
* Project name: Next.js Sample App
|
||||
* Environment: Production
|
||||
* Project Id: a7844231-064c-016c-1dad-38228cbc505d
|
||||
*/
|
||||
export const assetFolders = {
|
||||
/**
|
||||
* Authors
|
||||
*/
|
||||
authors: {
|
||||
id: 'eaf64b25-4cd1-5d71-ba2f-e02e6556213b',
|
||||
name: 'Authors',
|
||||
externalId: '82274911-5ab9-4881-853e-4f7a97b2caf2',
|
||||
folders: {},
|
||||
},
|
||||
|
||||
/**
|
||||
* Post images
|
||||
*/
|
||||
postImages: {
|
||||
id: '290176f9-bb83-5d6a-ad07-2843a1dd2208',
|
||||
name: 'Post images',
|
||||
externalId: '499af581-d817-49cc-a548-5b34ba7b2fc6',
|
||||
folders: {},
|
||||
},
|
||||
} as const
|
17
examples/cms-kontent-ai/models/project/collections.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Generated by '@kontent-ai/model-generator@5.9.0'
|
||||
*
|
||||
* Project name: Next.js Sample App
|
||||
* Environment: Production
|
||||
* Project Id: a7844231-064c-016c-1dad-38228cbc505d
|
||||
*/
|
||||
export const collections = {
|
||||
/**
|
||||
* Default
|
||||
*/
|
||||
default: {
|
||||
codename: 'default',
|
||||
id: '00000000-0000-0000-0000-000000000000',
|
||||
name: 'Default',
|
||||
},
|
||||
} as const
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Generated by '@kontent-ai/model-generator@5.9.0'
|
||||
*
|
||||
* Project name: Next.js Sample App
|
||||
* Environment: Production
|
||||
* Project Id: a7844231-064c-016c-1dad-38228cbc505d
|
||||
*/
|
||||
export const contentTypeSnippets = {} as const
|
147
examples/cms-kontent-ai/models/project/contentTypes.ts
Normal file
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* Generated by '@kontent-ai/model-generator@5.9.0'
|
||||
*
|
||||
* Project name: Next.js Sample App
|
||||
* Environment: Production
|
||||
* Project Id: a7844231-064c-016c-1dad-38228cbc505d
|
||||
*/
|
||||
export const contentTypes = {
|
||||
/**
|
||||
* Author
|
||||
*/
|
||||
author: {
|
||||
codename: 'author',
|
||||
id: 'c204b17f-d320-5755-b913-7b4caa8902b6',
|
||||
externalId: 'ba75f751-d649-4fad-a643-bd58c660bb86',
|
||||
name: 'Author',
|
||||
elements: {
|
||||
/**
|
||||
* Name (text)
|
||||
*/
|
||||
name: {
|
||||
codename: 'name',
|
||||
id: 'fab7d833-5672-5271-ac00-49e6fcf34754',
|
||||
externalId: '27cd83d2-9bb6-4c3c-9e0e-08abd3c604dc',
|
||||
name: 'Name',
|
||||
required: false,
|
||||
type: 'text',
|
||||
snippetCodename: undefined,
|
||||
},
|
||||
|
||||
/**
|
||||
* Picture (asset)
|
||||
*/
|
||||
picture: {
|
||||
codename: 'picture',
|
||||
id: '78b67780-55e4-5b29-903c-de37d3aa263b',
|
||||
externalId: '7a94a17f-fbe6-4415-a0fd-a99d150d74e3',
|
||||
name: 'Picture',
|
||||
required: false,
|
||||
type: 'asset',
|
||||
snippetCodename: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Post
|
||||
*/
|
||||
post: {
|
||||
codename: 'post',
|
||||
id: 'b1efdda5-18a3-5445-8ca2-51c252a9ee2d',
|
||||
externalId: '5126584e-b734-457a-8a9f-f954fd597fa6',
|
||||
name: 'Post',
|
||||
elements: {
|
||||
/**
|
||||
* Author (modular_content)
|
||||
*/
|
||||
author: {
|
||||
codename: 'author',
|
||||
id: 'aa68b9d2-e807-54be-ac82-543ff122b6b2',
|
||||
externalId: 'bc8e9421-f4d0-4c05-a607-572ac635aaa9',
|
||||
name: 'Author',
|
||||
required: false,
|
||||
type: 'modular_content',
|
||||
snippetCodename: undefined,
|
||||
},
|
||||
|
||||
/**
|
||||
* Content (rich_text)
|
||||
*/
|
||||
content: {
|
||||
codename: 'content',
|
||||
id: 'f8b91d9f-0a7d-557e-9fe8-ca1203ae4a67',
|
||||
externalId: 'b496e813-c1ad-4468-aee4-e0fcbfc2f075',
|
||||
name: 'Content',
|
||||
required: false,
|
||||
type: 'rich_text',
|
||||
snippetCodename: undefined,
|
||||
},
|
||||
|
||||
/**
|
||||
* Cover Image (asset)
|
||||
*/
|
||||
cover_image: {
|
||||
codename: 'cover_image',
|
||||
id: '8f1bd2ae-b15a-5100-b8ee-b279a87862fb',
|
||||
externalId: '072504ad-20d9-4918-a2db-bac37c7ac3d2',
|
||||
name: 'Cover Image',
|
||||
required: false,
|
||||
type: 'asset',
|
||||
snippetCodename: undefined,
|
||||
},
|
||||
|
||||
/**
|
||||
* Date (date_time)
|
||||
*/
|
||||
date: {
|
||||
codename: 'date',
|
||||
id: '1739ed56-ccd8-55a3-9cd3-67f09a8073db',
|
||||
externalId: 'c0510c50-1f74-47f1-85de-4780138651a0',
|
||||
name: 'Date',
|
||||
required: false,
|
||||
type: 'date_time',
|
||||
snippetCodename: undefined,
|
||||
},
|
||||
|
||||
/**
|
||||
* Excerpt (text)
|
||||
*/
|
||||
excerpt: {
|
||||
codename: 'excerpt',
|
||||
id: 'e1461965-9004-500e-9441-39a51aa3088b',
|
||||
externalId: '58f63166-ecb7-4c87-a644-6f616c6b2028',
|
||||
name: 'Excerpt',
|
||||
required: false,
|
||||
type: 'text',
|
||||
snippetCodename: undefined,
|
||||
},
|
||||
|
||||
/**
|
||||
* Slug (url_slug)
|
||||
*/
|
||||
slug: {
|
||||
codename: 'slug',
|
||||
id: 'f12efb12-764a-512c-82f8-4423d978b62b',
|
||||
externalId: 'b1bb5d5b-68b8-4fdc-91d9-1c9ad9efc84f',
|
||||
name: 'Slug',
|
||||
required: false,
|
||||
type: 'url_slug',
|
||||
snippetCodename: undefined,
|
||||
},
|
||||
|
||||
/**
|
||||
* Title (text)
|
||||
*/
|
||||
title: {
|
||||
codename: 'title',
|
||||
id: '933469d0-fe2b-51fa-8f62-600f103aee8b',
|
||||
externalId: '63664f0c-3d1d-4a89-adbe-5f502b227f9d',
|
||||
name: 'Title',
|
||||
required: true,
|
||||
type: 'text',
|
||||
snippetCodename: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const
|
9
examples/cms-kontent-ai/models/project/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export * from './languages'
|
||||
export * from './collections'
|
||||
export * from './contentTypes'
|
||||
export * from './contentTypeSnippets'
|
||||
export * from './taxonomies'
|
||||
export * from './workflows'
|
||||
export * from './roles'
|
||||
export * from './assetFolders'
|
||||
export * from './webhooks'
|
21
examples/cms-kontent-ai/models/project/languages.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Generated by '@kontent-ai/model-generator@5.9.0'
|
||||
*
|
||||
* Project name: Next.js Sample App
|
||||
* Environment: Production
|
||||
* Project Id: a7844231-064c-016c-1dad-38228cbc505d
|
||||
*/
|
||||
export const languages = {
|
||||
/**
|
||||
* Default project language
|
||||
*/
|
||||
default: {
|
||||
codename: 'default',
|
||||
id: '00000000-0000-0000-0000-000000000000',
|
||||
isActive: true,
|
||||
isDefault: true,
|
||||
fallbackLanguageId: '00000000-0000-0000-0000-000000000000',
|
||||
externalId: undefined,
|
||||
name: 'Default project language',
|
||||
},
|
||||
} as const
|
8
examples/cms-kontent-ai/models/project/roles.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Generated by '@kontent-ai/model-generator@5.9.0'
|
||||
*
|
||||
* Project name: Next.js Sample App
|
||||
* Environment: Production
|
||||
* Project Id: a7844231-064c-016c-1dad-38228cbc505d
|
||||
*/
|
||||
export const roles = {} as const
|
8
examples/cms-kontent-ai/models/project/taxonomies.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Generated by '@kontent-ai/model-generator@5.9.0'
|
||||
*
|
||||
* Project name: Next.js Sample App
|
||||
* Environment: Production
|
||||
* Project Id: a7844231-064c-016c-1dad-38228cbc505d
|
||||
*/
|
||||
export const taxonomies = {} as const
|
8
examples/cms-kontent-ai/models/project/webhooks.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Generated by '@kontent-ai/model-generator@5.9.0'
|
||||
*
|
||||
* Project name: Next.js Sample App
|
||||
* Environment: Production
|
||||
* Project Id: a7844231-064c-016c-1dad-38228cbc505d
|
||||
*/
|
||||
export const webhooks = {} as const
|
19
examples/cms-kontent-ai/models/project/workflows.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* Generated by '@kontent-ai/model-generator@5.9.0'
|
||||
*
|
||||
* Project name: Next.js Sample App
|
||||
* Environment: Production
|
||||
* Project Id: a7844231-064c-016c-1dad-38228cbc505d
|
||||
*/
|
||||
export const workflows = {
|
||||
/**
|
||||
* Default
|
||||
* Archived step Id: 7a535a69-ad34-47f8-806a-def1fdf4d391
|
||||
* Published step Id: c199950d-99f0-4983-b711-6c4c91624b22
|
||||
*/
|
||||
default: {
|
||||
codename: 'default',
|
||||
id: '00000000-0000-0000-0000-000000000000',
|
||||
name: 'Default',
|
||||
},
|
||||
} as const
|
1
examples/cms-kontent-ai/models/taxonomies/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export {}
|
|
@ -6,15 +6,19 @@
|
|||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kentico/kontent-delivery": "^11.13.0",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/react": "18.0.27",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"@kontent-ai/delivery-sdk": "^12.4.2",
|
||||
"classnames": "2.3.1",
|
||||
"date-fns": "2.28.0",
|
||||
"gray-matter": "4.0.3",
|
||||
"next": "latest",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"remark": "14.0.2",
|
||||
"remark-html": "15.0.1"
|
||||
"remark-html": "15.0.1",
|
||||
"typescript": "4.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "10.4.7",
|
6
examples/cms-kontent-ai/pages/_app.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { AppProps } from 'next/app'
|
||||
import '../styles/index.css'
|
||||
|
||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
||||
return <Component {...pageProps} />
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
export default async function exit(_, res) {
|
||||
import { NextApiResponse } from 'next'
|
||||
|
||||
export default async function exit(_: any, res: NextApiResponse) {
|
||||
// Exit the current user from "Preview Mode". This function accepts no args.
|
||||
res.clearPreviewData()
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { getPostBySlug } from '../../lib/api'
|
||||
|
||||
export default async function preview(req, res) {
|
||||
export default async function preview(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
// Check the secret and next parameters
|
||||
// This secret should only be known to this API route and the CMS
|
||||
if (
|
||||
|
@ -13,7 +17,7 @@ export default async function preview(req, res) {
|
|||
}
|
||||
|
||||
// Fetch the headless CMS to check if the provided `slug` exists
|
||||
const post = await getPostBySlug(req.query.slug, true)
|
||||
const post = await getPostBySlug(req.query.slug as string, true)
|
||||
|
||||
// If the slug doesn't exist prevent preview mode from being enabled
|
||||
if (!post) {
|
|
@ -6,8 +6,14 @@ import Layout from '../components/layout'
|
|||
import { getAllPosts } from '../lib/api'
|
||||
import Head from 'next/head'
|
||||
import { CMS_NAME } from '../lib/constants'
|
||||
import { Post } from '@/viewmodels/post'
|
||||
|
||||
export default function Index({ allPosts, preview }) {
|
||||
type IndexProps = {
|
||||
allPosts: Array<Post>
|
||||
preview: boolean
|
||||
}
|
||||
|
||||
export default function Index({ allPosts, preview }: IndexProps) {
|
||||
const heroPost = allPosts[0]
|
||||
const morePosts = allPosts.slice(1)
|
||||
return (
|
||||
|
@ -28,7 +34,7 @@ export default function Index({ allPosts, preview }) {
|
|||
excerpt={heroPost.excerpt}
|
||||
/>
|
||||
)}
|
||||
{morePosts.length > 0 && <MoreStories posts={morePosts} />}
|
||||
<>{morePosts.length > 0 && <MoreStories posts={morePosts} />}</>
|
||||
</Container>
|
||||
</Layout>
|
||||
</>
|
|
@ -15,8 +15,15 @@ import {
|
|||
import PostTitle from '../../components/post-title'
|
||||
import Head from 'next/head'
|
||||
import { CMS_NAME } from '../../lib/constants'
|
||||
import { Post as PostModel } from '@/viewmodels/post'
|
||||
|
||||
export default function Post({ post, morePosts = [], preview }) {
|
||||
type PostProps = {
|
||||
post: PostModel
|
||||
morePosts: Array<PostModel>
|
||||
preview: boolean
|
||||
}
|
||||
|
||||
export default function Post({ post, morePosts = [], preview }: PostProps) {
|
||||
const router = useRouter()
|
||||
if (!router.isFallback && !post?.slug) {
|
||||
return <ErrorPage statusCode={404} />
|
||||
|
@ -26,7 +33,7 @@ export default function Post({ post, morePosts = [], preview }) {
|
|||
<Container>
|
||||
<Header />
|
||||
{router.isFallback ? (
|
||||
<PostTitle>Loading…</PostTitle>
|
||||
<PostTitle title="Loading..."></PostTitle>
|
||||
) : (
|
||||
<>
|
||||
<article className="mb-32">
|
||||
|
@ -34,7 +41,7 @@ export default function Post({ post, morePosts = [], preview }) {
|
|||
<title>
|
||||
{post.title} | Next.js Blog Example with {CMS_NAME}
|
||||
</title>
|
||||
<meta property="og:image" content={post.coverImage.url} />
|
||||
<meta property="og:image" content={post.coverImage} />
|
||||
</Head>
|
||||
<PostHeader
|
||||
title={post.title}
|
||||
|
@ -53,10 +60,17 @@ export default function Post({ post, morePosts = [], preview }) {
|
|||
)
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params, preview = null }) {
|
||||
type StaticProps = {
|
||||
params: {
|
||||
slug: string
|
||||
}
|
||||
preview: boolean | null
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params, preview = null }: StaticProps) {
|
||||
return await Promise.all([
|
||||
getPostBySlug(params.slug, preview),
|
||||
getMorePostsForSlug(params.slug, preview),
|
||||
getPostBySlug(params.slug, preview ?? false),
|
||||
getMorePostsForSlug(params.slug, preview ?? false),
|
||||
]).then((values) => ({
|
||||
props: {
|
||||
post: values[0],
|
||||
|
@ -67,7 +81,7 @@ export async function getStaticProps({ params, preview = null }) {
|
|||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const slugs = await getAllPostSlugs(['slug'])
|
||||
const slugs = await getAllPostSlugs()
|
||||
return {
|
||||
paths: slugs.map(
|
||||
(slug) =>
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 595 B After Width: | Height: | Size: 595 B |
Before Width: | Height: | Size: 880 B After Width: | Height: | Size: 880 B |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
24
examples/cms-kontent-ai/tsconfig.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
4
examples/cms-kontent-ai/viewmodels/author.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export type Author = {
|
||||
name: string
|
||||
picture: string
|
||||
}
|
11
examples/cms-kontent-ai/viewmodels/post.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Author } from './author'
|
||||
|
||||
export type Post = {
|
||||
title: string
|
||||
slug: string
|
||||
date: string | null
|
||||
content: string
|
||||
excerpt: string
|
||||
coverImage: string
|
||||
author: Author
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export default function Container({ children }) {
|
||||
return <div className="container mx-auto px-5">{children}</div>
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import { parseISO, format } from 'date-fns'
|
||||
|
||||
export default function DateFormatter({ dateString }) {
|
||||
const date = parseISO(dateString)
|
||||
return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import NextImage from 'next/image'
|
||||
import { transformImageUrl } from '@kentico/kontent-delivery'
|
||||
|
||||
const srcIsKontentAsset = (src) => {
|
||||
try {
|
||||
const { hostname } = new URL(src)
|
||||
return hostname.endsWith('.kc-usercontent.com')
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const kontentImageLoader = ({ src, width, quality = 75 }) => {
|
||||
return new transformImageUrl(src)
|
||||
.withWidth(width)
|
||||
.withQuality(quality)
|
||||
.withCompression('lossless')
|
||||
.withAutomaticFormat()
|
||||
.getUrl()
|
||||
}
|
||||
|
||||
const getLoader = (src) => {
|
||||
return srcIsKontentAsset(src) ? kontentImageLoader : undefined
|
||||
}
|
||||
|
||||
export default function Image(props) {
|
||||
const loader = getLoader(props.src)
|
||||
|
||||
return <NextImage {...props} loader={loader} />
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import Alert from '../components/alert'
|
||||
import Footer from '../components/footer'
|
||||
import Meta from '../components/meta'
|
||||
|
||||
export default function Layout({ preview, children }) {
|
||||
return (
|
||||
<>
|
||||
<Meta />
|
||||
<div className="min-h-screen">
|
||||
<Alert preview={preview} />
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
import PostPreview from '../components/post-preview'
|
||||
|
||||
export default function MoreStories({ posts }) {
|
||||
return (
|
||||
<section>
|
||||
<h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight">
|
||||
More Stories
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32">
|
||||
{posts.map((post) => (
|
||||
<PostPreview
|
||||
key={post.slug}
|
||||
title={post.title}
|
||||
coverImage={post.coverImage}
|
||||
date={post.date}
|
||||
author={post.author}
|
||||
slug={post.slug}
|
||||
excerpt={post.excerpt}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
import Avatar from '../components/avatar'
|
||||
import DateFormatter from '../components/date-formatter'
|
||||
import CoverImage from './cover-image'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function PostPreview({
|
||||
title,
|
||||
coverImage,
|
||||
date,
|
||||
excerpt,
|
||||
author,
|
||||
slug,
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-5">
|
||||
<CoverImage slug={slug} title={title} src={coverImage} />
|
||||
</div>
|
||||
<h3 className="text-3xl mb-3 leading-snug">
|
||||
<Link href={`/posts/${slug}`} className="hover:underline">
|
||||
{title}
|
||||
</Link>
|
||||
</h3>
|
||||
<div className="text-lg mb-4">
|
||||
<DateFormatter dateString={date} />
|
||||
</div>
|
||||
<p className="text-lg leading-relaxed mb-4">{excerpt}</p>
|
||||
<Avatar name={author.name} picture={author.picture} />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export const EXAMPLE_PATH = 'cms-kontent'
|
||||
export const CMS_NAME = 'Kontent'
|
||||
export const CMS_URL = 'https://kontent.ai'
|
||||
export const HOME_OG_IMAGE_URL =
|
||||
'https://og-image.vercel.app/Next.js%20Blog%20Example%20with%20**Kontent**.png?theme=light&md=1&fontSize=75px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg&images=https%3A%2F%2Fraw.githubusercontent.com%2FKentico%2FHome%2Fmaster%2Fimages%2Fkk-logo-nextjs.png'
|
|
@ -1,5 +0,0 @@
|
|||
import '../styles/index.css'
|
||||
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
return <Component {...pageProps} />
|
||||
}
|