chore(examples): Cosmic cms updates (#41080)

Lands #38163 with merge conflict fixes. Needed to open a new PR, because
GitHub does not allow maintainers to edit organization forks.

https://github.com/orgs/community/discussions/5634

Closes #38163


## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)

Co-authored-by: sherazmalik06 <sherazmalik06@gmail.com>
Co-authored-by: Tony Spiro <tspiro@tonyspiro.com>
This commit is contained in:
Balázs Orbán 2022-10-01 06:46:25 +02:00 committed by GitHub
parent e875dded8b
commit 3caebdef67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 293 additions and 83 deletions

View file

@ -2,7 +2,12 @@ import Container from './container'
import cn from 'classnames'
import { EXAMPLE_PATH } from '@/lib/constants'
export default function Alert({ preview }) {
type AlertProps = {
preview: boolean
}
const Alert = (props: AlertProps) => {
const { preview } = props
return (
<div
className={cn('border-b', {
@ -14,7 +19,7 @@ export default function Alert({ preview }) {
<div className="py-2 text-center text-sm">
{preview ? (
<>
This is page is a preview.{' '}
This page is a preview.{' '}
<a
href="/api/exit-preview"
className="underline hover:text-cyan duration-200 transition-colors"
@ -40,3 +45,5 @@ export default function Alert({ preview }) {
</div>
)
}
export default Alert

View file

@ -1,6 +1,13 @@
import Image from 'next/image'
export default function Avatar({ name, picture }) {
type AvatarProps = {
name: string
picture: string
}
const Avatar = (props: AvatarProps) => {
const { name, picture } = props
return (
<div className="flex items-center">
<div className="w-12 h-12 relative mr-4">
@ -17,3 +24,5 @@ export default function Avatar({ name, picture }) {
</div>
)
}
export default Avatar

View file

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

View file

@ -0,0 +1,12 @@
import { ReactNode } from 'react'
type ContainerProps = {
children: ReactNode
}
const Container = (props: ContainerProps) => {
const { children } = props
return <div className="container mx-auto px-5">{children}</div>
}
export default Container

View file

@ -2,7 +2,14 @@ import cn from 'classnames'
import Link from 'next/link'
import Imgix from 'react-imgix'
export default function CoverImage({ title, url, slug }) {
type CoverImageProps = {
title
url
slug
}
const CoverImage = (props: CoverImageProps) => {
const { title, url, slug } = props
const image = (
<Imgix
src={url}
@ -33,3 +40,4 @@ export default function CoverImage({ title, url, slug }) {
</div>
)
}
export default CoverImage

View file

@ -1,6 +1,13 @@
import { parseISO, format } from 'date-fns'
export default function Date({ dateString }) {
type DateProps = {
dateString: string
}
const Date = (props: DateProps) => {
const { dateString } = props
const date = parseISO(dateString)
return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>
}
export default Date

View file

@ -1,7 +1,7 @@
import Container from './container'
import { EXAMPLE_PATH } from '@/lib/constants'
export default function Footer() {
const Footer = () => {
return (
<footer className="bg-accent-1 border-t border-accent-2">
<Container>
@ -28,3 +28,5 @@ export default function Footer() {
</footer>
)
}
export default Footer

View file

@ -1,6 +1,6 @@
import Link from 'next/link'
export default function Header() {
const Header = () => {
return (
<h2 className="text-2xl md:text-4xl font-bold tracking-tight md:tracking-tighter leading-tight mb-20 mt-8">
<Link href="/">
@ -10,3 +10,5 @@ export default function Header() {
</h2>
)
}
export default Header

View file

@ -2,15 +2,20 @@ import Avatar from './avatar'
import Date from './date'
import CoverImage from './cover-image'
import Link from 'next/link'
import { AuthorType, ImgixType } from 'interfaces'
type HeroPostProps = {
title: string
coverImage: ImgixType
date: string
excerpt: string
author: AuthorType
slug: string
}
const HeroPost = (props: HeroPostProps) => {
const { title, coverImage, date, excerpt, author, slug } = props
export default function HeroPost({
title,
coverImage,
date,
excerpt,
author,
slug,
}) {
return (
<section>
<div className="mb-8 md:mb-16">
@ -38,3 +43,5 @@ export default function HeroPost({
</section>
)
}
export default HeroPost

View file

@ -1,6 +1,6 @@
import { CMS_NAME, CMS_URL } from '@/lib/constants'
export default function Intro() {
const Intro = () => {
return (
<section className="flex-col md:flex-row flex items-center md:justify-between mt-16 mb-16 md:mb-12">
<h1 className="text-6xl md:text-8xl font-bold tracking-tighter leading-tight md:pr-8">
@ -26,3 +26,5 @@ export default function Intro() {
</section>
)
}
export default Intro

View file

@ -3,8 +3,14 @@ import Footer from './footer'
import Meta from './meta'
import 'lazysizes'
import 'lazysizes/plugins/parent-fit/ls.parent-fit'
import { ReactNode } from 'react'
export default function Layout({ preview, children }) {
type LayoutProps = {
preview: boolean
children: ReactNode
}
const Layout = ({ preview, children }: LayoutProps) => {
return (
<>
<Meta />
@ -16,3 +22,5 @@ export default function Layout({ preview, children }) {
</>
)
}
export default Layout

View file

@ -1,7 +1,7 @@
import Head from 'next/head'
import { CMS_NAME, HOME_OG_IMAGE_URL } from '@/lib/constants'
export default function Meta() {
const Meta = () => {
return (
<Head>
<link
@ -40,3 +40,5 @@ export default function Meta() {
</Head>
)
}
export default Meta

View file

@ -1,6 +1,12 @@
import { PostType } from 'interfaces'
import PostPreview from './post-preview'
export default function MoreStories({ posts }) {
type MoreStoriesProps = {
posts: PostType[]
}
const MoreStories = (props: MoreStoriesProps) => {
const { posts } = props
return (
<section>
<h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight">
@ -22,3 +28,5 @@ export default function MoreStories({ posts }) {
</section>
)
}
export default MoreStories

View file

@ -1,6 +1,11 @@
import markdownStyles from './markdown-styles.module.css'
export default function PostBody({ content }) {
type PostBodyProps = {
content: string
}
const PostBody = (props: PostBodyProps) => {
const { content } = props
return (
<div className="max-w-2xl mx-auto">
<div
@ -10,3 +15,5 @@ export default function PostBody({ content }) {
</div>
)
}
export default PostBody

View file

@ -2,8 +2,17 @@ import Avatar from './avatar'
import Date from './date'
import CoverImage from './cover-image'
import PostTitle from './post-title'
import { AuthorType, ImgixType } from 'interfaces'
export default function PostHeader({ title, coverImage, date, author }) {
type PostHeaderProps = {
title: string
coverImage: ImgixType
date: string
author: AuthorType
}
const PostHeader = (props: PostHeaderProps) => {
const { title, coverImage, date, author } = props
return (
<>
<PostTitle>{title}</PostTitle>
@ -14,11 +23,14 @@ export default function PostHeader({ title, coverImage, date, author }) {
/>
</div>
<div className="mb-8 md:mb-16 sm:mx-0">
<CoverImage title={title} url={coverImage.imgix_url} />
<CoverImage title={title} url={coverImage.imgix_url} slug={''} />
</div>
<div className="max-w-2xl mx-auto">
<div className="block md:hidden mb-6">
<Avatar name={author.name} picture={author.picture} />
<Avatar
name={author.title}
picture={author.metadata.picture.imgix_url}
/>
</div>
<div className="mb-6 text-lg">
<Date dateString={date} />
@ -27,3 +39,5 @@ export default function PostHeader({ title, coverImage, date, author }) {
</>
)
}
export default PostHeader

View file

@ -2,15 +2,20 @@ import Avatar from './avatar'
import Date from './date'
import CoverImage from './cover-image'
import Link from 'next/link'
import { AuthorType, ImgixType } from 'interfaces'
type PostPreviewProps = {
title: string
coverImage: ImgixType
date: string
excerpt: string
author: AuthorType
slug: string
}
const PostPreview = (props: PostPreviewProps) => {
const { title, coverImage, date, excerpt, author, slug } = props
export default function PostPreview({
title,
coverImage,
date,
excerpt,
author,
slug,
}) {
return (
<div>
<div className="mb-5">
@ -29,3 +34,5 @@ export default function PostPreview({
</div>
)
}
export default PostPreview

View file

@ -1,7 +1,15 @@
export default function PostTitle({ children }) {
import { ReactNode } from 'react'
type PostTitleProps = {
children: ReactNode
}
const PostTitle = (props: PostTitleProps) => {
const { children } = props
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}
</h1>
)
}
export default PostTitle

View file

@ -1,3 +0,0 @@
export default function SectionSeparator() {
return <hr className="border-accent-2 mt-28 mb-24" />
}

View file

@ -0,0 +1,5 @@
const SectionSeparator = () => {
return <hr className="border-accent-2 mt-28 mb-24" />
}
export default SectionSeparator

View file

@ -0,0 +1,24 @@
export type ImgixType = {
url: string
imgix_url: string
}
export type AuthorType = {
title: string
metadata: {
picture: ImgixType
}
}
export type PostType = {
title: string
slug: string
content: string
created_at: string
metadata: {
cover_image: ImgixType
author: AuthorType
excerpt: string
content: string
}
}

View file

@ -1,4 +1,5 @@
import Cosmic from 'cosmicjs'
import { PostType } from 'interfaces'
import ErrorPage from 'next/error'
const BUCKET_SLUG = process.env.COSMIC_BUCKET_SLUG
@ -9,26 +10,26 @@ const bucket = Cosmic().bucket({
read_key: READ_KEY,
})
export async function getPreviewPostBySlug(slug) {
export const getPreviewPostBySlug = async (slug: string) => {
const params = {
query: {
slug,
type: 'posts',
},
props: 'slug',
status: 'all',
status: 'any',
}
try {
const data = await bucket.getObjects(params)
return data.objects[0]
} catch (err) {
// 404 if slug not found
// Don't throw if an slug doesn't exist
return <ErrorPage statusCode={err.status} />
}
}
export async function getAllPostsWithSlug() {
export const getAllPostsWithSlug = async () => {
const params = {
query: {
type: 'posts',
@ -39,27 +40,33 @@ export async function getAllPostsWithSlug() {
return data.objects
}
export async function getAllPostsForHome(preview) {
export const getAllPostsForHome = async (preview: boolean): Promise<Post[]> => {
const params = {
query: {
type: 'posts',
},
props: 'title,slug,metadata,created_at',
sort: '-created_at',
...(preview && { status: 'all' }),
...(preview && { status: 'any' }),
}
const data = await bucket.getObjects(params)
return data.objects
}
export async function getPostAndMorePosts(slug, preview) {
export const getPostAndMorePosts = async (
slug: string,
preview: boolean
): Promise<{
post: PostType
morePosts: PostType[]
}> => {
const singleObjectParams = {
query: {
slug,
type: 'posts',
},
props: 'slug,title,metadata,created_at',
...(preview && { status: 'all' }),
...(preview && { status: 'any' }),
}
const moreObjectParams = {
query: {
@ -67,15 +74,14 @@ export async function getPostAndMorePosts(slug, preview) {
},
limit: 3,
props: 'title,slug,metadata,created_at',
...(preview && { status: 'all' }),
...(preview && { status: 'any' }),
}
let object
try {
const data = await bucket.getObjects(singleObjectParams)
object = data.objects[0]
} catch (err) {
// 404 if slug not found
return <ErrorPage statusCode={err.status} />
throw err
}
const moreObjects = await bucket.getObjects(moreObjectParams)
const morePosts = moreObjects.objects

View file

@ -1,7 +1,9 @@
import { remark } from 'remark'
import html from 'remark-html'
export default async function markdownToHtml(markdown) {
const markdownToHtml = async (markdown: string) => {
const result = await remark().use(html).process(markdown)
return result.toString()
}
export default markdownToHtml

5
examples/cms-cosmic/next-env.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -9,18 +9,20 @@
"classnames": "2.3.1",
"cosmicjs": "4.1.7",
"date-fns": "2.28.0",
"lazysizes": "^5.2.1-rc2",
"lazysizes": "^5.3.2",
"next": "latest",
"react": "^17.0.2",
"react-datocms": "1.6.3",
"react-dom": "^17.0.2",
"react-imgix": "^9.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-imgix": "^9.5.1",
"remark": "14.0.2",
"remark-html": "15.0.1"
},
"devDependencies": {
"autoprefixer": "10.4.2",
"postcss": "8.4.5",
"tailwindcss": "^3.0.15"
"@types/node": "^18.0.0",
"@types/react": "^18.0.14",
"autoprefixer": "10.4.7",
"postcss": "8.4.14",
"tailwindcss": "^3.1.4",
"typescript": "^4.7.4"
}
}

View file

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

View file

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

View file

@ -1,4 +1,4 @@
export default async function exit(_, res) {
export default function exit (_, res) {
// Exit the current user from "Preview Mode". This function accepts no args.
res.clearPreviewData()

View file

@ -1,7 +1,7 @@
import { getPreviewPostBySlug } from '@/lib/api'
export default async function preview(req, res) {
// Check the secret and next parameters
// Check the secret and next parameters
// This secret should only be known to this API route and the CMS
if (
req.query.secret !== process.env.COSMIC_PREVIEW_SECRET ||

View file

@ -6,13 +6,22 @@ import Layout from '@/components/layout'
import { getAllPostsForHome } from '@/lib/api'
import Head from 'next/head'
import { CMS_NAME } from '@/lib/constants'
import { PostType } from 'interfaces'
type IndexProps = {
allPosts: PostType[]
preview: boolean
}
const Index = (props: IndexProps) => {
const { allPosts, preview } = props
export default function Index({ allPosts }) {
const heroPost = allPosts[0]
const morePosts = allPosts.slice(1)
return (
<>
<Layout>
<Layout preview={preview}>
<Head>
<title>Next.js Blog Example with {CMS_NAME}</title>
</Head>
@ -35,9 +44,16 @@ export default function Index({ allPosts }) {
)
}
export async function getStaticProps({ preview }) {
export default Index
type staticProps = {
preview: boolean
}
export const getStaticProps = async (props: staticProps) => {
const { preview = null } = props
const allPosts = (await getAllPostsForHome(preview)) || []
return {
props: { allPosts },
props: { allPosts, preview },
}
}

View file

@ -12,8 +12,17 @@ import PostTitle from '@/components/post-title'
import Head from 'next/head'
import { CMS_NAME } from '@/lib/constants'
import markdownToHtml from '@/lib/markdownToHtml'
import { PostType } from 'interfaces'
import { ParsedUrlQueryInput } from 'querystring'
export default function Post({ post, morePosts, preview }) {
type PostProps = {
post: PostType
morePosts: PostType[]
preview
}
const Post = (props: PostProps) => {
const { post, morePosts, preview } = props
const router = useRouter()
if (!router.isFallback && !post?.slug) {
return <ErrorPage statusCode={404} />
@ -52,20 +61,30 @@ export default function Post({ post, morePosts, preview }) {
</Layout>
)
}
export default Post
export async function getStaticProps({ params, preview = null }) {
const data = await getPostAndMorePosts(params.slug, preview)
const content = await markdownToHtml(data.post?.metadata?.content || '')
type staticProps = {
params: ParsedUrlQueryInput
preview: boolean
}
return {
props: {
preview,
post: {
...data.post,
content,
export const getStaticProps = async (props: staticProps) => {
const { params, preview = null } = props
try {
const data = await getPostAndMorePosts(params.slug as string, preview)
const content = await markdownToHtml(data['post']?.metadata?.content || '')
return {
props: {
preview,
post: {
...data['post'],
content,
},
morePosts: data['morePosts'] || [],
},
morePosts: data.morePosts || [],
},
}
} catch (err) {
return <ErrorPage statusCode={err.status} />
}
}

View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"],
"@/lib/*": ["lib/*"],
"@/styles/*": ["styles/*"]
},
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "Node",
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["./components/..", "./lib/..", "./styles/.."],
"exclude": ["node_modules"]
}