[Examples] Add WordPress Blog (#13194)
* Added most of the stuff * Updated pages * Removed unrequired deps * API fixes * Fixes fixes and updated readme * Updated og image * Added demo and links to example * Updated packages * update name. bump dependencies * Renamed .env.example to .env.local.example * Added node_modules to .gitignore * use recommended config * enable absolute import/alias support * remove jsconfig.json * allow HTML entities in post titles * add underline to content links * add basic ul & ol styles * add code block styles * add basic text alignment * add basic image alignment styles * adjust pre font-size and figcaption * indent ul,ol lists to line up with grid * add basic button styles * add basic file styles * add basic blockquote style * add basic audio styles * add h4 and enhance blockquote styles * add basic cover block styles * add basic verse styles * add basic two-column block styles * add tags * add categories * Only ignore .vercel The rest is injected by create-next-app * now → vercel * npm init → npx * Wordsmith * Wordsmith * Wordsmith * Wordsmith * Improve issue link * Wordsmith Co-authored-by: Greg Rickaby <greg@gregrickaby.com> Co-authored-by: Joe Haddad <joe.haddad@zeit.co> Co-authored-by: Shu Uesugi <shu@chibicode.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
|
@ -9,6 +9,7 @@ description: Next.js has the preview mode for statically generated pages. You ca
|
|||
<details open>
|
||||
<summary><b>Examples</b></summary>
|
||||
<ul>
|
||||
<li><a href="https://github.com/vercel/next.js/tree/canary/examples/cms-wordpress">WordPress Example</a> (<a href="https://next-blog-wordpress.now.sh">Demo</a>)</li>
|
||||
<li><a href="https://github.com/vercel/next.js/tree/canary/examples/cms-datocms">DatoCMS Example</a> (<a href="https://next-blog-datocms.now.sh/">Demo</a>)</li>
|
||||
<li><a href="https://github.com/vercel/next.js/tree/canary/examples/cms-takeshape">TakeShape Example</a> (<a href="https://next-blog-takeshape.now.sh/">Demo</a>)</li>
|
||||
<li><a href="https://github.com/vercel/next.js/tree/canary/examples/cms-sanity">Sanity Example</a> (<a href="https://next-blog-sanity.now.sh/">Demo</a>)</li>
|
||||
|
|
|
@ -9,6 +9,7 @@ description: 'Next.js has 2 pre-rendering modes: Static Generation and Server-si
|
|||
<details open>
|
||||
<summary><b>Examples</b></summary>
|
||||
<ul>
|
||||
<li><a href="https://github.com/vercel/next.js/tree/canary/examples/cms-wordpress">WordPress Example</a> (<a href="https://next-blog-wordpress.now.sh">Demo</a>)</li>
|
||||
<li><a href="https://github.com/vercel/next.js/tree/canary/examples/blog-starter">Blog Starter using markdown files</a> (<a href="https://next-blog-starter.now.sh/">Demo</a>)</li>
|
||||
<li><a href="https://github.com/vercel/next.js/tree/canary/examples/cms-datocms">DatoCMS Example</a> (<a href="https://next-blog-datocms.now.sh/">Demo</a>)</li>
|
||||
<li><a href="https://github.com/vercel/next.js/tree/canary/examples/cms-takeshape">TakeShape Example</a> (<a href="https://next-blog-takeshape.now.sh/">Demo</a>)</li>
|
||||
|
|
|
@ -48,6 +48,7 @@ Finally, you can always use **Client-side Rendering** along with Static Generati
|
|||
<details open>
|
||||
<summary><b>Examples</b></summary>
|
||||
<ul>
|
||||
<li><a href="https://github.com/vercel/next.js/tree/canary/examples/cms-wordpress">WordPress Example</a> (<a href="https://next-blog-wordpress.now.sh">Demo</a>)</li>
|
||||
<li><a href="https://github.com/vercel/next.js/tree/canary/examples/blog-starter">Blog Starter using markdown files</a> (<a href="https://next-blog-starter.now.sh/">Demo</a>)</li>
|
||||
<li><a href="https://github.com/vercel/next.js/tree/canary/examples/cms-datocms">DatoCMS Example</a> (<a href="https://next-blog-datocms.now.sh/">Demo</a>)</li>
|
||||
<li><a href="https://github.com/vercel/next.js/tree/canary/examples/cms-takeshape">TakeShape Example</a> (<a href="https://next-blog-takeshape.now.sh/">Demo</a>)</li>
|
||||
|
|
|
@ -8,11 +8,12 @@ This example showcases Next.js's [Static Generation](/docs/basic-features/pages.
|
|||
|
||||
### Related examples
|
||||
|
||||
- [Blog Starter](/examples/blog-starter)
|
||||
- [WordPress](/examples/cms-wordpress)
|
||||
- [DatoCMS](/examples/cms-datocms)
|
||||
- [Prismic](/examples/cms-prismic)
|
||||
- [TakeShape](/examples/cms-takeshape)
|
||||
- [Sanity](/examples/cms-sanity)
|
||||
- [TakeShape](/examples/cms-takeshape)
|
||||
- [Prismic](/examples/cms-prismic)
|
||||
- [Blog Starter](/examples/blog-starter)
|
||||
|
||||
## How to use
|
||||
|
||||
|
|
|
@ -8,10 +8,12 @@ This example showcases Next.js's [Static Generation](https://nextjs.org/docs/bas
|
|||
|
||||
### Related examples
|
||||
|
||||
- [Blog Starter](/examples/blog-starter)
|
||||
- [WordPress](/examples/cms-wordpress)
|
||||
- [Sanity](/examples/cms-sanity)
|
||||
- [TakeShape](/examples/cms-takeshape)
|
||||
- [Prismic](/examples/cms-prismic)
|
||||
- [Contentful](/examples/cms-contentful)
|
||||
- [Blog Starter](/examples/blog-starter)
|
||||
|
||||
## How to use
|
||||
|
||||
|
|
|
@ -8,11 +8,12 @@ This example showcases Next.js's [Static Generation](/docs/basic-features/pages.
|
|||
|
||||
### Related examples
|
||||
|
||||
- [Blog Starter](/examples/blog-starter)
|
||||
- [WordPress](/examples/cms-wordpress)
|
||||
- [DatoCMS](/examples/cms-datocms)
|
||||
- [TakeShape](/examples/cms-takeshape)
|
||||
- [Sanity](/examples/cms-sanity)
|
||||
- [TakeShape](/examples/cms-takeshape)
|
||||
- [Contentful](/examples/cms-contentful)
|
||||
- [Blog Starter](/examples/blog-starter)
|
||||
|
||||
## How to use
|
||||
|
||||
|
|
|
@ -8,11 +8,12 @@ This example showcases Next.js's [Static Generation](https://nextjs.org/docs/bas
|
|||
|
||||
### Related examples
|
||||
|
||||
- [Blog Starter](/examples/blog-starter)
|
||||
- [WordPress](/examples/cms-wordpress)
|
||||
- [DatoCMS](/examples/cms-datocms)
|
||||
- [TakeShape](/examples/cms-takeshape)
|
||||
- [Prismic](/examples/cms-prismic)
|
||||
- [Contentful](/examples/cms-contentful)
|
||||
- [Blog Starter](/examples/blog-starter)
|
||||
|
||||
## How to use
|
||||
|
||||
|
|
|
@ -8,11 +8,12 @@ This example showcases Next.js's [Static Generation](https://nextjs.org/docs/bas
|
|||
|
||||
### Related examples
|
||||
|
||||
- [Blog Starter](/examples/blog-starter)
|
||||
- [WordPress](/examples/cms-wordpress)
|
||||
- [DatoCMS](/examples/cms-datocms)
|
||||
- [Sanity](/examples/cms-sanity)
|
||||
- [Prismic](/examples/cms-prismic)
|
||||
- [Contentful](/examples/cms-contentful)
|
||||
- [Blog Starter](/examples/blog-starter)
|
||||
|
||||
## How to use
|
||||
|
||||
|
|
5
examples/cms-wordpress/.env.local.example
Normal file
|
@ -0,0 +1,5 @@
|
|||
NEXT_EXAMPLE_CMS_WORDPRESS_API_URL=
|
||||
|
||||
# Only required if you want to enable preview mode
|
||||
# NEXT_EXAMPLE_CMS_WORDPRESS_AUTH_REFRESH_TOKEN=
|
||||
# NEXT_EXAMPLE_CMS_WORDPRESS_PREVIEW_SECRET=
|
1
examples/cms-wordpress/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.vercel
|
206
examples/cms-wordpress/README.md
Normal file
|
@ -0,0 +1,206 @@
|
|||
# A statically generated blog example using Next.js and WordPress
|
||||
|
||||
This example showcases Next.js's [Static Generation](https://nextjs.org/docs/basic-features/pages) feature using [WordPress](https://wordpress.org) as the data source.
|
||||
|
||||
## Demo
|
||||
|
||||
### [https://next-blog-wordpress.now.sh](https://next-blog-wordpress.now.sh)
|
||||
|
||||
### Related examples
|
||||
|
||||
- [DatoCMS](/examples/cms-datocms)
|
||||
- [Sanity](/examples/cms-sanity)
|
||||
- [TakeShape](/examples/cms-takeshape)
|
||||
- [Prismic](/examples/cms-prismic)
|
||||
- [Contentful](/examples/cms-contentful)
|
||||
- [Blog Starter](/examples/blog-starter)
|
||||
|
||||
## How to use
|
||||
|
||||
### Using `create-next-app`
|
||||
|
||||
Execute [`create-next-app`](https://github.com/zeit/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-wordpress cms-wordpress-app
|
||||
# or
|
||||
yarn create next-app --example cms-wordpress cms-wordpress-app
|
||||
```
|
||||
|
||||
### Download manually
|
||||
|
||||
Download the example:
|
||||
|
||||
```bash
|
||||
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/cms-wordpress
|
||||
cd cms-wordpress
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Step 1. Prepare your WordPress site
|
||||
|
||||
First, you need a WordPress site. There are many solutions for WordPress hosting, such as [WP Engine](https://wpengine.com/) and [WordPress.com](https://wordpress.com/).
|
||||
|
||||
Once the site is ready, you'll need to install the [WPGraphQL](https://www.wpgraphql.com/) plugin. It will add GraphQL API to your WordPress site, which we'll use to query the posts. Follow these steps to install it:
|
||||
|
||||
- Download the [WPGraphQL repo](https://github.com/wp-graphql/wp-graphql) as a ZIP archive.
|
||||
- Inside your WordPress admin, go to **Plugins** and then click **Add New**.
|
||||
|
||||
![Add new plugin](./docs/plugins-add-new.png)
|
||||
|
||||
- Click the **Upload Plugin** button at the top of the page and upload the WPGraphQL plugin.
|
||||
|
||||
![Upload new plugin](./docs/plugins-upload-new.png)
|
||||
|
||||
- Once the plugin has been added, activate it from either the **Activate Plugin** button displayed after uploading or from the **Plugins** page.
|
||||
|
||||
![WPGraphQL installed](./docs/plugin-installed.png)
|
||||
|
||||
#### Optional: Add WPGraphiQL
|
||||
|
||||
The [WPGraphiQL](https://github.com/wp-graphql/wp-graphiql) plugin gives you access to a GraphQL IDE directly from your WordPress Admin, allowing you to inspect and play around with the GraphQL API.
|
||||
|
||||
The process to add WPGraphiQL is the same as the one for WPGraphQL: Go to the [WPGraphiQL repo](https://github.com/wp-graphql/wp-graphiql), download the ZIP archive, and install it as a plugin in your WordPress site. Once that's done you should be able to access the GraphiQL page in the admin:
|
||||
|
||||
![WPGraphiQL page](./docs/wp-graphiql.png)
|
||||
|
||||
### Step 2. Populate Content
|
||||
|
||||
Inside your WordPress admin, go to **Posts** and start adding new posts:
|
||||
|
||||
- We recommend creating at least **2 posts**
|
||||
- Use dummy data for the content
|
||||
- Pick an author from your WordPress users
|
||||
- Add a **Featured Image**. You can download one from [Unsplash](https://unsplash.com/)
|
||||
- Fill the **Excerpt** field
|
||||
|
||||
![New post](./docs/new-post.png)
|
||||
|
||||
When you’re done, make sure to **Publish** the posts.
|
||||
|
||||
> **Note:** Only **published** posts and public fields will be rendered by the app unless [Preview Mode](/docs/advanced-features/preview-mode.md) is enabled.
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
Then open `.env.local` and set `NEXT_EXAMPLE_CMS_WORDPRESS_API_URL` to be the URL to your GraphQL endpoint in WordPress. For example: `https://myapp.wpengine.com/graphql`.
|
||||
|
||||
Your `.env.local` file should look like this:
|
||||
|
||||
```bash
|
||||
NEXT_EXAMPLE_CMS_WORDPRESS_API_URL=...
|
||||
|
||||
# Only required if you want to enable preview mode
|
||||
# NEXT_EXAMPLE_CMS_WORDPRESS_AUTH_REFRESH_TOKEN=
|
||||
# NEXT_EXAMPLE_CMS_WORDPRESS_PREVIEW_SECRET=
|
||||
```
|
||||
|
||||
### 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/zeit/next.js/discussions).
|
||||
|
||||
### Step 5. Add authentication for Preview Mode (Optional)
|
||||
|
||||
**This step is optional.** By default, the blog will work with public posts from your WordPress site. Private content such as unpublished posts and private fields cannot be retrieved. To have access to unpublished posts you'll need to set up authentication.
|
||||
|
||||
To add [authentication to WPGraphQL](https://docs.wpgraphql.com/guides/authentication-and-authorization/), first you need to add the [WPGraphQL JWT plugin](https://github.com/wp-graphql/wp-graphql-jwt-authentication) to your Wordpress Admin following the same process you used to add the WPGraphQL plugin.
|
||||
|
||||
> Adding the WPGraphQL JWT plugin will disable your GraphQL API until you add a JWT secret ([GitHub issue](https://github.com/wp-graphql/wp-graphql-jwt-authentication/issues/91)).
|
||||
|
||||
Once that's done, you'll need to access the WordPress filesystem to add the secret required to validate JWT tokens. We recommend using SFTP — the instructions vary depending on your hosting provider. For example:
|
||||
|
||||
- [SFTP guide for WP Engine](https://wpengine.com/support/sftp/)
|
||||
- [SFTP guide for WordPress.com](https://wordpress.com/support/sftp/)
|
||||
|
||||
Once you have SFTP access, open `wp-config.php` and add a secret for your JWT:
|
||||
|
||||
```php
|
||||
define( 'GRAPHQL_JWT_AUTH_SECRET_KEY', 'YOUR_STRONG_SECRET' );
|
||||
```
|
||||
|
||||
> You can read more about this in the documentation for [WPGraphQL JWT Authentication](https://docs.wpgraphql.com/extensions/wpgraphql-jwt-authentication/).
|
||||
|
||||
Now, you need to get a **refresh token** to make authenticated requests with GraphQL. Make the following GraphQL mutation to your WordPress site from the GraphQL IDE (See notes about WPGraphiQL from earlier). Replace `your_username` with the **username** of a user with the `Administrator` role, and `your_password` with the user's password.
|
||||
|
||||
```graphql
|
||||
mutation Login {
|
||||
login(
|
||||
input: {
|
||||
clientMutationId: "uniqueId"
|
||||
password: "your_password"
|
||||
username: "your_username"
|
||||
}
|
||||
) {
|
||||
refreshToken
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Copy the `refreshToken` returned by the mutation, then open `.env.local`, and make the following changes:
|
||||
|
||||
- Uncomment `NEXT_EXAMPLE_CMS_WORDPRESS_AUTH_REFRESH_TOKEN` and set it to be the `refreshToken` you just received.
|
||||
- Uncomment `NEXT_EXAMPLE_CMS_WORDPRESS_PREVIEW_SECRET` and set it to be any random string (ideally URL friendly).
|
||||
|
||||
Your `.env.local` file should look like this:
|
||||
|
||||
```bash
|
||||
NEXT_EXAMPLE_CMS_WORDPRESS_API_URL=...
|
||||
|
||||
# Only required if you want to enable preview mode
|
||||
NEXT_EXAMPLE_CMS_WORDPRESS_AUTH_REFRESH_TOKEN=...
|
||||
NEXT_EXAMPLE_CMS_WORDPRESS_PREVIEW_SECRET=...
|
||||
```
|
||||
|
||||
**Important:** Restart your Next.js server to update the environment variables.
|
||||
|
||||
### Step 6. Try preview mode
|
||||
|
||||
On your WordPress admin, create a new post like before, but **do not publish** it.
|
||||
|
||||
Now, if you go to `http://localhost:3000`, you won’t see the post. However, if you enable **Preview Mode**, you'll be able to see the change ([Documentation](/docs/advanced-features/preview-mode.md)).
|
||||
|
||||
To enable Preview Mode, go to this URL:
|
||||
|
||||
```
|
||||
http://localhost:3000/api/preview?secret=<secret>&id=<id>
|
||||
```
|
||||
|
||||
- `<secret>` should be the string you entered for `NEXT_EXAMPLE_CMS_WORDPRESS_PREVIEW_SECRET`.
|
||||
- `<id>` should be the post's `databaseId` field, which is the integer that you usually see in the URL (`?post=18` → 18).
|
||||
- Alternatively, you can use `<slug>` instead of `<id>`. `<slug>` is generated based on the title.
|
||||
|
||||
You should now be able to see this post. To exit Preview Mode, you can click on **Click here to exit preview mode** at the top.
|
||||
|
||||
### Step 7. Deploy on Vercel
|
||||
|
||||
You can deploy this app to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
|
||||
|
||||
To deploy on Vercel, you need to set the environment variables with **Vercel Secrets** using [Vercel CLI](https://vercel.com/download) ([Documentation](https://vercel.com/docs/cli#commands/secrets)).
|
||||
|
||||
Install [Vercel CLI](https://vercel.com/download), log in to your account from the CLI, and run the following commands to add the environment variables. Replace the values with the corresponding strings in `.env.local`:
|
||||
|
||||
```bash
|
||||
vercel secrets add next_example_cms_wordpress_api_url <NEXT_EXAMPLE_CMS_WORDPRESS_API_URL>
|
||||
|
||||
# If you added authentication for preview mode:
|
||||
vercel secrets add next_example_cms_wordpress_auth_refresh_token <NEXT_EXAMPLE_CMS_WORDPRESS_AUTH_REFRESH_TOKEN>
|
||||
vercel secrets add next_example_cms_wordpress_preview_secret <NEXT_EXAMPLE_CMS_WORDPRESS_PREVIEW_SECRET>
|
||||
```
|
||||
|
||||
Then push the project to GitHub/GitLab/Bitbucket and [import to Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) to deploy.
|
42
examples/cms-wordpress/components/alert.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
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 is 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/zeit/next.js/tree/canary/examples/${EXAMPLE_PATH}`}
|
||||
className="underline hover:text-success duration-200 transition-colors"
|
||||
>
|
||||
available on GitHub
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
17
examples/cms-wordpress/components/avatar.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
export default function Avatar({ author }) {
|
||||
const name =
|
||||
author.firstName && author.lastName
|
||||
? `${author.firstName} ${author.lastName}`
|
||||
: author.name
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={author.avatar.url}
|
||||
className="w-12 h-12 rounded-full mr-4"
|
||||
alt={name}
|
||||
/>
|
||||
<div className="text-xl font-bold">{name}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
16
examples/cms-wordpress/components/categories.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
export default function Categories({ categories }) {
|
||||
return (
|
||||
<span className="ml-1">
|
||||
under
|
||||
{categories.edges.length > 0 ? (
|
||||
categories.edges.map((category, index) => (
|
||||
<span key={index} className="ml-1">
|
||||
{category.node.name}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="ml-1">{categories.edges.node.name}</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
3
examples/cms-wordpress/components/container.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Container({ children }) {
|
||||
return <div className="container mx-auto px-5">{children}</div>
|
||||
}
|
24
examples/cms-wordpress/components/cover-image.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import cn from 'classnames'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function CoverImage({ title, coverImage, slug }) {
|
||||
const image = (
|
||||
<img
|
||||
src={coverImage?.sourceUrl}
|
||||
className={cn('shadow-small', {
|
||||
'hover:shadow-medium transition-shadow duration-200': slug,
|
||||
})}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<div className="-mx-5 sm:mx-0">
|
||||
{slug ? (
|
||||
<Link as={`/posts/${slug}`} href="/posts/[slug]">
|
||||
<a aria-label={title}>{image}</a>
|
||||
</Link>
|
||||
) : (
|
||||
image
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
6
examples/cms-wordpress/components/date.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
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>
|
||||
}
|
30
examples/cms-wordpress/components/footer.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
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/zeit/next.js/tree/canary/examples/${EXAMPLE_PATH}`}
|
||||
className="mx-3 font-bold hover:underline"
|
||||
>
|
||||
View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</footer>
|
||||
)
|
||||
}
|
12
examples/cms-wordpress/components/header.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
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>
|
||||
)
|
||||
}
|
43
examples/cms-wordpress/components/hero-post.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
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 title={title} coverImage={coverImage} slug={slug} />
|
||||
</div>
|
||||
<div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28">
|
||||
<div>
|
||||
<h3 className="mb-4 text-4xl lg:text-6xl leading-tight">
|
||||
<Link as={`/posts/${slug}`} href="/posts/[slug]">
|
||||
<a
|
||||
className="hover:underline"
|
||||
dangerouslySetInnerHTML={{ __html: title }}
|
||||
/>
|
||||
</Link>
|
||||
</h3>
|
||||
<div className="mb-4 md:mb-0 text-lg">
|
||||
<Date dateString={date} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="text-lg leading-relaxed mb-4"
|
||||
dangerouslySetInnerHTML={{ __html: excerpt }}
|
||||
/>
|
||||
<Avatar author={author} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
28
examples/cms-wordpress/components/intro.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
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>
|
||||
)
|
||||
}
|
16
examples/cms-wordpress/components/layout.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
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 />
|
||||
</>
|
||||
)
|
||||
}
|
42
examples/cms-wordpress/components/meta.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
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>
|
||||
)
|
||||
}
|
24
examples/cms-wordpress/components/more-stories.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
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:col-gap-16 lg:col-gap-32 row-gap-20 md:row-gap-32 mb-32">
|
||||
{posts.map(({ node }) => (
|
||||
<PostPreview
|
||||
key={node.slug}
|
||||
title={node.title}
|
||||
coverImage={node.featuredImage}
|
||||
date={node.date}
|
||||
author={node.author}
|
||||
slug={node.slug}
|
||||
excerpt={node.excerpt}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
12
examples/cms-wordpress/components/post-body.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import styles from './post-body.module.css'
|
||||
|
||||
export default function PostBody({ content }) {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div
|
||||
className={styles.content}
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
76
examples/cms-wordpress/components/post-body.module.css
Normal file
|
@ -0,0 +1,76 @@
|
|||
.content {
|
||||
@apply text-lg leading-relaxed;
|
||||
}
|
||||
|
||||
.content p,
|
||||
.content ul,
|
||||
.content ol,
|
||||
.content blockquote {
|
||||
@apply my-6;
|
||||
}
|
||||
|
||||
.content a {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
.content ul,
|
||||
.content ol {
|
||||
@apply pl-4;
|
||||
}
|
||||
|
||||
.content ul {
|
||||
@apply list-disc;
|
||||
}
|
||||
|
||||
.content ol {
|
||||
@apply list-decimal;
|
||||
}
|
||||
|
||||
.content ul > li > ul,
|
||||
.content ol > li > ol {
|
||||
@apply my-0 ml-4;
|
||||
}
|
||||
|
||||
.content ul > li > ul {
|
||||
list-style: circle;
|
||||
}
|
||||
|
||||
.content h2 {
|
||||
@apply text-3xl mt-12 mb-4 leading-snug;
|
||||
}
|
||||
|
||||
.content h3 {
|
||||
@apply text-2xl mt-8 mb-4 leading-snug;
|
||||
}
|
||||
|
||||
.content h4 {
|
||||
@apply text-xl mt-6 mb-4 leading-snug;
|
||||
}
|
||||
|
||||
.content pre {
|
||||
@apply whitespace-pre overflow-x-auto p-4 text-sm leading-tight border border-gray-400 bg-gray-100;
|
||||
}
|
||||
|
||||
.content code {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
.content figcaption {
|
||||
@apply text-center text-sm;
|
||||
}
|
||||
|
||||
.content blockquote {
|
||||
@apply border-l-4 border-gray-500 bg-gray-200 italic ml-0 py-4 px-6;
|
||||
}
|
||||
|
||||
.content blockquote p {
|
||||
@apply mt-0;
|
||||
}
|
||||
|
||||
.content blockquote cite {
|
||||
@apply not-italic;
|
||||
}
|
||||
|
||||
.content audio {
|
||||
@apply w-full;
|
||||
}
|
34
examples/cms-wordpress/components/post-header.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import Avatar from '../components/avatar'
|
||||
import Date from '../components/date'
|
||||
import CoverImage from '../components/cover-image'
|
||||
import PostTitle from '../components/post-title'
|
||||
import Categories from '../components/categories'
|
||||
|
||||
export default function PostHeader({
|
||||
title,
|
||||
coverImage,
|
||||
date,
|
||||
author,
|
||||
categories,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<PostTitle>{title}</PostTitle>
|
||||
<div className="hidden md:block md:mb-12">
|
||||
<Avatar author={author} />
|
||||
</div>
|
||||
<div className="mb-8 md:mb-16 -mx-5 sm:mx-0">
|
||||
<CoverImage title={title} coverImage={coverImage} />
|
||||
</div>
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="block md:hidden mb-6">
|
||||
<Avatar author={author} />
|
||||
</div>
|
||||
<div className="mb-6 text-lg">
|
||||
Posted <Date dateString={date} />
|
||||
<Categories categories={categories} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
37
examples/cms-wordpress/components/post-preview.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
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 title={title} coverImage={coverImage} slug={slug} />
|
||||
</div>
|
||||
<h3 className="text-3xl mb-3 leading-snug">
|
||||
<Link as={`/posts/${slug}`} href="/posts/[slug]">
|
||||
<a
|
||||
className="hover:underline"
|
||||
dangerouslySetInnerHTML={{ __html: title }}
|
||||
></a>
|
||||
</Link>
|
||||
</h3>
|
||||
<div className="text-lg mb-4">
|
||||
<Date dateString={date} />
|
||||
</div>
|
||||
<div
|
||||
className="text-lg leading-relaxed mb-4"
|
||||
dangerouslySetInnerHTML={{ __html: excerpt }}
|
||||
/>
|
||||
<Avatar author={author} />
|
||||
</div>
|
||||
)
|
||||
}
|
8
examples/cms-wordpress/components/post-title.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
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"
|
||||
dangerouslySetInnerHTML={{ __html: children }}
|
||||
/>
|
||||
)
|
||||
}
|
3
examples/cms-wordpress/components/section-separator.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function SectionSeparator() {
|
||||
return <hr className="border-accent-2 mt-28 mb-24" />
|
||||
}
|
14
examples/cms-wordpress/components/tags.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
export default function Tags({ tags }) {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<p className="mt-8 text-lg font-bold">
|
||||
Tagged
|
||||
{tags.edges.map((tag, index) => (
|
||||
<span key={index} className="ml-4 font-normal">
|
||||
{tag.node.name}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
BIN
examples/cms-wordpress/docs/new-post.png
Normal file
After Width: | Height: | Size: 358 KiB |
BIN
examples/cms-wordpress/docs/plugin-installed.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
examples/cms-wordpress/docs/plugins-add-new.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
examples/cms-wordpress/docs/plugins-upload-new.png
Normal file
After Width: | Height: | Size: 201 KiB |
BIN
examples/cms-wordpress/docs/wp-graphiql.png
Normal file
After Width: | Height: | Size: 221 KiB |
201
examples/cms-wordpress/lib/api.js
Normal file
|
@ -0,0 +1,201 @@
|
|||
const API_URL = process.env.NEXT_EXAMPLE_CMS_WORDPRESS_API_URL
|
||||
|
||||
async function fetchAPI(query, { variables } = {}) {
|
||||
const headers = { 'Content-Type': 'application/json' }
|
||||
|
||||
if (process.env.NEXT_EXAMPLE_CMS_WORDPRESS_AUTH_REFRESH_TOKEN) {
|
||||
headers[
|
||||
'Authorization'
|
||||
] = `Bearer ${process.env.NEXT_EXAMPLE_CMS_WORDPRESS_AUTH_REFRESH_TOKEN}`
|
||||
}
|
||||
|
||||
const res = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
variables,
|
||||
}),
|
||||
})
|
||||
|
||||
const json = await res.json()
|
||||
if (json.errors) {
|
||||
console.error(json.errors)
|
||||
throw new Error('Failed to fetch API')
|
||||
}
|
||||
return json.data
|
||||
}
|
||||
|
||||
export async function getPreviewPost(id, idType = 'DATABASE_ID') {
|
||||
const data = await fetchAPI(
|
||||
`
|
||||
query PreviewPost($id: ID!, $idType: PostIdType!) {
|
||||
post(id: $id, idType: $idType) {
|
||||
databaseId
|
||||
slug
|
||||
status
|
||||
}
|
||||
}`,
|
||||
{
|
||||
variables: { id, idType },
|
||||
}
|
||||
)
|
||||
return data.post
|
||||
}
|
||||
|
||||
export async function getAllPostsWithSlug() {
|
||||
const data = await fetchAPI(`
|
||||
{
|
||||
posts(first: 10000) {
|
||||
edges {
|
||||
node {
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
return data?.posts
|
||||
}
|
||||
|
||||
export async function getAllPostsForHome(preview) {
|
||||
const data = await fetchAPI(
|
||||
`
|
||||
query AllPosts {
|
||||
posts(first: 20, where: { orderby: { field: DATE, order: DESC } }) {
|
||||
edges {
|
||||
node {
|
||||
title
|
||||
excerpt
|
||||
slug
|
||||
date
|
||||
featuredImage {
|
||||
sourceUrl
|
||||
}
|
||||
author {
|
||||
name
|
||||
firstName
|
||||
lastName
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
variables: {
|
||||
onlyEnabled: !preview,
|
||||
preview,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return data?.posts
|
||||
}
|
||||
|
||||
export async function getPostAndMorePosts(slug, preview, previewData) {
|
||||
const postPreview = preview && previewData?.post
|
||||
// The slug may be the id of an unpublished post
|
||||
const isId = Number.isInteger(Number(slug))
|
||||
const isSamePost = isId
|
||||
? Number(slug) === postPreview.id
|
||||
: slug === postPreview.slug
|
||||
const isDraft = isSamePost && postPreview?.status === 'draft'
|
||||
const isRevision = isSamePost && postPreview?.status === 'publish'
|
||||
const data = await fetchAPI(
|
||||
`
|
||||
fragment AuthorFields on User {
|
||||
name
|
||||
firstName
|
||||
lastName
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
fragment PostFields on Post {
|
||||
title
|
||||
excerpt
|
||||
slug
|
||||
date
|
||||
featuredImage {
|
||||
sourceUrl
|
||||
}
|
||||
author {
|
||||
...AuthorFields
|
||||
}
|
||||
categories {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
tags {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
query PostBySlug($id: ID!, $idType: PostIdType!) {
|
||||
post(id: $id, idType: $idType) {
|
||||
...PostFields
|
||||
content
|
||||
${
|
||||
// Only some of the fields of a revision are considered as there are some inconsistencies
|
||||
isRevision
|
||||
? `
|
||||
revisions(first: 1, where: { orderby: { field: MODIFIED, order: ASC } }) {
|
||||
edges {
|
||||
node {
|
||||
title
|
||||
excerpt
|
||||
content
|
||||
author {
|
||||
...AuthorFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
: ''
|
||||
}
|
||||
}
|
||||
posts(first: 3, where: { orderby: { field: DATE, order: DESC } }) {
|
||||
edges {
|
||||
node {
|
||||
...PostFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
variables: {
|
||||
id: isDraft ? postPreview.id : slug,
|
||||
idType: isDraft ? 'DATABASE_ID' : 'SLUG',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Draft posts may not have an slug
|
||||
if (isDraft) data.post.slug = postPreview.id
|
||||
// Apply a revision (changes in a published post)
|
||||
if (isRevision && data.post.revisions) {
|
||||
const revision = data.post.revisions.edges[0]?.node
|
||||
|
||||
if (revision) Object.assign(data.post, revision)
|
||||
delete data.post.revisions
|
||||
}
|
||||
|
||||
// Filter out the main post
|
||||
data.posts.edges = data.posts.edges.filter(({ node }) => node.slug !== slug)
|
||||
// If there are still 3 posts, remove the last one
|
||||
if (data.posts.edges.length > 2) data.posts.edges.pop()
|
||||
|
||||
return data
|
||||
}
|
5
examples/cms-wordpress/lib/constants.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export const EXAMPLE_PATH = 'cms-wordpress'
|
||||
export const CMS_NAME = 'WordPress'
|
||||
export const CMS_URL = 'https://wordpress.org'
|
||||
export const HOME_OG_IMAGE_URL =
|
||||
'https://og-image.now.sh/Next.js%20Blog%20Example%20with%20**WordPress**.png?theme=light&md=1&fontSize=75px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg&images=data%3Aimage%2Fsvg%2Bxml%2C%253C%253Fxml+version%3D%271.0%27+encoding%3D%27UTF-8%27%253F%253E%253Csvg+preserveAspectRatio%3D%27xMidYMid%27+version%3D%271.1%27+viewBox%3D%270+0+256+255%27+xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%253E%253Cg+fill%3D%27%2523464342%27%253E%253Cpath+d%3D%27m18.124+127.5c0+43.295+25.161+80.711+61.646+98.441l-52.176-142.96c-6.0691+13.603-9.4699+28.657-9.4699+44.515zm183.22-5.5196c0-13.518-4.8557-22.88-9.0204-30.166-5.5446-9.01-10.742-16.64-10.742-25.65+0-10.055+7.6259-19.414+18.367-19.414+0.48494+0+0.94491+0.060358+1.4174+0.087415-19.46-17.828-45.387-28.714-73.863-28.714-38.213+0-71.832+19.606-91.39+49.302+2.5662+0.077008+4.9847+0.13112+7.039+0.13112+11.441+0+29.151-1.3882+29.151-1.3882+5.8963-0.34758+6.5915+8.3127+0.7014+9.01+0+0-5.9255+0.69724-12.519+1.0427l39.832+118.48+23.937-71.79-17.042-46.692c-5.8901-0.3455-11.47-1.0427-11.47-1.0427-5.8942-0.3455-5.2033-9.3575+0.69099-9.01+0+0+18.064+1.3882+28.811+1.3882+11.439+0+29.151-1.3882+29.151-1.3882+5.9005-0.34758+6.5936+8.3127+0.7014+9.01+0+0-5.938+0.69724-12.519+1.0427l39.528+117.58+10.91-36.458c4.7287-15.129+8.3273-25.995+8.3273-35.359zm-71.921+15.087l-32.818+95.363c9.7988+2.8805+20.162+4.4561+30.899+4.4561+12.738+0+24.953-2.202+36.323-6.2002-0.29346-0.46829-0.55987-0.96572-0.77841-1.5069l-33.625-92.112zm94.058-62.046c0.47037+3.4841+0.73678+7.2242+0.73678+11.247+0+11.1-2.073+23.577-8.3169+39.178l-33.411+96.599c32.518-18.963+54.391-54.193+54.391-94.545+0.002081-19.017-4.8557-36.899-13.399-52.48zm-95.977-75.023c-70.304+0-127.5+57.196-127.5+127.5+0+70.313+57.2+127.51+127.5+127.51+70.302+0+127.51-57.194+127.51-127.51-0.002082-70.304-57.209-127.5-127.51-127.5zm0+249.16c-67.08+0-121.66-54.578-121.66-121.66+0-67.08+54.576-121.65+121.66-121.65+67.078+0+121.65+54.574+121.65+121.65+0+67.084-54.574+121.66-121.65+121.66z%27%2F%253E%253C%2Fg%253E%253C%2Fsvg%253E'
|
21
examples/cms-wordpress/package.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "cms-wordpress",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "2.2.6",
|
||||
"date-fns": "2.14.0",
|
||||
"next": "latest",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"postcss-flexbugs-fixes": "4.2.1",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"tailwindcss": "^1.4.6"
|
||||
}
|
||||
}
|
7
examples/cms-wordpress/pages/_app.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import '../styles/index.css'
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return <Component {...pageProps} />
|
||||
}
|
||||
|
||||
export default MyApp
|
15
examples/cms-wordpress/pages/_document.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
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>
|
||||
)
|
||||
}
|
||||
}
|
8
examples/cms-wordpress/pages/api/exit-preview.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default async function exit(_, 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()
|
||||
}
|
37
examples/cms-wordpress/pages/api/preview.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { getPreviewPost } from '../../lib/api'
|
||||
|
||||
export default async function preview(req, res) {
|
||||
const { secret, id, slug } = req.query
|
||||
|
||||
// Check the secret and next parameters
|
||||
// This secret should only be known by this API route
|
||||
if (
|
||||
!process.env.NEXT_EXAMPLE_CMS_WORDPRESS_PREVIEW_SECRET ||
|
||||
secret !== process.env.NEXT_EXAMPLE_CMS_WORDPRESS_PREVIEW_SECRET ||
|
||||
(!id && !slug)
|
||||
) {
|
||||
return res.status(401).json({ message: 'Invalid token' })
|
||||
}
|
||||
|
||||
// Fetch WordPress to check if the provided `id` or `slug` exists
|
||||
const post = await getPreviewPost(id || slug, id ? 'DATABASE_ID' : 'SLUG')
|
||||
|
||||
// If the post doesn't exist prevent preview mode from being enabled
|
||||
if (!post) {
|
||||
return res.status(401).json({ message: 'Post not found' })
|
||||
}
|
||||
|
||||
// Enable Preview Mode by setting the cookies
|
||||
res.setPreviewData({
|
||||
post: {
|
||||
id: post.databaseId,
|
||||
slug: post.slug,
|
||||
status: post.status,
|
||||
},
|
||||
})
|
||||
|
||||
// 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 || post.databaseId}` })
|
||||
res.end()
|
||||
}
|
44
examples/cms-wordpress/pages/index.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import Head from 'next/head'
|
||||
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/api'
|
||||
import { CMS_NAME } from '../lib/constants'
|
||||
|
||||
export default function Index({ allPosts: { edges }, preview }) {
|
||||
const heroPost = edges[0]?.node
|
||||
const morePosts = edges.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.featuredImage}
|
||||
date={heroPost.date}
|
||||
author={heroPost.author}
|
||||
slug={heroPost.slug}
|
||||
excerpt={heroPost.excerpt}
|
||||
/>
|
||||
)}
|
||||
{morePosts.length > 0 && <MoreStories posts={morePosts} />}
|
||||
</Container>
|
||||
</Layout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getStaticProps({ preview = false }) {
|
||||
const allPosts = await getAllPostsForHome(preview)
|
||||
return {
|
||||
props: { allPosts, preview },
|
||||
}
|
||||
}
|
83
examples/cms-wordpress/pages/posts/[slug].js
Normal file
|
@ -0,0 +1,83 @@
|
|||
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/api'
|
||||
import PostTitle from '../../components/post-title'
|
||||
import Head from 'next/head'
|
||||
import { CMS_NAME } from '../../lib/constants'
|
||||
import Tags from '../../components/tags'
|
||||
|
||||
export default function Post({ post, posts, preview }) {
|
||||
const router = useRouter()
|
||||
const morePosts = posts?.edges
|
||||
|
||||
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.featuredImage?.sourceUrl}
|
||||
/>
|
||||
</Head>
|
||||
<PostHeader
|
||||
title={post.title}
|
||||
coverImage={post.featuredImage}
|
||||
date={post.date}
|
||||
author={post.author}
|
||||
categories={post.categories}
|
||||
/>
|
||||
<PostBody content={post.content} />
|
||||
<footer>
|
||||
{post.tags.edges.length > 0 && <Tags tags={post.tags} />}
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<SectionSeparator />
|
||||
{morePosts.length > 0 && <MoreStories posts={morePosts} />}
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params, preview = false, previewData }) {
|
||||
const data = await getPostAndMorePosts(params.slug, preview, previewData)
|
||||
|
||||
return {
|
||||
props: {
|
||||
preview,
|
||||
post: data.post,
|
||||
posts: data.posts,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const allPosts = await getAllPostsWithSlug()
|
||||
|
||||
return {
|
||||
paths: allPosts.edges.map(({ node }) => `/posts/${node.slug}`) || [],
|
||||
fallback: true,
|
||||
}
|
||||
}
|
18
examples/cms-wordpress/postcss.config.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
module.exports = {
|
||||
plugins: [
|
||||
'tailwindcss',
|
||||
'postcss-flexbugs-fixes',
|
||||
[
|
||||
'postcss-preset-env',
|
||||
{
|
||||
autoprefixer: {
|
||||
flexbox: 'no-2009',
|
||||
},
|
||||
stage: 3,
|
||||
features: {
|
||||
'custom-properties': false,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
BIN
examples/cms-wordpress/public/favicon/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
examples/cms-wordpress/public/favicon/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
examples/cms-wordpress/public/favicon/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
9
examples/cms-wordpress/public/favicon/browserconfig.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/favicons/mstile-150x150.png"/>
|
||||
<TileColor>#000000</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
BIN
examples/cms-wordpress/public/favicon/favicon-16x16.png
Normal file
After Width: | Height: | Size: 595 B |
BIN
examples/cms-wordpress/public/favicon/favicon-32x32.png
Normal file
After Width: | Height: | Size: 880 B |
BIN
examples/cms-wordpress/public/favicon/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
examples/cms-wordpress/public/favicon/mstile-150x150.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
33
examples/cms-wordpress/public/favicon/safari-pinned-tab.svg
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?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>
|
After Width: | Height: | Size: 1.9 KiB |
19
examples/cms-wordpress/public/favicon/site.webmanifest
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"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"
|
||||
}
|
104
examples/cms-wordpress/styles/index.css
Normal file
|
@ -0,0 +1,104 @@
|
|||
@tailwind base;
|
||||
|
||||
/* Write your own custom base styles here */
|
||||
|
||||
/* Start purging... */
|
||||
@tailwind components;
|
||||
/* Stop purging. */
|
||||
|
||||
/* Write you own custom component styles here */
|
||||
|
||||
/* Start purging... */
|
||||
@tailwind utilities;
|
||||
/* Stop purging. */
|
||||
|
||||
/* Your own custom utilities */
|
||||
|
||||
.has-text-align-left {
|
||||
@apply text-left;
|
||||
}
|
||||
|
||||
.has-text-align-center {
|
||||
@apply text-center;
|
||||
}
|
||||
|
||||
.has-text-align-right {
|
||||
@apply text-right;
|
||||
}
|
||||
|
||||
.has-large-font-size {
|
||||
@apply text-4xl;
|
||||
}
|
||||
|
||||
.alignfull {
|
||||
@apply w-screen relative;
|
||||
|
||||
left: 50%;
|
||||
margin-left: -50vw;
|
||||
margin-right: -50vw;
|
||||
max-width: 100vw;
|
||||
right: 50%;
|
||||
}
|
||||
|
||||
.wp-block-image img {
|
||||
@apply max-w-full mt-2;
|
||||
}
|
||||
.wp-block-image.aligncenter {
|
||||
@apply text-center;
|
||||
}
|
||||
.wp-block-image.alignfull img,
|
||||
.wp-block-image.alignwide img {
|
||||
@apply w-full;
|
||||
}
|
||||
.wp-block-image .alignleft,
|
||||
.wp-block-image .alignright,
|
||||
.wp-block-image .aligncenter,
|
||||
.wp-block-image.is-resized {
|
||||
@apply table ml-0 mr-0;
|
||||
}
|
||||
.wp-block-image .alignleft > figcaption,
|
||||
.wp-block-image .alignright > figcaption,
|
||||
.wp-block-image .aligncenter > figcaption,
|
||||
.wp-block-image.is-resized > figcaption {
|
||||
@apply table-caption;
|
||||
|
||||
caption-side: bottom;
|
||||
}
|
||||
|
||||
.wp-block-image .alignleft {
|
||||
@apply float-left mr-4;
|
||||
}
|
||||
.wp-block-image .alignright {
|
||||
@apply float-right ml-4;
|
||||
}
|
||||
.wp-block-image .aligncenter {
|
||||
@apply m-auto;
|
||||
}
|
||||
|
||||
.wp-block-button a,
|
||||
.wp-block-file a.wp-block-file__button {
|
||||
@apply bg-blue-500 text-white no-underline py-2 px-4;
|
||||
}
|
||||
|
||||
.wp-block-button a:hover,
|
||||
.wp-block-file a.wp-block-file__button:hover {
|
||||
@apply bg-blue-400 cursor-pointer;
|
||||
}
|
||||
|
||||
.wp-block-file a:first-of-type {
|
||||
@apply mr-4;
|
||||
}
|
||||
|
||||
.wp-block-cover {
|
||||
@apply flex flex-wrap justify-center items-center bg-cover bg-center overflow-hidden;
|
||||
|
||||
min-height: 430px;
|
||||
}
|
||||
|
||||
.wp-block-verse {
|
||||
@apply font-sans;
|
||||
}
|
||||
|
||||
.wp-block-media-text {
|
||||
@apply grid grid-cols-2 gap-4;
|
||||
}
|
33
examples/cms-wordpress/tailwind.config.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
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)',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
13
examples/cms-wordpress/vercel.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"env": {
|
||||
"NEXT_EXAMPLE_CMS_WORDPRESS_API_URL": "@next_example_cms_wordpress_api_url",
|
||||
"NEXT_EXAMPLE_CMS_WORDPRESS_AUTH_REFRESH_TOKEN": "@next_example_cms_wordpress_auth_refresh_token",
|
||||
"NEXT_EXAMPLE_CMS_WORDPRESS_PREVIEW_SECRET": "@next_example_cms_wordpress_preview_secret"
|
||||
},
|
||||
"build": {
|
||||
"env": {
|
||||
"NEXT_EXAMPLE_CMS_WORDPRESS_API_URL": "@next_example_cms_wordpress_api_url",
|
||||
"NEXT_EXAMPLE_CMS_WORDPRESS_AUTH_REFRESH_TOKEN": "@next_example_cms_wordpress_auth_refresh_token"
|
||||
}
|
||||
}
|
||||
}
|