未验证 提交 1f5c2c85 编写于 作者: T Tim 提交者: GitHub

Add example prepr cms (#22776)

Added preview example with Prepr CMS.

Working demo at https://next-blog-prepr.vercel.app/
上级 f00f4bd9
PREPRIO_API=https://graphql.prepr.io/graphql
PREPRIO_PRODUCTION_TOKEN=
PREPRIO_PREVIEW_TOKEN=
PREPRIO_PREVIEW_KEY=
\ No newline at end of file
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
# A statically generated blog example using Next.js and Prepr
This example showcases Next.js's [Static Generation](https://nextjs.org/docs/basic-features/pages) feature using [Prepr](https://prepr.io/) as the data source.
## Demo
- **Live**: [https://next-blog-prepr.now.sh/](https://next-blog-prepr.now.sh/)
- **Preview Mode**: [https://next-blog-prepr.now.sh/api/preview...](https://next-blog-prepr.now.sh/api/preview?secret=237864ihasdhj283768&slug=discover-enjoy-amsterdam)
### [https://next-blog-prepr.now.sh/](https://next-blog-prepr.now.sh/)
### Related examples
- [WordPress](/examples/cms-wordpress)
- [DatoCMS](/examples/cms-datocms)
- [Sanity](/examples/cms-sanity)
- [TakeShape](/examples/cms-takeshape)
- [Prismic](/examples/cms-prismic)
- [Contentful](/examples/cms-contentful)
- [Strapi](/examples/cms-strapi)
- [Agility CMS](/examples/cms-agilitycms)
- [Cosmic](/examples/cms-cosmic)
- [ButterCMS](/examples/cms-buttercms)
- [Storyblok](/examples/cms-storyblok)
- [Kontent](/examples/cms-kontent)
- [Ghost](/examples/cms-ghost)
- [GraphCMS](/examples/cms-graphcms)
- [Blog Starter](/examples/blog-starter)
## Getting Started
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-prepr&project-name=cms-prepr&repository-name=cms-prepr&env=PREPRIO_API,PREPRIO_PRODUCTION_TOKEN,PREPRIO_PREVIEW_TOKEN,PREPRIO_PREVIEW_KEY&envDescription=Required%20to%20connect%20the%20app%20with%20Prepr&envLink=https://vercel.link/cms-prepr-env)
## How to use
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
```bash
npx create-next-app --example cms-prepr cms-prepr-app
# or
yarn create next-app --example cms-prepr cms-prepr-app
```
## Configuration
### Step 1. Create an account and a environment in Prepr
First, [create an account in Prepr](https://prepr.io).
### Step 2. Create Author model
From your Prepr dashboard, click **Settings** -> **Models**
Click on the arrow next to **Add model** and select **Import**.
Import the [`models/author.json`](models/author.json) file.
After that
Import the [`models/post.json`](models/post.json) file.
Click on the Author field and select `Author` at the option `Publication model` and click **Save**.
### Step 3. Set up environment variables
Copy the `.env.local.example` file in this directory to `.env.local` (which will be ignored by Git):
```bash
cp .env.local.example .env.local
```
Inside your environment, navigate to **Settings > Development > Access Tokens**.
Click **Add access token**, enter the name `Next.js Preview` and add the scope `graphql_preview` and click **Save**.
Copy the generated access token and set the variable `PREPRIO_PREVIEW_TOKEN` in `.env.local`.
Go back to the Access token overview and click **Add access token**.
Enter the name `Next.js Production` and add the scope `graphql_published` and click **Save**.
Copy the generated access token and set the variable `PREPRIO_PRODUCTION_TOKEN` in `.env.local`.
The `PREPRIO_PREVIEW_KEY` can be any random string (but avoid spaces), like a UUID`, this is used
for [Preview Mode](https://nextjs.org/docs/advanced-features/preview-mode).
### Step 4. Run Next.js in development mode
```bash
npm install
npm run dev
# or
yarn install
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
In Prepr, go to one of the posts in your environment and:
- **Update the title**. For example, you can add `[REVIEW]` in front of the title.
- After you edit the publication save the post with a review state.
To view the preview, transform the url to the following format: `http://localhost:3000/api/preview?secret=<YOUR_SECRET_TOKEN>&slug=<SLUG_TO_PREVIEW>` where `<YOUR_SECRET_TOKEN>` is
the same secret you defined in the `.env.local` file and `<SLUG_TO_PREVIEW>` is the slug of one of the posts you want to preview.
You should now be able to see post that are in Review and Done state. To exit the preview mode, you can click on _"Click here to exit preview mode"_ at the top.
### Step 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).
**Important**: When you import your project on Vercel, make sure to click on **Environment Variables** and set them to match your `.env.local` file.
#### Deploy from Our Template
Alternatively, you can deploy using our template by clicking on the Deploy button below.
[![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-prepr&project-name=cms-prepr&repository-name=cms-prepr&env=PREPRIO_API,PREPRIO_PRODUCTION_TOKEN,PREPRIO_PREVIEW_TOKEN,PREPRIO_PREVIEW_KEY&envDescription=Required%20to%20connect%20the%20app%20with%20Prepr&envLink=https://vercel.link/cms-prepr-env)
import Container from './container'
import cn from 'classnames'
import { EXAMPLE_PATH } from '../lib/constants'
export default function Alert({ preview }) {
return (
<div
className={cn('border-b', {
'bg-accent-7 border-accent-7 text-white': preview,
'bg-accent-1 border-accent-2': !preview,
})}
>
<Container>
<div className="py-2 text-center text-sm">
{preview ? (
<>
This page is a preview.{' '}
<a
href="/api/exit-preview"
className="underline hover:text-cyan duration-200 transition-colors"
>
Click here
</a>{' '}
to exit preview mode.
</>
) : (
<>
The source code for this blog is{' '}
<a
href={`https://github.com/vercel/next.js/tree/canary/examples/${EXAMPLE_PATH}`}
className="underline hover:text-success duration-200 transition-colors"
>
available on GitHub
</a>
.
</>
)}
</div>
</Container>
</div>
)
}
import Image from 'next/image'
export default function Avatar({ name, picture }) {
return (
<div className="flex items-center">
<div className="w-12 h-12 relative mr-4">
<Image
src={picture}
layout="fill"
className="rounded-full"
alt={name}
/>
</div>
<div className="text-xl font-bold">{name}</div>
</div>
)
}
export default function Container({ children }) {
return <div className="container mx-auto px-5">{children}</div>
}
import Image from 'next/image'
import Link from 'next/link'
import cn from 'classnames'
export default function CoverImage({ title, url, slug }) {
const image = (
<Image
width={2000}
height={1000}
alt={`Cover Image for ${title}`}
className={cn('shadow-small', {
'hover:shadow-medium transition-shadow duration-200': slug,
})}
src={url}
/>
)
return (
<div className="sm:mx-0">
{slug ? (
<Link as={`/posts/${slug}`} href="/posts/[slug]">
<a aria-label={title}>{image}</a>
</Link>
) : (
image
)}
</div>
)
}
import { parseISO, format } from 'date-fns'
export default function Date({ dateString }) {
const date = parseISO(dateString)
return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>
}
import Container from './container'
import { EXAMPLE_PATH } from '../lib/constants'
export default function Footer() {
return (
<footer className="bg-accent-1 border-t border-accent-2">
<Container>
<div className="py-28 flex flex-col lg:flex-row items-center">
<h3 className="text-4xl lg:text-5xl font-bold tracking-tighter leading-tight text-center lg:text-left mb-10 lg:mb-0 lg:pr-4 lg:w-1/2">
Statically Generated with Next.js.
</h3>
<div className="flex flex-col lg:flex-row justify-center items-center lg:pl-4 lg:w-1/2">
<a
href="https://nextjs.org/docs/basic-features/pages"
className="mx-3 bg-black hover:bg-white hover:text-black border border-black text-white font-bold py-3 px-12 lg:px-8 duration-200 transition-colors mb-6 lg:mb-0"
>
Read Documentation
</a>
<a
href={`https://github.com/vercel/next.js/tree/canary/examples/${EXAMPLE_PATH}`}
className="mx-3 font-bold hover:underline"
>
View on GitHub
</a>
</div>
</div>
</Container>
</footer>
)
}
import Link from 'next/link'
export default function Header() {
return (
<h2 className="text-2xl md:text-4xl font-bold tracking-tight md:tracking-tighter leading-tight mb-20 mt-8">
<Link href="/">
<a className="hover:underline">Blog</a>
</Link>
.
</h2>
)
}
import Avatar from '../components/avatar'
import Date from '../components/date'
import CoverImage from '../components/cover-image'
import Link from 'next/link'
export default function HeroPost({
title,
coverImage,
date,
excerpt,
author,
slug,
}) {
return (
<section>
<div className="mb-8 md:mb-16">
<CoverImage slug={slug} title={title} url={coverImage} />
</div>
<div className="mb-20 md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 md:mb-28">
<div>
<h3 className="mb-4 text-4xl leading-tight lg:text-6xl">
<Link as={`/posts/${slug}`} href="/posts/[slug]">
<a className="hover:underline">{title}</a>
</Link>
</h3>
<div className="mb-4 text-lg md:mb-0">
<Date dateString={date} />
</div>
</div>
<div>
<p className="mb-4 text-lg leading-relaxed">{excerpt}</p>
<Avatar
name={author.name}
picture={author.cover[0].cdn_files[0].url}
/>
</div>
</div>
</section>
)
}
import { CMS_NAME, CMS_URL } from '../lib/constants'
export default function 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">
Blog.
</h1>
<h4 className="text-center md:text-left text-lg mt-5 md:pl-8">
A statically generated blog example using{' '}
<a
href="https://nextjs.org/"
className="underline hover:text-success duration-200 transition-colors"
>
Next.js
</a>{' '}
and{' '}
<a
href={CMS_URL}
className="underline hover:text-success duration-200 transition-colors"
>
{CMS_NAME}
</a>
.
</h4>
</section>
)
}
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 />
</>
)
}
import Head from 'next/head'
import { CMS_NAME, HOME_OG_IMAGE_URL } from '../lib/constants'
export default function Meta() {
return (
<Head>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/favicon/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon/favicon-16x16.png"
/>
<link rel="manifest" href="/favicon/site.webmanifest" />
<link
rel="mask-icon"
href="/favicon/safari-pinned-tab.svg"
color="#000000"
/>
<link rel="shortcut icon" href="/favicon/favicon.ico" />
<meta name="msapplication-TileColor" content="#000000" />
<meta name="msapplication-config" content="/favicon/browserconfig.xml" />
<meta name="theme-color" content="#000" />
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
<meta
name="description"
content={`A statically generated blog example using Next.js and ${CMS_NAME}.`}
/>
<meta property="og:image" content={HOME_OG_IMAGE_URL} />
</Head>
)
}
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.cover[0].cdn_files[0].url}
date={post.date}
author={post.author[0]}
slug={post._slug}
excerpt={post.summary}
/>
))}
</div>
</section>
)
}
import postStyles from './post-styles.module.css'
export default function PostBody({ content }) {
return (
<div
className={`max-w-2xl mx-auto post ${postStyles.post}`}
dangerouslySetInnerHTML={{ __html: content }}
/>
)
}
import Avatar from '../components/avatar'
import Date from '../components/date'
import CoverImage from '../components/cover-image'
import PostTitle from '../components/post-title'
export default function PostHeader({ title, coverImage, date, author }) {
return (
<>
<PostTitle>{title}</PostTitle>
<div className="hidden md:block md:mb-12">
<Avatar name={author.name} picture={author.cover[0].cdn_files[0].url} />
</div>
<div className="mb-8 -mx-5 md:mb-16 sm:mx-0">
<CoverImage title={title} url={coverImage} />
</div>
<div className="max-w-2xl mx-auto">
<div className="block mb-6 md:hidden">
<Avatar
name={author.name}
picture={author.cover[0].cdn_files[0].url}
/>
</div>
<div className="mb-6 text-lg">
<Date dateString={date} />
</div>
</div>
</>
)
}
import Avatar from '../components/avatar'
import Date from '../components/date'
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} url={coverImage} />
</div>
<h3 className="mb-3 text-3xl leading-snug">
<Link as={`/posts/${slug}`} href="/posts/[slug]">
<a className="hover:underline">{title}</a>
</Link>
</h3>
<div className="mb-4 text-lg">
<Date dateString={date} />
</div>
<p className="mb-4 text-lg leading-relaxed">{excerpt}</p>
<Avatar name={author.name} picture={author.cover[0].cdn_files[0].url} />
</div>
)
}
.post {
@apply text-lg leading-relaxed;
}
.post p,
.post ul,
.post ol,
.post blockquote {
@apply my-6;
}
.post h1 {
@apply mt-12 mb-4 text-4xl leading-snug;
}
.post h2 {
@apply mt-12 mb-4 text-3xl leading-snug;
}
.post h3 {
@apply mt-8 mb-4 text-2xl leading-snug;
}
export default function PostTitle({ children }) {
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 function SectionSeparator() {
return <hr className="border-accent-2 mt-28 mb-24" />
}
{
"compilerOptions": {
"baseUrl": "."
}
}
export const EXAMPLE_PATH = 'cms-prepr'
export const CMS_NAME = 'Prepr'
export const CMS_URL = 'https://prepr.io/'
export const HOME_OG_IMAGE_URL = ''
import { createPreprClient } from '@preprio/nodejs-sdk'
const prepr = createPreprClient({
token: process.env.PREPRIO_PRODUCTION_TOKEN,
timeout: 4000,
baseUrl: process.env.PREPRIO_API,
})
export { prepr }
export async function getAllPostsForHome(preview) {
// Query publications
const data =
(await prepr
.graphqlQuery(
`
query {
Posts {
items {
_id,
_slug,
date: _publish_on
title,
summary,
author {
name
cover {
cdn_files {
url(width: 100, height:100)
}
}
}
cover {
cdn_files {
url(width:2000, height:1000)
}
}
}
}
}`
)
.token(
preview
? process.env.PREPRIO_PREVIEW_TOKEN
: process.env.PREPRIO_PRODUCTION_TOKEN
)
.fetch()) || []
return data.data.Posts.items
}
export async function getAllPostsWithSlug() {
// Query publications
const data =
(await prepr
.graphqlQuery(
`
query {
Posts {
items {
slug : _slug,
}
}
}`
)
.fetch()) || []
return data.data.Posts.items
}
export async function getPostAndMorePosts(slug, preview) {
// Query publications
const data =
(await prepr
.graphqlQuery(
`
query slugPost($slug: String!) {
Post ( slug : $slug) {
_id,
_slug,
date: _publish_on
title,
summary,
content,
author {
name
cover {
cdn_files {
url(width: 100, height:100)
}
}
}
cover {
cdn_files {
url(width:2000, height:1000)
}
}
}
morePosts : Posts(where : { _slug_nany : [$slug] }) {
items {
_id,
_slug,
date: _publish_on
title,
summary,
author {
name
cover {
cdn_files {
url(width: 100, height:100)
}
}
}
cover {
cdn_files {
url(width:2000, height:1000)
}
}
}
}
}`
)
.graphqlVariables({
slug: slug,
})
.token(
preview
? process.env.PREPRIO_PREVIEW_TOKEN
: process.env.PREPRIO_PRODUCTION_TOKEN
)
.fetch()) || []
return data.data
}
export async function getPreviewPostBySlug(slug) {
// Query publications
const data =
(await prepr
.graphqlQuery(
`
query preview($slug: String!) {
Post ( slug : $slug) {
_id,
slug : _slug
}
}`
)
.token(process.env.PREPRIO_PREVIEW_TOKEN)
.graphqlVariables({
slug: slug,
})
.fetch()) || []
return data.data.Post
}
{
"id": "f7153965-7bc9-4b9b-b964-49129b34c5e8",
"created_on": "2021-03-04T12:03:46+00:00",
"changed_on": "2021-03-04T12:03:46+00:00",
"body": "Author",
"description": null,
"label": "PublicationModel",
"status": "published",
"stories": false,
"timelines": false,
"allow_stories": false,
"channels": false,
"allow_channels": false,
"container_required": false,
"channels_required": false,
"seo_score": false,
"ab_testing": false,
"versioning": false,
"slug": null,
"for": null,
"fields": [
{
"type": "Text",
"required": false,
"preview": true,
"body": "Name",
"view": "single_line",
"localized": true,
"id": "fd7f062f-84e4-4362-9e15-61c1a26a4078",
"api_id": "name",
"created_on": "2021-03-04T12:03:55+00:00",
"changed_on": "2021-03-04T12:03:55+00:00",
"label": "PublicationModelField",
"title": false,
"seo_title": false,
"seo_description": false,
"description": null,
"disable_editing": false,
"appearance": "single_line",
"accept_restrictions": null,
"no_html": false
},
{
"accept": ["photo"],
"max": "1",
"preview": true,
"type": "Asset",
"min": "1",
"required": true,
"body": "Cover Image",
"localized": true,
"id": "fd81a8e2-2f59-44b7-b3b2-873a27958db1",
"api_id": "cover",
"created_on": "2021-03-04T12:04:13+00:00",
"changed_on": "2021-03-04T12:04:13+00:00",
"label": "PublicationModelField",
"title": false,
"seo_title": false,
"seo_description": false,
"description": null,
"disable_editing": false,
"appearance": null,
"accept_restrictions": null,
"presets_change_required": false
}
]
}
{
"id": "4da8ca3d-86c2-40ba-84c0-5ea949293b9b",
"created_on": "2021-03-04T12:02:44+00:00",
"changed_on": "2021-03-04T12:10:37+00:00",
"body": "Post",
"description": null,
"label": "PublicationModel",
"status": "published",
"stories": false,
"timelines": false,
"allow_stories": false,
"channels": false,
"allow_channels": false,
"container_required": false,
"channels_required": false,
"seo_score": false,
"ab_testing": false,
"versioning": false,
"slug": null,
"for": null,
"fields": [
{
"type": "Text",
"required": false,
"preview": true,
"body": "Title",
"view": "single_line",
"localized": true,
"id": "7aa96451-d3b5-41e9-b012-77f66dd2213b",
"api_id": "title",
"created_on": "2021-03-04T12:02:49+00:00",
"changed_on": "2021-03-04T12:02:49+00:00",
"label": "PublicationModelField",
"title": false,
"seo_title": false,
"seo_description": false,
"description": null,
"disable_editing": false,
"appearance": "single_line",
"accept_restrictions": null,
"no_html": false
},
{
"type": "Text",
"required": false,
"preview": true,
"body": "Summary",
"view": "textarea",
"localized": true,
"id": "ea6a35ca-5a39-4786-9cf7-15e917a0dcf4",
"api_id": "summary",
"created_on": "2021-03-04T12:02:59+00:00",
"changed_on": "2021-03-04T12:02:59+00:00",
"label": "PublicationModelField",
"title": false,
"seo_title": false,
"seo_description": false,
"description": null,
"disable_editing": false,
"appearance": "textarea",
"accept_restrictions": null,
"no_html": false
},
{
"accept": [
"bold",
"italic",
"underline",
"unordered-list",
"ordered-list",
"link",
"table"
],
"preview": true,
"view": "html_editor",
"type": "Text",
"required": false,
"body": "Content",
"localized": true,
"id": "cd31e930-a08c-4740-903e-5c41fcd1c555",
"api_id": "content",
"created_on": "2021-03-04T12:04:36+00:00",
"changed_on": "2021-03-04T12:04:36+00:00",
"label": "PublicationModelField",
"title": false,
"seo_title": false,
"seo_description": false,
"description": null,
"disable_editing": false,
"appearance": "html_editor",
"accept_restrictions": null,
"no_html": false
},
{
"accept": ["photo"],
"max": "1",
"preview": true,
"type": "Asset",
"min": "1",
"required": true,
"body": "Cover Image",
"localized": true,
"id": "cfca83f3-5a73-4567-bde1-4adeb57915d0",
"api_id": "cover",
"created_on": "2021-03-04T12:03:17+00:00",
"changed_on": "2021-03-04T12:03:17+00:00",
"label": "PublicationModelField",
"title": false,
"seo_title": false,
"seo_description": false,
"description": null,
"disable_editing": false,
"appearance": null,
"accept_restrictions": null,
"presets_change_required": false
},
{
"type": "Publication",
"min": "1",
"required": true,
"max": "1",
"preview": false,
"body": "Author",
"localized": true,
"id": "8308380f-af0a-40e7-ae30-33b2f7ce93d9",
"api_id": "author",
"created_on": "2021-03-04T12:04:51+00:00",
"changed_on": "2021-03-04T12:04:51+00:00",
"label": "PublicationModelField",
"title": false,
"seo_title": false,
"seo_description": false,
"description": null,
"disable_editing": false,
"appearance": null,
"accept_restrictions": null
},
{
"type": "Slug",
"description": "The slug is generated automatically. You can change the slug, as long as it is unique.",
"body": "Slug",
"id": "7214e365-1b4c-4de3-8412-e311250fb4da",
"api_id": null,
"created_on": "2021-03-04T12:03:32+00:00",
"changed_on": "2021-03-04T12:03:32+00:00",
"label": "PublicationModelField",
"title": false,
"seo_title": false,
"seo_description": false,
"required": false,
"localized": false,
"preview": false,
"disable_editing": false,
"appearance": null,
"accept_restrictions": null
}
]
}
module.exports = {
images: {
domains: ['b-cdn.net'],
},
}
{
"name": "cms-prepr",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@preprio/nodejs-sdk": "^1.1.0",
"autoprefixer": "10.1.0",
"classnames": "2.2.6",
"date-fns": "2.10.0",
"next": "latest",
"postcss": "8.2.2",
"react": "17.0.1",
"react-dom": "17.0.1"
},
"devDependencies": {
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-preset-env": "^6.7.0",
"tailwindcss": "2.0.2"
},
"license": "MIT"
}
import '../styles/index.css'
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp
import Document, { Html, Head, Main, NextScript } from 'next/document'
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default async function handler(_, res) {
// Exit the current user from "Preview Mode". This function accepts no args.
res.clearPreviewData()
// Redirect the user back to the index page.
res.writeHead(307, { Location: '/' })
res.end()
}
import { getPreviewPostBySlug } from '../../lib/preprio'
export default async function handler(req, res) {
// 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.PREPRIO_PREVIEW_KEY || !req.query.slug) {
return res.status(401).json({ message: 'Invalid token' })
}
// Fetch the headless CMS to check if the provided `slug` exists
const post = await getPreviewPostBySlug(req.query.slug)
// If the slug doesn't exist prevent preview mode from being enabled
if (!post) {
return res.status(401).json({ message: 'Invalid slug' })
}
// Enable Preview Mode by setting the cookies
res.setPreviewData({})
// Redirect to the path from the fetched post
// We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities
res.writeHead(307, { Location: `/posts/${post.slug}` })
res.end()
}
import Container from '../components/container'
import MoreStories from '../components/more-stories'
import HeroPost from '../components/hero-post'
import Intro from '../components/intro'
import Layout from '../components/layout'
import { getAllPostsForHome } from '../lib/preprio'
import Head from 'next/head'
import { CMS_NAME } from '../lib/constants'
export default function Index({ posts, preview }) {
const heroPost = posts[0]
const morePosts = posts.slice(1)
return (
<>
<Layout preview={preview}>
<Head>
<title>Next.js Blog Example with {CMS_NAME}</title>
</Head>
<Container>
<Intro />
{heroPost && (
<HeroPost
title={heroPost.title}
coverImage={heroPost.cover[0].cdn_files[0].url}
date={heroPost.date}
author={heroPost.author[0]}
slug={heroPost._slug}
excerpt={heroPost.summary}
/>
)}
{morePosts.length > 0 && <MoreStories posts={morePosts} />}
</Container>
</Layout>
</>
)
}
export async function getStaticProps({ preview = false }) {
const posts = (await getAllPostsForHome(preview)) || []
return {
props: { posts, preview },
}
}
import { useRouter } from 'next/router'
import ErrorPage from 'next/error'
import Container from 'components/container'
import PostBody from 'components/post-body'
import MoreStories from 'components/more-stories'
import Header from 'components/header'
import PostHeader from 'components/post-header'
import SectionSeparator from 'components/section-separator'
import Layout from 'components/layout'
import { getAllPostsWithSlug, getPostAndMorePosts } from 'lib/preprio'
import PostTitle from 'components/post-title'
import Head from 'next/head'
import { CMS_NAME } from 'lib/constants'
export default function Post({ post, morePosts, preview }) {
const router = useRouter()
if (!router.isFallback && !post?._slug) {
return <ErrorPage statusCode={404} />
}
return (
<Layout preview={preview}>
<Container>
<Header />
{router.isFallback ? (
<PostTitle>Loading</PostTitle>
) : (
<>
<article>
<Head>
<title>
{post.title} | Next.js Blog Example with {CMS_NAME}
</title>
{/* <meta property="og:image" content={post.ogImage.url} /> */}
</Head>
<PostHeader
title={post.title}
coverImage={post.cover[0].cdn_files[0].url}
date={post.date}
author={post.author[0]}
/>
<PostBody content={post.content} />
</article>
<SectionSeparator />
{morePosts.length > 0 && <MoreStories posts={morePosts} />}
</>
)}
</Container>
</Layout>
)
}
export async function getStaticProps({ params, preview = false }) {
const data = await getPostAndMorePosts(params.slug, preview)
return {
props: {
preview,
post: data.Post,
morePosts: data.morePosts.items || [],
},
}
}
export async function getStaticPaths() {
const posts = await getAllPostsWithSlug()
return {
paths: posts.map(({ slug }) => ({
params: { slug },
})),
fallback: true,
}
}
module.exports = {
plugins: [
'tailwindcss',
'postcss-flexbugs-fixes',
[
'postcss-preset-env',
{
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
features: {
'custom-properties': false,
},
},
],
],
}
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/favicons/mstile-150x150.png"/>
<TileColor>#000000</TileColor>
</tile>
</msapplication>
</browserconfig>
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1024.000000pt" height="1024.000000pt" viewBox="0 0 1024.000000 1024.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M4785 10234 c-22 -2 -92 -9 -155 -14 -1453 -131 -2814 -915 -3676
-2120 -480 -670 -787 -1430 -903 -2235 -41 -281 -46 -364 -46 -745 0 -381 5
-464 46 -745 278 -1921 1645 -3535 3499 -4133 332 -107 682 -180 1080 -224
155 -17 825 -17 980 0 687 76 1269 246 1843 539 88 45 105 57 93 67 -8 6 -383
509 -833 1117 l-818 1105 -1025 1517 c-564 834 -1028 1516 -1032 1516 -4 1 -8
-673 -10 -1496 -3 -1441 -4 -1499 -22 -1533 -26 -49 -46 -69 -88 -91 -32 -16
-60 -19 -211 -19 l-173 0 -46 29 c-30 19 -52 44 -67 73 l-21 45 2 2005 3 2006
31 39 c16 21 50 48 74 61 41 20 57 22 230 22 204 0 238 -8 291 -66 15 -16 570
-852 1234 -1859 664 -1007 1572 -2382 2018 -3057 l810 -1227 41 27 c363 236
747 572 1051 922 647 743 1064 1649 1204 2615 41 281 46 364 46 745 0 381 -5
464 -46 745 -278 1921 -1645 3535 -3499 4133 -327 106 -675 179 -1065 223 -96
10 -757 21 -840 13z m2094 -3094 c48 -24 87 -70 101 -118 8 -26 10 -582 8
-1835 l-3 -1798 -317 486 -318 486 0 1307 c0 845 4 1320 10 1343 16 56 51 100
99 126 41 21 56 23 213 23 148 0 174 -2 207 -20z"/>
<path d="M7843 789 c-35 -22 -46 -37 -15 -20 22 13 58 40 52 41 -3 0 -20 -10
-37 -21z"/>
<path d="M7774 744 c-18 -14 -18 -15 4 -4 12 6 22 13 22 15 0 8 -5 6 -26 -11z"/>
<path d="M7724 714 c-18 -14 -18 -15 4 -4 12 6 22 13 22 15 0 8 -5 6 -26 -11z"/>
<path d="M7674 684 c-18 -14 -18 -15 4 -4 12 6 22 13 22 15 0 8 -5 6 -26 -11z"/>
<path d="M7598 644 c-38 -20 -36 -28 2 -9 17 9 30 18 30 20 0 7 -1 6 -32 -11z"/>
</g>
</svg>
{
"name": "Next.js",
"short_name": "Next.js",
"icons": [
{
"src": "/favicons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/favicons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#000000",
"background_color": "#000000",
"display": "standalone"
}
/* purgecss start ignore */
@tailwind base;
@tailwind components;
/* purgecss end ignore */
@tailwind utilities;
module.exports = {
purge: ['./components/**/*.js', './pages/**/*.js'],
theme: {
extend: {
colors: {
'accent-1': '#FAFAFA',
'accent-2': '#EAEAEA',
'accent-7': '#333',
success: '#0070f3',
cyan: '#79FFE1',
},
spacing: {
28: '7rem',
},
letterSpacing: {
tighter: '-.04em',
},
lineHeight: {
tight: 1.2,
},
fontSize: {
'5xl': '2.5rem',
'6xl': '2.75rem',
'7xl': '4.5rem',
'8xl': '6.25rem',
},
boxShadow: {
small: '0 5px 10px rgba(0, 0, 0, 0.12)',
medium: '0 8px 30px rgba(0, 0, 0, 0.12)',
},
},
},
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册