chore: update website template 2 (#7076)
This commit is contained in:
@@ -13,10 +13,9 @@ Core features:
|
||||
- [Pre-configured Payload Config](#how-it-works)
|
||||
- [Authentication](#users-authentication)
|
||||
- [Access Control](#access-control)
|
||||
- [Premium Content](#premium-content)
|
||||
- [Comments](#comments)
|
||||
- [Layout Builder](#layout-builder)
|
||||
- [Draft Preview](#draft-preview)
|
||||
- [Live Preview](#live-preview)
|
||||
- [Redirects](#redirects)
|
||||
- [SEO](#seo)
|
||||
- [Website](#website)
|
||||
@@ -37,7 +36,7 @@ Go to Payload Cloud and [clone this template](https://payloadcms.com/new/clone/w
|
||||
|
||||
Use the `create-payload-app` CLI to clone this template directly to your machine:
|
||||
|
||||
npx create-payload-app@latest my-project -t website
|
||||
npx create-payload-app@beta my-project -t website
|
||||
|
||||
#### Method 3
|
||||
|
||||
@@ -49,8 +48,8 @@ Use the `git` CLI to clone this template directly to your machine:
|
||||
|
||||
1. First [clone the repo](#clone) if you have not done so already
|
||||
1. `cd my-project && cp .env.example .env` to copy the example environment variables
|
||||
1. `yarn && yarn dev` to install dependencies and start the dev server
|
||||
1. `open http://localhost:3000` to open the app in your browser
|
||||
1. `pnpm install && pnpm dev` to install dependencies and start the dev server
|
||||
1. open `http://localhost:3000` to open the app in your browser
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. Follow the on-screen instructions to login and create your first admin user. Then check out [Production](#production) once you're ready to build and serve your app, and [Deployment](#deployment) when you're ready to go live.
|
||||
|
||||
@@ -60,30 +59,18 @@ The Payload config is tailored specifically to the needs of most websites. It is
|
||||
|
||||
### Collections
|
||||
|
||||
See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend this functionality.
|
||||
See the [Collections](https://payloadcms.com/docs/beta/configuration/collections) docs for details on how to extend this functionality.
|
||||
|
||||
- #### Users (Authentication)
|
||||
|
||||
Users are auth-enabled and encompass both admins and regular users based on the value of their `roles` field. Only `admin` users can access your admin panel to manage your website whereas `user` can authenticate on your front-end to leave [comments](#comments) and read [premium content](#premium-content) but have limited access to the platform. See [Access Control](#access-control) for more details.
|
||||
Users are auth-enabled collections that have access to the admin panel and unpublished content. See [Access Control](#access-control) for more details.
|
||||
|
||||
For additional help, see the official [Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) or the [Authentication](https://payloadcms.com/docs/authentication/overview#authentication-overview) docs.
|
||||
For additional help, see the official [Auth Example](https://github.com/payloadcms/payload/tree/beta/examples/auth) or the [Authentication](https://payloadcms.com/docs/beta/authentication/overview#authentication-overview) docs.
|
||||
|
||||
- #### Posts
|
||||
|
||||
Posts are used to generated blog posts, news articles, or any other type of content that is published over time. All posts are layout builder enabled so you can generate unique layouts for each post using layout-building blocks, see [Layout Builder](#layout-builder) for more details. Posts are also draft-enabled so you can preview them before publishing them to your website, see [Draft Preview](#draft-preview) for more details.
|
||||
|
||||
Users can also leave comments on posts if they are logged in. Then, editors can log in to review and approve comments before they are published. See [Comments](#comments) for more details.
|
||||
|
||||
Posts can also restrict access to content or digital assets behind authentication, see [Premium Content](#premium-content) for more details.
|
||||
|
||||
- ### Comments (Collection)
|
||||
|
||||
Comments are used to allow logged-in users to leave comments on posts. Comments are draft-enabled so admins can review and approve them before they are published to your website, see [Comments](#comments) for more details.
|
||||
|
||||
- #### Projects
|
||||
|
||||
Projects are used to showcase your work. All projects are layout builder enabled so you can generate unique layouts for each project using layout-building blocks, see [Layout Builder](#layout-builder) for more details. Projects are also draft-enabled so you can preview them before publishing them to your website, see [Draft Preview](#draft-preview) for more details.
|
||||
|
||||
- #### Pages
|
||||
|
||||
All pages are layout builder enabled so you can generate unique layouts for each page using layout-building blocks, see [Layout Builder](#layout-builder) for more details. Pages are also draft-enabled so you can preview them before publishing them to your website, see [Draft Preview](#draft-preview) for more details.
|
||||
@@ -94,7 +81,7 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
|
||||
|
||||
- #### Categories
|
||||
|
||||
A taxonomy used to group posts or projects together. Categories can be nested inside of one another, for example "News > Technology". See the official [Payload Nested Docs Plugin](https://github.com/payloadcms/plugin-nested-docs) for more details.
|
||||
A taxonomy used to group posts together. Categories can be nested inside of one another, for example "News > Technology". See the official [Payload Nested Docs Plugin](https://payloadcms.com/docs/beta/plugins/nested-docs) for more details.
|
||||
|
||||
### Globals
|
||||
|
||||
@@ -110,46 +97,17 @@ See the [Globals](https://payloadcms.com/docs/configuration/globals) docs for de
|
||||
|
||||
## Access control
|
||||
|
||||
Basic role-based access control is setup to determine what users can and cannot do based on their roles, which are:
|
||||
Basic access control is setup to limit access to various content based based on publishing status.
|
||||
|
||||
- `admin`: They can access the Payload admin panel to manage your site. They can see all data and make all operations.
|
||||
- `user`: They cannot access the Payload admin panel and can perform limited operations based on their user (see below).
|
||||
- `users`: Users can access the admin panel and create or edit content.
|
||||
- `posts`: Everyone can access published posts, but only users can create, update, or delete them.
|
||||
- `pages`: Everyone can access published pages, but only users can create, update, or delete them.
|
||||
|
||||
This applies to each collection in the following ways:
|
||||
|
||||
- `users`: Only admins and the user themselves can access their profile. Anyone can create a user but only admins can delete users.
|
||||
- `posts`: Everyone can access published posts, but only admins can create, update, or delete them. Some posts may also have content that is only accessible to users who are logged in. See [Premium Content](#premium-content) for more details.
|
||||
- `projects`: Everyone can access published projects, but only admins can create, update, or delete them.
|
||||
- `pages`: Everyone can access published pages, but only admins can create, update, or delete them.
|
||||
- `comments`: Everyone can access published comments, but only admins can access draft comments. Users can create new comments but they will be saved as drafts until an admin approves them.
|
||||
|
||||
For more details on how to extend this functionality, see the [Payload Access Control](https://payloadcms.com/docs/access-control/overview#access-control) docs.
|
||||
|
||||
## Premium Content
|
||||
|
||||
Posts can optionally restrict access to content or digital assets behind authentication. This will ensure that only members of your site can access the full post data and its resources. To do this, a `premiumContent` field is added to the `posts` collection with `read` access control set to check for an authenticated user on the request. Every time a user requests a post, this will only return data to those who have access to it:
|
||||
|
||||
```ts
|
||||
{
|
||||
name: 'premiumContent',
|
||||
label: 'Premium Content',
|
||||
type: 'blocks',
|
||||
access: {
|
||||
read: isLoggedIn,
|
||||
},
|
||||
fields: [
|
||||
// content
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Comments
|
||||
|
||||
Users can leave comments on posts for editors to review and approve before they are published to the website. To do this, a `comments` collection is added with `drafts` set to `true` so that all comments are saved as drafts and inaccessible until an admin approves them. Each comment references a single `user` and a `doc` for cross reference. To leave a comment you must be logged-in, and to publish a comment you must has the role `admin`.
|
||||
For more details on how to extend this functionality, see the [Payload Access Control](https://payloadcms.com/docs/beta/access-control/overview#access-control) docs.
|
||||
|
||||
## Layout Builder
|
||||
|
||||
Create unique page, post, or project layouts for any type of content using a powerful layout builder. This template comes pre-configured with the following layout building blocks:
|
||||
Create unique page layouts for any type of content using a powerful layout builder. This template comes pre-configured with the following layout building blocks:
|
||||
|
||||
- Hero
|
||||
- Content
|
||||
@@ -159,56 +117,56 @@ Create unique page, post, or project layouts for any type of content using a pow
|
||||
|
||||
Each block is fully designed and built into the front-end website that comes with this template. See [Website](#website) for more details.
|
||||
|
||||
## Lexical editor
|
||||
|
||||
A deep editorial experience that allows complete freedom to focus just on writing content without breaking out of the flow with support for Payload blocks, media, links and other features provided out of the box. See [Lexical](https://payloadcms.com/docs/beta/lexical/overview) docs.
|
||||
|
||||
## Draft Preview
|
||||
|
||||
All posts, projects, and pages are draft-enabled so you can preview them before publishing them to your website. To do this, these collections use [Versions](https://payloadcms.com/docs/configuration/collections#versions) with `drafts` set to `true`. This means that when you create a new post, project, or page, it will be saved as a draft and will not be visible on your website until you publish it. This also means that you can preview your draft before publishing it to your website. To do this, we automatically format a custom URL which redirects to your front-end to securely fetch the draft version of your content.
|
||||
All posts and pages are draft-enabled so you can preview them before publishing them to your website. To do this, these collections use [Versions](https://payloadcms.com/docs/beta/configuration/collections#versions) with `drafts` set to `true`. This means that when you create a new post, project, or page, it will be saved as a draft and will not be visible on your website until you publish it. This also means that you can preview your draft before publishing it to your website. To do this, we automatically format a custom URL which redirects to your front-end to securely fetch the draft version of your content.
|
||||
|
||||
Since the front-end of this template is statically generated, this also means that pages, posts, and projects will need to be regenerated as changes are made to published documents. To do this, we use an `afterChange` hook to regenerate the front-end when a document has changed and its `_status` is `published`.
|
||||
|
||||
For more details on how to extend this functionality, see the official [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview).
|
||||
For more details on how to extend this functionality, see the official [Draft Preview Example](https://github.com/payloadcms/payload/tree/beta/examples/draft-preview).
|
||||
|
||||
## Live preview
|
||||
|
||||
In addition to draft previews you can also enable live preview to view your end resulting page as you're editing content with full support for SSR rendering. See [Live preview docs](https://payloadcms.com/docs/beta/live-preview/overview) for more details.
|
||||
|
||||
## SEO
|
||||
|
||||
This template comes pre-configured with the official [Payload SEO Plugin](https://github.com/payloadcms/plugin-seo) for complete SEO control from the admin panel. All SEO data is fully integrated into the front-end website that comes with this template. See [Website](#website) for more details.
|
||||
This template comes pre-configured with the official [Payload SEO Plugin](https://payloadcms.com/docs/beta/plugins/seo) for complete SEO control from the admin panel. All SEO data is fully integrated into the front-end website that comes with this template. See [Website](#website) for more details.
|
||||
|
||||
## Redirects
|
||||
|
||||
If you are migrating an existing site or moving content to a new URL, you can use the `redirects` collection to create a proper redirect from old URLs to new ones. This will ensure that proper request status codes are returned to search engines and that your users are not left with a broken link. This template comes pre-configured with the official [Payload Redirects Plugin](https://github.com/payloadcms/plugin-redirects) for complete redirect control from the admin panel. All redirects are fully integrated into the front-end website that comes with this template. See [Website](#website) for more details.
|
||||
If you are migrating an existing site or moving content to a new URL, you can use the `redirects` collection to create a proper redirect from old URLs to new ones. This will ensure that proper request status codes are returned to search engines and that your users are not left with a broken link. This template comes pre-configured with the official [Payload Redirects Plugin](https://payloadcms.com/docs/beta/plugins/redirects) for complete redirect control from the admin panel. All redirects are fully integrated into the front-end website that comes with this template. See [Website](#website) for more details.
|
||||
|
||||
## Website
|
||||
|
||||
This template includes a beautifully designed, production-ready front-end built with the [Next.js App Router](https://nextjs.org), served right alongside your Payload app in a single Express server. This makes is so that you can deploy both apps simultaneously and host them together. If you prefer a different front-end framework, this pattern works for any framework that supports a custom server. If you prefer to host your website separately from Payload, you can easily [Eject](#eject) the front-end out from this template to swap in your own, or to use it as a standalone CMS. For more details, see the official [Custom Server Example](https://github.com/payloadcms/payload/tree/main/examples/custom-server).
|
||||
This template includes a beautifully designed, production-ready front-end built with the [Next.js App Router](https://nextjs.org), served right alongside your Payload app in a instance. This makes it so that you can deploy both your backend and website where you need it.
|
||||
|
||||
Core features:
|
||||
|
||||
- [Next.js App Router](https://nextjs.org)
|
||||
- [GraphQL](https://graphql.org)
|
||||
- [TypeScript](https://www.typescriptlang.org)
|
||||
- [React Hook Form](https://react-hook-form.com)
|
||||
- [Payload Admin Bar](https://github.com/payloadcms/payload-admin-bar)
|
||||
- [TailwindCSS styling](https://tailwindcss.com/)
|
||||
- [shadcn/ui components](https://ui.shadcn.com/)
|
||||
- Authentication
|
||||
- Fully featured blog
|
||||
- Publication workflow
|
||||
- Comments
|
||||
- Premium content
|
||||
- User accounts
|
||||
- Dark mode
|
||||
- Pre-made layout building blocks
|
||||
- SEO
|
||||
- Redirects
|
||||
- Live preview
|
||||
|
||||
### Cache
|
||||
|
||||
Although Next.js includes a robust set of caching strategies out of the box, Payload Cloud proxies and caches all files through Cloudflare using the [Official Cloud Plugin](https://github.com/payloadcms/plugin-cloud). This means that Next.js caching is not needed and is disabled by default. If you are hosting your app outside of Payload Cloud, you can easily reenable the Next.js caching mechanisms by removing the `no-store` directive from all fetch requests in `./src/app/_api` and then removing all instances of `export const dynamic = 'force-dynamic'` from pages files, such as `./src/app/(pages)/[slug]/page.tsx`. For more details, see the official [Next.js Caching Docs](https://nextjs.org/docs/app/building-your-application/caching).
|
||||
|
||||
### Eject
|
||||
|
||||
If you prefer another front-end framework or would like to use Payload as a standalone CMS, you can easily eject the front-end from this template. To eject, simply run `yarn eject`. This will uninstall all Next.js related dependencies and delete all files and folders related to the Next.js front-end. It also removes all custom routing from your `server.ts` file and updates your `eslintrc.js`.
|
||||
|
||||
> Note: Your eject script may not work as expected if you've made significant modifications to your project. If you run into any issues, compare your project's dependencies and file structure with this template. See [./src/eject](./src/eject) for full details.
|
||||
|
||||
For more details on how setup a custom server, see the official [Custom Server Example](https://github.com/payloadcms/payload/tree/main/examples/custom-server).
|
||||
|
||||
## Development
|
||||
|
||||
To spin up this example locally, follow the [Quick Start](#quick-start). Then [Seed](#seed) the database with a few pages, posts, and projects.
|
||||
@@ -225,43 +183,40 @@ That's it! The Docker instance will help you get up and running quickly while al
|
||||
|
||||
### Seed
|
||||
|
||||
To seed the database with a few pages, posts, and projects you can run `yarn seed`. This template also comes with a `GET /api/seed` endpoint you can use to seed the database from the admin panel.
|
||||
To seed the database with a few pages, posts, and projects you can click the 'seed database' link from the admin panel.
|
||||
|
||||
The seed script will also create two users for demonstration purposes only:
|
||||
The seed script will also create a demo user for demonstration purposes only:
|
||||
|
||||
1. Demo Author
|
||||
- Demo Author
|
||||
- Email: `demo-author@payloadcms.com`
|
||||
- Password: `password`
|
||||
- Role: `admin`
|
||||
2. Demo User
|
||||
- Email: `demo-user@payloadcms.com`
|
||||
- Password: `password`
|
||||
- Role: `user`
|
||||
|
||||
> NOTICE: seeding the database is destructive because it drops your current database to populate a fresh one from the seed template. Only run this command if you are starting a new project or can afford to lose your current data.
|
||||
|
||||
### Conflicting routes
|
||||
|
||||
> In a monorepo when routes are bootstrapped to the same host, they can conflict with Payload's own routes if they have the same name. In our template we've named the Nextjs API routes to `next` to avoid this conflict.
|
||||
>
|
||||
> This can happen with any other routes conflicting with Payload such as `admin` and we recommend using different names for custom routes.
|
||||
> Alternatively you can also rename Payload's own routes via the [configuration](https://payloadcms.com/docs/configuration/overview).
|
||||
|
||||
## Production
|
||||
|
||||
To run Payload in production, you need to build and serve the Admin panel. To do so, follow these steps:
|
||||
To run Payload in production, you need to build and start the Admin panel. To do so, follow these steps:
|
||||
|
||||
1. Invoke the `payload build` script by running `yarn build` or `npm run build` in your project root. This creates a `./build` directory with a production-ready admin bundle.
|
||||
1. Finally run `yarn serve` or `npm run serve` to run Node in production and serve Payload from the `./build` directory.
|
||||
1. Invoke the `next build` script by running `pnpm build` or `npm run build` in your project root. This creates a `.next` directory with a production-ready admin bundle.
|
||||
1. Finally run `pnpm start` or `npm run start` to run Node in production and serve Payload from the `.build` directory.
|
||||
1. When you're ready to go live, see [Deployment](#deployment) for more details.
|
||||
|
||||
### Deployment
|
||||
### Deploying to Payload Cloud
|
||||
|
||||
The easiest way to deploy your project is to use [Payload Cloud](https://payloadcms.com/new/import), a one-click hosting solution to deploy production-ready instances of your Payload apps directly from your GitHub repo.
|
||||
|
||||
### Deploying to Vercel
|
||||
|
||||
Coming soon.
|
||||
|
||||
### Self-hosting
|
||||
|
||||
Before deploying your app, you need to:
|
||||
|
||||
1. Ensure your app builds and serves in production. See [Production](#production) for more details.
|
||||
2. Serve it from a
|
||||
|
||||
The easiest way to deploy your project is to use [Payload Cloud](https://payloadcms.com/new/import), a one-click hosting solution to deploy production-ready instances of your Payload apps directly from your GitHub repo. You can also deploy your app manually, check out the [deployment documentation](https://payloadcms.com/docs/production/deployment) for full details.
|
||||
You can also deploy your app manually, check out the [deployment documentation](https://payloadcms.com/docs/beta/production/deployment) for full details.
|
||||
|
||||
## Questions
|
||||
|
||||
|
||||
@@ -5,9 +5,16 @@ import redirects from './redirects.js'
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
images: {
|
||||
domains: ['localhost', process.env.NEXT_PUBLIC_SERVER_URL]
|
||||
.filter(Boolean)
|
||||
.map((url) => url.replace(/https?:\/\//, '')),
|
||||
remotePatterns: [
|
||||
...[process.env.NEXT_PUBLIC_SERVER_URL /* 'https://example.com' */].map((item) => {
|
||||
const url = new URL(item)
|
||||
|
||||
return {
|
||||
hostname: url.hostname,
|
||||
protocol: url.protocol.replace(':', ''),
|
||||
}
|
||||
}),
|
||||
],
|
||||
},
|
||||
reactStrictMode: true,
|
||||
redirects,
|
||||
|
||||
@@ -14,25 +14,24 @@
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src",
|
||||
"payload": "payload",
|
||||
"reinstall": "rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install",
|
||||
"seed": "rm -rf media && cross-env PAYLOAD_SEED=true PAYLOAD_DROP_DATABASE=true next dev",
|
||||
"serve": "next start"
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lexical/list": "^0.15",
|
||||
"@lexical/react": "^0.15",
|
||||
"@lexical/rich-text": "^0.15",
|
||||
"@lexical/utils": "^0.15",
|
||||
"@payloadcms/db-mongodb": "3.0.0-beta.40",
|
||||
"@payloadcms/db-postgres": "3.0.0-beta.40",
|
||||
"@payloadcms/live-preview-react": "3.0.0-beta.40",
|
||||
"@payloadcms/next": "3.0.0-beta.40",
|
||||
"@payloadcms/plugin-cloud": "3.0.0-beta.40",
|
||||
"@payloadcms/plugin-form-builder": "3.0.0-beta.40",
|
||||
"@payloadcms/plugin-nested-docs": "3.0.0-beta.40",
|
||||
"@payloadcms/plugin-redirects": "3.0.0-beta.40",
|
||||
"@payloadcms/plugin-seo": "3.0.0-beta.40",
|
||||
"@payloadcms/richtext-lexical": "3.0.0-beta.40",
|
||||
"@payloadcms/ui": "3.0.0-beta.40",
|
||||
"@lexical/list": "0.16.1",
|
||||
"@lexical/react": "0.16.1",
|
||||
"@lexical/rich-text": "0.16.1",
|
||||
"@lexical/utils": "0.16.1",
|
||||
"@payloadcms/db-mongodb": "3.0.0-beta.59",
|
||||
"@payloadcms/db-postgres": "3.0.0-beta.59",
|
||||
"@payloadcms/live-preview-react": "3.0.0-beta.59",
|
||||
"@payloadcms/next": "3.0.0-beta.59",
|
||||
"@payloadcms/plugin-cloud": "3.0.0-beta.59",
|
||||
"@payloadcms/plugin-form-builder": "3.0.0-beta.59",
|
||||
"@payloadcms/plugin-nested-docs": "3.0.0-beta.59",
|
||||
"@payloadcms/plugin-redirects": "3.0.0-beta.59",
|
||||
"@payloadcms/plugin-seo": "3.0.0-beta.59",
|
||||
"@payloadcms/richtext-lexical": "3.0.0-beta.59",
|
||||
"@payloadcms/ui": "3.0.0-beta.59",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
@@ -45,10 +44,10 @@
|
||||
"geist": "^1.3.0",
|
||||
"graphql": "^16.8.1",
|
||||
"jsonwebtoken": "9.0.1",
|
||||
"lexical": "^0.15",
|
||||
"lexical": "0.16.1",
|
||||
"lucide-react": "^0.378.0",
|
||||
"next": "15.0.0-rc.0",
|
||||
"payload": "3.0.0-beta.40",
|
||||
"next": "15.0.0-canary.58",
|
||||
"payload": "3.0.0-beta.59",
|
||||
"payload-admin-bar": "^1.0.6",
|
||||
"prism-react-renderer": "^2.3.1",
|
||||
"qs": "6.11.2",
|
||||
@@ -75,7 +74,7 @@
|
||||
"prettier": "^3.0.3",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "5.4.2"
|
||||
"typescript": "5.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
|
||||
7941
templates/website/pnpm-lock.yaml
generated
7941
templates/website/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 91 KiB |
BIN
templates/website/public/website-template-OG.webp
Normal file
BIN
templates/website/public/website-template-OG.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -1,15 +1,16 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import { PayloadRedirects } from '@/components/PayloadRedirects'
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import { draftMode, headers } from 'next/headers'
|
||||
import React from 'react'
|
||||
import { homeStatic } from 'src/payload/seed/home-static'
|
||||
|
||||
import type { Page } from '../../../payload-types'
|
||||
import type { Page as PageType } from '../../../payload-types'
|
||||
|
||||
import { Blocks } from '../../components/Blocks'
|
||||
import { Hero } from '../../components/Hero'
|
||||
import { PayloadRedirects } from '../../components/PayloadRedirects'
|
||||
import { generateMeta } from '../../utilities/generateMeta'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
@@ -27,10 +28,17 @@ export async function generateStaticParams() {
|
||||
export default async function Page({ params: { slug = 'home' } }) {
|
||||
const url = '/' + slug
|
||||
|
||||
const page = await queryPageBySlug({
|
||||
let page: PageType | null
|
||||
|
||||
page = await queryPageBySlug({
|
||||
slug,
|
||||
})
|
||||
|
||||
// Remove this code once your website is seeded
|
||||
if (!page) {
|
||||
page = homeStatic
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
return <PayloadRedirects url={url} />
|
||||
}
|
||||
@@ -39,6 +47,9 @@ export default async function Page({ params: { slug = 'home' } }) {
|
||||
|
||||
return (
|
||||
<article className="pt-16 pb-24">
|
||||
{/* Allows redirects for valid pages too */}
|
||||
<PayloadRedirects disableNotFound url={url} />
|
||||
|
||||
<Hero {...hero} />
|
||||
<Blocks blocks={layout} />
|
||||
</article>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import { PayloadRedirects } from '@/components/PayloadRedirects'
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import { draftMode, headers } from 'next/headers'
|
||||
@@ -9,7 +10,6 @@ import RichText from 'src/app/components/RichText'
|
||||
|
||||
import type { Post } from '../../../../payload-types'
|
||||
|
||||
import { PayloadRedirects } from '../../../components/PayloadRedirects'
|
||||
import { PostHero } from '../../../heros/PostHero'
|
||||
import { generateMeta } from '../../../utilities/generateMeta'
|
||||
import PageClient from './page.client'
|
||||
@@ -30,14 +30,15 @@ export default async function Post({ params: { slug = '' } }) {
|
||||
const url = '/posts/' + slug
|
||||
const post = await queryPostBySlug({ slug })
|
||||
|
||||
if (!post) {
|
||||
return <PayloadRedirects url={url} />
|
||||
}
|
||||
if (!post) return <PayloadRedirects url={url} />
|
||||
|
||||
return (
|
||||
<article className="pt-16 pb-16">
|
||||
<PageClient />
|
||||
|
||||
{/* Allows redirects for valid pages too */}
|
||||
<PayloadRedirects disableNotFound url={url} />
|
||||
|
||||
<PostHero post={post} />
|
||||
|
||||
<div className="flex flex-col gap-4 pt-8">
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
segments: string[]
|
||||
}
|
||||
searchParams: {
|
||||
[key: string]: string | string[]
|
||||
}
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
|
||||
|
||||
export default NotFound
|
||||
@@ -1,7 +1,9 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import { RootPage } from '@payloadcms/next/views'
|
||||
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
@@ -12,6 +14,9 @@ type Args = {
|
||||
}
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
|
||||
|
||||
export default Page
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
import config from '@payload-config'
|
||||
import { REST_DELETE, REST_GET, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
|
||||
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = REST_GET(config)
|
||||
export const POST = REST_POST(config)
|
||||
export const DELETE = REST_DELETE(config)
|
||||
export const PATCH = REST_PATCH(config)
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
|
||||
@@ -2,18 +2,21 @@ import type React from 'react'
|
||||
import type { Page, Post } from 'src/payload-types'
|
||||
|
||||
import { getCachedDocument } from '@/utilities/getDocument'
|
||||
import { getCachedRedirect } from '@/utilities/getRedirect'
|
||||
import { getCachedRedirects } from '@/utilities/getRedirects'
|
||||
import { notFound, redirect } from 'next/navigation'
|
||||
|
||||
interface Props {
|
||||
disableNotFound?: boolean
|
||||
url: string
|
||||
}
|
||||
|
||||
/* This component helps us with SSR based dynamic redirects */
|
||||
export const PayloadRedirects: React.FC<Props> = async ({ url }) => {
|
||||
export const PayloadRedirects: React.FC<Props> = async ({ disableNotFound, url }) => {
|
||||
const slug = url.startsWith('/') ? url : `${url}`
|
||||
|
||||
const redirectItem = await getCachedRedirect(slug)()
|
||||
const redirects = await getCachedRedirects()()
|
||||
|
||||
const redirectItem = redirects.find((redirect) => redirect.from === slug)
|
||||
|
||||
if (redirectItem) {
|
||||
if (redirectItem.to?.url) {
|
||||
@@ -39,5 +42,6 @@ export const PayloadRedirects: React.FC<Props> = async ({ url }) => {
|
||||
if (redirectUrl) redirect(redirectUrl)
|
||||
}
|
||||
|
||||
if (disableNotFound) return null
|
||||
return notFound()
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
import { revalidatePath, revalidateTag } from 'next/cache'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export type RevalidationType = 'path' | 'tag'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
export async function GET(request: NextRequest): Promise<Response> {
|
||||
const path = request.nextUrl.searchParams.get('path')
|
||||
const secret = request.nextUrl.searchParams.get('secret')
|
||||
const tag = request.nextUrl.searchParams.get('tag')
|
||||
const type: RevalidationType =
|
||||
(request.nextUrl.searchParams.get('type') as RevalidationType) ?? 'path'
|
||||
|
||||
if (!secret || secret !== process.env.NEXT_PRIVATE_REVALIDATION_KEY) {
|
||||
// Do not indicate that the revalidation key is incorrect in the response
|
||||
// This will protect this API route from being exploited
|
||||
return new Response('Invalid request', { status: 400 })
|
||||
}
|
||||
|
||||
if ((type === 'path' && !path) || (type === 'tag' && !tag)) {
|
||||
return new Response('Invalid request', { status: 400 })
|
||||
}
|
||||
|
||||
if (type === 'path') {
|
||||
revalidatePath(path)
|
||||
return NextResponse.json({ now: Date.now(), revalidated: true })
|
||||
}
|
||||
|
||||
if (type === 'tag') {
|
||||
revalidateTag(tag)
|
||||
return NextResponse.json({ now: Date.now(), revalidated: true })
|
||||
}
|
||||
|
||||
return NextResponse.json({ now: Date.now(), revalidated: false })
|
||||
}
|
||||
@@ -14,6 +14,8 @@ export const generateMeta = async (args: { doc: Page | Post }): Promise<Metadata
|
||||
'url' in doc.meta.image &&
|
||||
`${process.env.NEXT_PUBLIC_SERVER_URL}${doc.meta.image.url}`
|
||||
|
||||
const title = doc?.meta?.title ? doc?.meta?.title + ' | Payload Template' : 'Payload Template'
|
||||
|
||||
return {
|
||||
description: doc?.meta?.description,
|
||||
openGraph: mergeOpenGraph({
|
||||
@@ -25,9 +27,9 @@ export const generateMeta = async (args: { doc: Page | Post }): Promise<Metadata
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
title: doc?.meta?.title || 'Payload',
|
||||
title,
|
||||
url: Array.isArray(doc?.slug) ? doc?.slug.join('/') : '/',
|
||||
}),
|
||||
title: doc?.meta?.title || 'Payload',
|
||||
title,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,27 +2,25 @@ import configPromise from '@payload-config'
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import { unstable_cache } from 'next/cache'
|
||||
|
||||
export async function getRedirect(slug: string, depth = 1) {
|
||||
export async function getRedirects(depth = 1) {
|
||||
const payload = await getPayloadHMR({ config: configPromise })
|
||||
|
||||
const { docs: redirects } = await payload.find({
|
||||
collection: 'redirects',
|
||||
depth,
|
||||
limit: 1,
|
||||
limit: 0,
|
||||
pagination: false,
|
||||
where: {
|
||||
from: {
|
||||
equals: slug,
|
||||
},
|
||||
},
|
||||
})
|
||||
return redirects[0]
|
||||
|
||||
return redirects
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unstable_cache function mapped with the cache tag for the slug
|
||||
* Returns a unstable_cache function mapped with the cache tag for 'redirects'.
|
||||
*
|
||||
* Cache all redirects together to avoid multiple fetches.
|
||||
*/
|
||||
export const getCachedRedirect = (slug: string) =>
|
||||
unstable_cache(async () => getRedirect(slug), [slug], {
|
||||
tags: [`redirects_${slug}`],
|
||||
export const getCachedRedirects = () =>
|
||||
unstable_cache(async () => getRedirects(), ['redirects'], {
|
||||
tags: ['redirects'],
|
||||
})
|
||||
@@ -5,7 +5,9 @@ const defaultOpenGraph: Metadata['openGraph'] = {
|
||||
description: 'An open-source website built with Payload and Next.js.',
|
||||
images: [
|
||||
{
|
||||
url: 'https://payloadcms.com/images/og-image.jpg',
|
||||
url: process.env.NEXT_PUBLIC_SERVER_URL
|
||||
? `${process.env.NEXT_PUBLIC_SERVER_URL}/website-template-OG.webp`
|
||||
: '/website-template-OG.webp',
|
||||
},
|
||||
],
|
||||
siteName: 'Payload Website Template',
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
config: () => null,
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
raw: () => {},
|
||||
url: () => {},
|
||||
}
|
||||
@@ -7,6 +7,9 @@
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
collections: {
|
||||
pages: Page;
|
||||
posts: Post;
|
||||
@@ -28,6 +31,19 @@ export interface Config {
|
||||
collection: 'users';
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
};
|
||||
login: {
|
||||
password: string;
|
||||
email: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages".
|
||||
@@ -318,7 +334,6 @@ export interface Post {
|
||||
export interface User {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
roles?: ('admin' | 'user')[] | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
@@ -641,6 +656,13 @@ export interface BannerBlock {
|
||||
blockName?: string | null;
|
||||
blockType: 'banner';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
|
||||
@@ -17,8 +17,7 @@ import { ItalicFeature } from '@payloadcms/richtext-lexical'
|
||||
import { BoldFeature } from '@payloadcms/richtext-lexical'
|
||||
import dotenv from 'dotenv'
|
||||
import path from 'path'
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { revalidateRedirect } from 'src/payload/hooks/revalidateRedirect'
|
||||
import { buildConfig } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import Categories from './payload/collections/Categories'
|
||||
@@ -31,6 +30,7 @@ import BeforeLogin from './payload/components/BeforeLogin'
|
||||
import { seed } from './payload/endpoints/seed'
|
||||
import { Footer } from './payload/globals/Footer/Footer'
|
||||
import { Header } from './payload/globals/Header/Header'
|
||||
import { revalidateRedirects } from './payload/hooks/revalidateRedirects'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
@@ -109,8 +109,22 @@ export default buildConfig({
|
||||
redirectsPlugin({
|
||||
collections: ['pages', 'posts'],
|
||||
overrides: {
|
||||
// @ts-expect-error
|
||||
fields: ({ defaultFields }) => {
|
||||
return defaultFields.map((field) => {
|
||||
if ('name' in field && field.name === 'from') {
|
||||
return {
|
||||
...field,
|
||||
admin: {
|
||||
description: 'You will need to rebuild the website when changing this field.',
|
||||
},
|
||||
}
|
||||
}
|
||||
return field
|
||||
})
|
||||
},
|
||||
hooks: {
|
||||
afterChange: [revalidateRedirect],
|
||||
afterChange: [revalidateRedirects],
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { AccessArgs } from 'payload/config'
|
||||
|
||||
import type { User } from '../../payload-types'
|
||||
|
||||
import { checkRole } from '../collections/Users/checkRole'
|
||||
|
||||
type isAdmin = (args: AccessArgs<User>) => boolean
|
||||
|
||||
export const admins: isAdmin = ({ req: { user } }) => {
|
||||
return checkRole(['admin'], user)
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
import type { Access } from 'payload/config'
|
||||
import type { Access } from 'payload'
|
||||
|
||||
export const anyone: Access = () => true
|
||||
|
||||
11
templates/website/src/payload/access/authenticated.ts
Normal file
11
templates/website/src/payload/access/authenticated.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { AccessArgs } from 'payload'
|
||||
|
||||
import type { User } from '../../payload-types'
|
||||
|
||||
type isAuthenticated = (args: AccessArgs<User>) => boolean
|
||||
|
||||
export const authenticated: isAuthenticated = ({ req: { user } }) => {
|
||||
if (user) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Access } from 'payload'
|
||||
|
||||
export const authenticatedOrPublished: Access = ({ req: { user } }) => {
|
||||
if (user) {
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
_status: {
|
||||
equals: 'published',
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { Access } from 'payload/config'
|
||||
|
||||
export const usersOrPublished: Access = ({ req: { user } }) => {
|
||||
if (user) {
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
_status: {
|
||||
equals: 'published',
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Block } from 'payload/types'
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Block } from 'payload/types'
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Block } from 'payload/types'
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Block } from 'payload/types'
|
||||
import type { Block } from 'payload'
|
||||
|
||||
export const Code: Block = {
|
||||
slug: 'code',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Block, Field } from 'payload/types'
|
||||
import type { Block, Field } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Block } from 'payload/types'
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Block } from 'payload/types'
|
||||
import type { Block } from 'payload'
|
||||
|
||||
export const MediaBlock: Block = {
|
||||
slug: 'mediaBlock',
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { anyone } from '../access/anyone'
|
||||
import { authenticated } from '../access/authenticated'
|
||||
|
||||
const Categories: CollectionConfig = {
|
||||
slug: 'categories',
|
||||
access: {
|
||||
read: () => true,
|
||||
create: authenticated,
|
||||
delete: authenticated,
|
||||
read: anyone,
|
||||
update: authenticated,
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
@@ -8,13 +8,19 @@ import {
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { anyone } from '../access/anyone'
|
||||
import { authenticated } from '../access/authenticated'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
slug: 'media',
|
||||
access: {
|
||||
read: () => true,
|
||||
create: authenticated,
|
||||
delete: authenticated,
|
||||
read: anyone,
|
||||
update: authenticated,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
import type { CollectionAfterChangeHook } from 'payload/types'
|
||||
import type { CollectionAfterChangeHook } from 'payload'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
import type { Page } from '../../../../payload-types'
|
||||
|
||||
import { revalidate } from '../../../utilities/revalidate'
|
||||
|
||||
export const revalidatePage: CollectionAfterChangeHook<Page> = ({ doc, req: { payload } }) => {
|
||||
export const revalidatePage: CollectionAfterChangeHook<Page> = ({
|
||||
doc,
|
||||
previousDoc,
|
||||
req: { payload },
|
||||
}) => {
|
||||
if (doc._status === 'published') {
|
||||
const path = doc.slug === 'home' ? '/' : `/${doc.slug}`
|
||||
void revalidate({ collection: 'pages', path, payload })
|
||||
|
||||
payload.logger.info(`Revalidating page at path: ${path}`)
|
||||
|
||||
revalidatePath(path)
|
||||
}
|
||||
|
||||
// If the page was previously published, we need to revalidate the old path
|
||||
if (previousDoc?._status === 'published' && doc._status !== 'published') {
|
||||
const oldPath = previousDoc.slug === 'home' ? '/' : `/${previousDoc.slug}`
|
||||
|
||||
payload.logger.info(`Revalidating old page at path: ${oldPath}`)
|
||||
|
||||
revalidatePath(oldPath)
|
||||
}
|
||||
|
||||
return doc
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { admins } from '../../access/admins'
|
||||
import { usersOrPublished } from '../../access/usersOrPublished'
|
||||
import { authenticated } from '../../access/authenticated'
|
||||
import { authenticatedOrPublished } from '../../access/authenticatedOrPublished'
|
||||
import { Archive } from '../../blocks/ArchiveBlock'
|
||||
import { CallToAction } from '../../blocks/CallToAction'
|
||||
import { Content } from '../../blocks/Content'
|
||||
@@ -16,10 +16,10 @@ import { revalidatePage } from './hooks/revalidatePage'
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
access: {
|
||||
create: admins,
|
||||
delete: admins,
|
||||
read: usersOrPublished,
|
||||
update: admins,
|
||||
create: authenticated,
|
||||
delete: authenticated,
|
||||
read: authenticatedOrPublished,
|
||||
update: authenticated,
|
||||
},
|
||||
admin: {
|
||||
defaultColumns: ['title', 'slug', 'updatedAt'],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CollectionAfterReadHook } from 'payload/types'
|
||||
import type { CollectionAfterReadHook } from 'payload'
|
||||
|
||||
// The `user` collection has access control locked so that users are not publicly accessible
|
||||
// This means that we need to populate the authors manually here to protect user privacy
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import type { CollectionAfterChangeHook } from 'payload/types'
|
||||
import type { CollectionAfterChangeHook } from 'payload'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
import type { Post } from '../../../../payload-types'
|
||||
|
||||
import { revalidate } from '../../../utilities/revalidate'
|
||||
|
||||
export const revalidatePost: CollectionAfterChangeHook<Post> = ({ doc, req: { payload } }) => {
|
||||
export const revalidatePost: CollectionAfterChangeHook<Post> = ({
|
||||
doc,
|
||||
previousDoc,
|
||||
req: { payload },
|
||||
}) => {
|
||||
if (doc._status === 'published') {
|
||||
void revalidate({ collection: 'posts', path: `/posts/${doc.slug}`, payload })
|
||||
const path = `/posts/${doc.slug}`
|
||||
|
||||
payload.logger.info(`Revalidating post at path: ${path}`)
|
||||
|
||||
revalidatePath(path)
|
||||
}
|
||||
|
||||
// If the post was previously published, we need to revalidate the old path
|
||||
if (previousDoc._status === 'published' && doc._status !== 'published') {
|
||||
const oldPath = `/posts/${previousDoc.slug}`
|
||||
|
||||
payload.logger.info(`Revalidating old post at path: ${oldPath}`)
|
||||
|
||||
revalidatePath(oldPath)
|
||||
}
|
||||
|
||||
return doc
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import { BlocksFeature } from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { admins } from '../../access/admins'
|
||||
import { usersOrPublished } from '../../access/usersOrPublished'
|
||||
import { authenticated } from '../../access/authenticated'
|
||||
import { authenticatedOrPublished } from '../../access/authenticatedOrPublished'
|
||||
import { Banner } from '../../blocks/Banner'
|
||||
import { Code } from '../../blocks/Code'
|
||||
import { MediaBlock } from '../../blocks/MediaBlock'
|
||||
@@ -22,10 +22,10 @@ import { revalidatePost } from './hooks/revalidatePost'
|
||||
export const Posts: CollectionConfig = {
|
||||
slug: 'posts',
|
||||
access: {
|
||||
create: admins,
|
||||
delete: admins,
|
||||
read: usersOrPublished,
|
||||
update: admins,
|
||||
create: authenticated,
|
||||
delete: authenticated,
|
||||
read: authenticatedOrPublished,
|
||||
update: authenticated,
|
||||
},
|
||||
admin: {
|
||||
defaultColumns: ['title', 'slug', 'updatedAt'],
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { Access } from 'payload/types'
|
||||
|
||||
import { checkRole } from '../checkRole'
|
||||
|
||||
const adminsAndUser: Access = ({ req: { user } }) => {
|
||||
if (user) {
|
||||
if (checkRole(['admin'], user)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
id: {
|
||||
equals: user.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export default adminsAndUser
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { User } from '../../../payload-types'
|
||||
|
||||
export const checkRole = (allRoles: User['roles'] = [], user?: User): boolean => {
|
||||
if (user) {
|
||||
if (
|
||||
allRoles.some((role) => {
|
||||
return user?.roles?.some((individualRole) => {
|
||||
return individualRole === role
|
||||
})
|
||||
})
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { FieldHook } from 'payload/types'
|
||||
|
||||
import type { User } from '../../../../payload-types'
|
||||
|
||||
// ensure the first user created is an admin
|
||||
// 1. lookup a single user on create as succinctly as possible
|
||||
// 2. if there are no users found, append `admin` to the roles array
|
||||
// access control is already handled by this fields `access` property
|
||||
// it ensures that only admins can create and update the `roles` field
|
||||
export const ensureFirstUserIsAdmin: FieldHook<User> = async ({ operation, req, value }) => {
|
||||
if (operation === 'create') {
|
||||
const users = await req.payload.find({ collection: 'users', depth: 0, limit: 0 })
|
||||
if (users.totalDocs === 0) {
|
||||
// if `admin` not in array of values, add it
|
||||
if (!(value || []).includes('admin')) {
|
||||
return [...(value || []), 'admin']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { CollectionAfterChangeHook } from 'payload/types'
|
||||
|
||||
export const loginAfterCreate: CollectionAfterChangeHook = async ({
|
||||
doc,
|
||||
operation,
|
||||
req,
|
||||
req: {
|
||||
data = {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
payload,
|
||||
user = null,
|
||||
},
|
||||
}) => {
|
||||
if (operation === 'create' && !user) {
|
||||
const { email, password } = data
|
||||
|
||||
if (email && typeof email === 'string' && password && typeof password === 'string') {
|
||||
const { token, user } = await payload.login({
|
||||
collection: 'users',
|
||||
data: { email, password },
|
||||
req,
|
||||
})
|
||||
|
||||
return {
|
||||
...doc,
|
||||
token,
|
||||
user,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
@@ -1,20 +1,15 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { admins } from '../../access/admins'
|
||||
import { anyone } from '../../access/anyone'
|
||||
import adminsAndUser from './access/adminsAndUser'
|
||||
import { checkRole } from './checkRole'
|
||||
import { ensureFirstUserIsAdmin } from './hooks/ensureFirstUserIsAdmin'
|
||||
import { loginAfterCreate } from './hooks/loginAfterCreate'
|
||||
import { authenticated } from '../../access/authenticated'
|
||||
|
||||
const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
access: {
|
||||
admin: ({ req: { user } }) => checkRole(['admin'], user),
|
||||
create: anyone,
|
||||
delete: admins,
|
||||
read: adminsAndUser,
|
||||
update: adminsAndUser,
|
||||
admin: authenticated,
|
||||
create: authenticated,
|
||||
delete: authenticated,
|
||||
read: authenticated,
|
||||
update: authenticated,
|
||||
},
|
||||
admin: {
|
||||
defaultColumns: ['name', 'email'],
|
||||
@@ -26,34 +21,7 @@ const Users: CollectionConfig = {
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'roles',
|
||||
type: 'select',
|
||||
access: {
|
||||
create: admins,
|
||||
read: admins,
|
||||
update: admins,
|
||||
},
|
||||
defaultValue: ['user'],
|
||||
hasMany: true,
|
||||
hooks: {
|
||||
beforeChange: [ensureFirstUserIsAdmin],
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: 'admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: 'user',
|
||||
value: 'user',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
hooks: {
|
||||
afterChange: [loginAfterCreate],
|
||||
},
|
||||
timestamps: true,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PayloadHandler } from 'payload/config'
|
||||
import type { PayloadHandler } from 'payload'
|
||||
|
||||
import { seed as seedScript } from '../seed'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Field } from 'payload/types'
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Field } from 'payload/types'
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import deepMerge from '../utilities/deepMerge'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ArrayField, Field } from 'payload/types'
|
||||
import type { ArrayField, Field } from 'payload'
|
||||
|
||||
import type { LinkAppearances } from './link'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Field } from 'payload/types'
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import deepMerge from '../utilities/deepMerge'
|
||||
import formatSlug from '../utilities/formatSlug'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GlobalConfig } from 'payload/types'
|
||||
import type { GlobalConfig } from 'payload'
|
||||
|
||||
import { link } from '../../fields/link'
|
||||
import { revalidateFooter } from './hooks/revalidateFooter'
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { GlobalAfterChangeHook } from 'payload/types'
|
||||
import type { GlobalAfterChangeHook } from 'payload'
|
||||
|
||||
import { revalidate } from '../../../utilities/revalidate'
|
||||
import { revalidateTag } from 'next/cache'
|
||||
|
||||
export const revalidateFooter: GlobalAfterChangeHook = ({ doc, req: { payload } }) => {
|
||||
void revalidate({ type: 'tag', collection: 'footer', payload, tag: 'global_footer' })
|
||||
payload.logger.info(`Revalidating footer`)
|
||||
|
||||
revalidateTag('global_footer')
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GlobalConfig } from 'payload/types'
|
||||
import type { GlobalConfig } from 'payload'
|
||||
|
||||
import { link } from '../../fields/link'
|
||||
import { revalidateHeader } from './hooks/revalidateHeader'
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { GlobalAfterChangeHook } from 'payload/types'
|
||||
import type { GlobalAfterChangeHook } from 'payload'
|
||||
|
||||
import { revalidate } from '../../../utilities/revalidate'
|
||||
import { revalidateTag } from 'next/cache'
|
||||
|
||||
export const revalidateHeader: GlobalAfterChangeHook = ({ doc, req: { payload } }) => {
|
||||
void revalidate({ type: 'tag', collection: 'header', payload, tag: 'global_header' })
|
||||
payload.logger.info(`Revalidating header`)
|
||||
|
||||
revalidateTag('global_header')
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FieldHook } from 'payload/types'
|
||||
import type { FieldHook } from 'payload'
|
||||
|
||||
const format = (val: string): string =>
|
||||
val
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CollectionBeforeChangeHook } from 'payload/types'
|
||||
import type { CollectionBeforeChangeHook } from 'payload'
|
||||
|
||||
export const populatePublishedAt: CollectionBeforeChangeHook = ({ data, operation, req }) => {
|
||||
if (operation === 'create' || operation === 'update') {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { CollectionAfterChangeHook } from 'payload/types'
|
||||
|
||||
// ensure that the home page is revalidated at '/' instead of '/home'
|
||||
export const formatAppURL = ({ doc }): string => {
|
||||
const pathToUse = doc.slug === 'home' ? '' : doc.slug
|
||||
const { pathname } = new URL(`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/${pathToUse}`)
|
||||
return pathname
|
||||
}
|
||||
|
||||
// Revalidate the page in the background, so the user doesn't have to wait
|
||||
// Notice that the hook itself is not async and we are not awaiting `revalidate`
|
||||
export const revalidatePage: CollectionAfterChangeHook = ({ doc, req }) => {
|
||||
const revalidate = async (): Promise<void> => {
|
||||
let url
|
||||
|
||||
try {
|
||||
url = formatAppURL({ doc })
|
||||
const res = await fetch(
|
||||
`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/next/revalidate?secret=${process.env.REVALIDATION_KEY}&revalidatePath=${url}`,
|
||||
)
|
||||
|
||||
if (res.ok) {
|
||||
req.payload.logger.info(`Revalidated path ${url}`)
|
||||
} else {
|
||||
req.payload.logger.error(`Error revalidating path ${url}`)
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
req.payload.logger.error(`Error hitting revalidate route for ${url}`)
|
||||
}
|
||||
}
|
||||
|
||||
void revalidate()
|
||||
|
||||
return doc
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { CollectionAfterChangeHook } from 'payload/types'
|
||||
|
||||
import { revalidate } from '../utilities/revalidate'
|
||||
|
||||
export const revalidateRedirect: CollectionAfterChangeHook = ({ doc, req: { payload } }) => {
|
||||
void revalidate({ type: 'tag', collection: 'redirects', payload, tag: `redirects_${doc.from}` })
|
||||
|
||||
return doc
|
||||
}
|
||||
11
templates/website/src/payload/hooks/revalidateRedirects.ts
Normal file
11
templates/website/src/payload/hooks/revalidateRedirects.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { CollectionAfterChangeHook } from 'payload'
|
||||
|
||||
import { revalidateTag } from 'next/cache'
|
||||
|
||||
export const revalidateRedirects: CollectionAfterChangeHook = ({ doc, req: { payload } }) => {
|
||||
payload.logger.info(`Revalidating redirects`)
|
||||
|
||||
revalidateTag('redirects')
|
||||
|
||||
return doc
|
||||
}
|
||||
634
templates/website/src/payload/seed/home-static.ts
Normal file
634
templates/website/src/payload/seed/home-static.ts
Normal file
@@ -0,0 +1,634 @@
|
||||
import type { Page } from '../../payload-types'
|
||||
|
||||
// Used for pre-seeded content so that the homepage is not empty
|
||||
// @ts-expect-error
|
||||
export const homeStatic: Page = {
|
||||
slug: 'home',
|
||||
_status: 'published',
|
||||
hero: {
|
||||
type: 'lowImpact',
|
||||
richText: {
|
||||
root: {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'heading',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Payload Website Template',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
tag: 'h1',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'link',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Visit the admin dashboard',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
fields: {
|
||||
linkType: 'custom',
|
||||
newTab: false,
|
||||
url: '/admin',
|
||||
},
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 2,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' to make your account and seed content for your website.',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
textFormat: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
/* layout: [
|
||||
{
|
||||
blockName: 'Content Block',
|
||||
blockType: 'content',
|
||||
columns: [
|
||||
{
|
||||
richText: {
|
||||
root: {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'heading',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Core features',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
tag: 'h2',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
size: 'full',
|
||||
},
|
||||
{
|
||||
enableLink: false,
|
||||
link: {
|
||||
label: '',
|
||||
reference: null,
|
||||
url: '',
|
||||
},
|
||||
richText: {
|
||||
root: {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'heading',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Admin Dashboard',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
tag: 'h3',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: "Manage this site's pages and posts from the ",
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'admin dashboard',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
fields: {
|
||||
linkType: 'custom',
|
||||
newTab: false,
|
||||
url: '/admin',
|
||||
},
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 2,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: '.',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
textFormat: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
size: 'oneThird',
|
||||
},
|
||||
{
|
||||
enableLink: false,
|
||||
link: {
|
||||
label: '',
|
||||
reference: null,
|
||||
url: '',
|
||||
},
|
||||
richText: {
|
||||
root: {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'heading',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Preview',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
tag: 'h3',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Using versions, drafts, and preview, editors can review and share their changes before publishing them.',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
textFormat: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
size: 'oneThird',
|
||||
},
|
||||
{
|
||||
enableLink: false,
|
||||
link: {
|
||||
label: '',
|
||||
reference: null,
|
||||
url: '',
|
||||
},
|
||||
richText: {
|
||||
root: {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'heading',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Page Builder',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
tag: 'h3',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Custom page builder allows you to create unique page, post, and project layouts for any type of content.',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
textFormat: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
size: 'oneThird',
|
||||
},
|
||||
{
|
||||
enableLink: false,
|
||||
link: {
|
||||
label: '',
|
||||
reference: null,
|
||||
url: '',
|
||||
},
|
||||
richText: {
|
||||
root: {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'heading',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'SEO',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
tag: 'h3',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Editors have complete control over SEO data and site content directly from the ',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'admin dashboard',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
fields: {
|
||||
linkType: 'custom',
|
||||
newTab: false,
|
||||
url: '/admin',
|
||||
},
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 2,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: '.',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
textFormat: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
size: 'oneThird',
|
||||
},
|
||||
{
|
||||
enableLink: false,
|
||||
link: {
|
||||
label: '',
|
||||
reference: null,
|
||||
url: '',
|
||||
},
|
||||
richText: {
|
||||
root: {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'heading',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Dark Mode',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
tag: 'h3',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Users will experience this site in their preferred color scheme and each block can be inverted.',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
textFormat: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
size: 'oneThird',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
blockName: 'Archive Block',
|
||||
blockType: 'archive',
|
||||
categories: [],
|
||||
introContent: {
|
||||
root: {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'heading',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Recent posts',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
tag: 'h3',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'The posts below are displayed in an "Archive" layout building block which is an extremely powerful way to display documents on a page. It can be auto-populated by collection or by category, or posts can be individually selected. Pagination controls will automatically appear if the number of results exceeds the number of items per page.',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
textFormat: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
populateBy: 'collection',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
{
|
||||
blockName: 'CTA',
|
||||
blockType: 'cta',
|
||||
links: [
|
||||
{
|
||||
link: {
|
||||
type: 'custom',
|
||||
appearance: 'default',
|
||||
label: 'All posts',
|
||||
url: '/posts',
|
||||
},
|
||||
},
|
||||
],
|
||||
richText: {
|
||||
root: {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'heading',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'This is a call to action',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
tag: 'h3',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'This is a custom layout building block ',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'configured in the admin dashboard',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
fields: {
|
||||
linkType: 'custom',
|
||||
newTab: false,
|
||||
url: '/admin',
|
||||
},
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 2,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: '.',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
textFormat: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
], */
|
||||
meta: {
|
||||
description: 'An open-source website built with Payload and Next.js.',
|
||||
title: 'Payload Website Template',
|
||||
},
|
||||
title: 'Home',
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export const seed = async (payload: Payload): Promise<void> => {
|
||||
payload.logger.info(`— Seeding demo author and user...`)
|
||||
|
||||
await Promise.all(
|
||||
['demo-author@payloadcms.com', 'demo-user@payloadcms.com'].map(async (email) => {
|
||||
['demo-author@payloadcms.com'].map(async (email) => {
|
||||
await payload.delete({
|
||||
collection: 'users',
|
||||
where: {
|
||||
@@ -78,16 +78,6 @@ export const seed = async (payload: Payload): Promise<void> => {
|
||||
name: 'Demo Author',
|
||||
email: 'demo-author@payloadcms.com',
|
||||
password: 'password',
|
||||
roles: ['admin'],
|
||||
},
|
||||
}),
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
name: 'Demo User',
|
||||
email: 'demo-user@payloadcms.com',
|
||||
password: 'password',
|
||||
roles: ['user'],
|
||||
},
|
||||
}),
|
||||
])
|
||||
@@ -311,7 +301,7 @@ export const seed = async (payload: Payload): Promise<void> => {
|
||||
type: 'custom',
|
||||
label: 'Source Code',
|
||||
newTab: true,
|
||||
url: 'https://github.com/payloadcms/payload/tree/main/templates/website',
|
||||
url: 'https://github.com/payloadcms/payload/tree/beta/templates/website',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -181,7 +181,7 @@ export const post1: Partial<Post> = {
|
||||
id: '664397d9e0ef87fa1fec2fc0',
|
||||
blockName: '',
|
||||
blockType: 'code',
|
||||
code: "import { CollectionConfig } from 'payload/types'\n\nexport const Orders: CollectionConfig = {\n slug: 'orders',\n fields: [\n {\n name: 'total',\n type: 'number',\n required: true,\n },\n {\n name: 'placedBy',\n type: 'relationship',\n relationTo: 'customers',\n required: true,\n },\n ],\n}",
|
||||
code: "import { CollectionConfig } from 'payload'\n\nexport const Orders: CollectionConfig = {\n slug: 'orders',\n fields: [\n {\n name: 'total',\n type: 'number',\n required: true,\n },\n {\n name: 'placedBy',\n type: 'relationship',\n relationTo: 'customers',\n required: true,\n },\n ],\n}",
|
||||
language: 'typescript',
|
||||
},
|
||||
format: '',
|
||||
|
||||
@@ -181,7 +181,7 @@ export const post2: Partial<Post> = {
|
||||
id: '664397d9e0ef87fa1fec2fc0',
|
||||
blockName: '',
|
||||
blockType: 'code',
|
||||
code: "import { CollectionConfig } from 'payload/types'\n\nexport const Orders: CollectionConfig = {\n slug: 'orders',\n fields: [\n {\n name: 'total',\n type: 'number',\n required: true,\n },\n {\n name: 'placedBy',\n type: 'relationship',\n relationTo: 'customers',\n required: true,\n },\n ],\n}",
|
||||
code: "import { CollectionConfig } from 'payload'\n\nexport const Orders: CollectionConfig = {\n slug: 'orders',\n fields: [\n {\n name: 'total',\n type: 'number',\n required: true,\n },\n {\n name: 'placedBy',\n type: 'relationship',\n relationTo: 'customers',\n required: true,\n },\n ],\n}",
|
||||
language: 'typescript',
|
||||
},
|
||||
format: '',
|
||||
|
||||
@@ -181,7 +181,7 @@ export const post3: Partial<Post> = {
|
||||
id: '664397d9e0ef87fa1fec2fc0',
|
||||
blockName: '',
|
||||
blockType: 'code',
|
||||
code: "import { CollectionConfig } from 'payload/types'\n\nexport const Orders: CollectionConfig = {\n slug: 'orders',\n fields: [\n {\n name: 'total',\n type: 'number',\n required: true,\n },\n {\n name: 'placedBy',\n type: 'relationship',\n relationTo: 'customers',\n required: true,\n },\n ],\n}",
|
||||
code: "import { CollectionConfig } from 'payload'\n\nexport const Orders: CollectionConfig = {\n slug: 'orders',\n fields: [\n {\n name: 'total',\n type: 'number',\n required: true,\n },\n {\n name: 'placedBy',\n type: 'relationship',\n relationTo: 'customers',\n required: true,\n },\n ],\n}",
|
||||
language: 'typescript',
|
||||
},
|
||||
format: '',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FieldHook } from 'payload/types'
|
||||
import type { FieldHook } from 'payload'
|
||||
|
||||
const format = (val: string): string =>
|
||||
val
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import type { RevalidationType } from '@/next/revalidate/route'
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
export const revalidate = async (args: {
|
||||
collection: string
|
||||
path?: string
|
||||
payload: Payload
|
||||
tag?: string
|
||||
type?: RevalidationType
|
||||
}): Promise<void> => {
|
||||
const { type = 'path', collection, path, payload, tag } = args
|
||||
|
||||
try {
|
||||
const baseUrl = `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/next/revalidate?secret=${process.env.REVALIDATION_KEY}`
|
||||
const url = baseUrl + (type === 'path' ? `&path=${path}` : `&tag=${tag}&type=tag`)
|
||||
|
||||
const res = await fetch(url)
|
||||
|
||||
if (res.ok) {
|
||||
switch (type) {
|
||||
case 'path':
|
||||
payload.logger.info(`Revalidated path '${path}' in collection '${collection}'`)
|
||||
break
|
||||
case 'tag':
|
||||
payload.logger.info(`Revalidated tag '${tag}'`)
|
||||
break
|
||||
}
|
||||
} else {
|
||||
switch (type) {
|
||||
case 'path':
|
||||
payload.logger.error(
|
||||
`Error revalidating path '${path}' in collection '${collection}`,
|
||||
res,
|
||||
)
|
||||
break
|
||||
case 'tag':
|
||||
payload.logger.error(`Error revalidating tag '${tag}' in '${collection}`, res)
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
switch (type) {
|
||||
case 'path':
|
||||
payload.logger.error(`Error revalidating path '${path}' in collection '${collection}`, err)
|
||||
break
|
||||
case 'tag':
|
||||
payload.logger.error(`Error revalidating tag '${tag}' in '${collection}`, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
"@/*": ["./src/app/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "redirects.js", "next.config.js"],
|
||||
"exclude": ["node_modules"],
|
||||
"ts-node": {
|
||||
"transpileOnly": true,
|
||||
|
||||
Reference in New Issue
Block a user