Upgrade librabries and rebranding kontent.ai (#45260)

Co-authored-by: JJ Kasper <jj@jjsweb.site>
This commit is contained in:
Ivan Kiráľ 2023-02-06 22:21:52 +01:00 committed by GitHub
parent a5f68b3c6c
commit 7c15278cc2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
78 changed files with 714 additions and 203 deletions

View file

@ -1,3 +1,3 @@
KONTENT_PROJECT_ID=
KONTENT_PREVIEW_SECRET=
KONTENT_PROJECT_ID=
KONTENT_PREVIEW_SECRET=
KONTENT_PREVIEW_API_KEY=

View file

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

View 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', {

View file

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

View 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>
}

View file

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

View 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>
}

View file

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

View 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} />
}

View 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 />
</>
)
}

View 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>
)
}

View file

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

View file

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

View 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>
)
}

View file

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

View file

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View file

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

View file

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 191 KiB

View file

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View file

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 193 KiB

View file

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

View 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'

View file

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

View file

@ -0,0 +1 @@
export {}

View 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
}>

View file

@ -0,0 +1,2 @@
export * from './author'
export * from './post'

View 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
}>

View file

@ -0,0 +1,4 @@
export * from './project'
export * from './content-types'
export * from './content-type-snippets'
export * from './taxonomies'

View 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

View 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

View 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 contentTypeSnippets = {} as const

View 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

View 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'

View 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

View 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

View 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

View 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

View 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

View file

@ -0,0 +1 @@
export {}

View file

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

View file

@ -0,0 +1,6 @@
import { AppProps } from 'next/app'
import '../styles/index.css'
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}

View file

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

View file

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

View file

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

View file

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

View file

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 595 B

After

Width:  |  Height:  |  Size: 595 B

View file

Before

Width:  |  Height:  |  Size: 880 B

After

Width:  |  Height:  |  Size: 880 B

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

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

View file

@ -0,0 +1,4 @@
export type Author = {
name: string
picture: string
}

View 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
}

View file

@ -1,3 +0,0 @@
export default function Container({ children }) {
return <div className="container mx-auto px-5">{children}</div>
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +0,0 @@
import '../styles/index.css'
export default function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}