Compare commits
94 Commits
chore/ci-e
...
feat/kv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0124afea55 | ||
|
|
466f109152 | ||
|
|
0588394a47 | ||
|
|
3bbc5bf949 | ||
|
|
154ad9d132 | ||
|
|
ed0d3395c7 | ||
|
|
68b5f61641 | ||
|
|
1372f2468d | ||
|
|
235eade08f | ||
|
|
e9a330d4b9 | ||
|
|
b08ff88fbd | ||
|
|
08eb13d189 | ||
|
|
f1901f6648 | ||
|
|
a58b9fc230 | ||
|
|
ec853c4336 | ||
|
|
2175451480 | ||
|
|
957867f6e2 | ||
|
|
4e953530df | ||
|
|
99481cbc7d | ||
|
|
4d50046104 | ||
|
|
a7109ed048 | ||
|
|
dec87e971a | ||
|
|
a501e604d6 | ||
|
|
6bba7bec4e | ||
|
|
2a6bac97f5 | ||
|
|
52b1a9a720 | ||
|
|
7bedd6d822 | ||
|
|
59fc9d094e | ||
|
|
7c4ea5b86e | ||
|
|
7292220109 | ||
|
|
dd3c2eb42b | ||
|
|
b3308736c4 | ||
|
|
46e50c4572 | ||
|
|
d03658de01 | ||
|
|
07be617963 | ||
|
|
d8c106cb2b | ||
|
|
e468292039 | ||
|
|
034b442699 | ||
|
|
6a8aecadf8 | ||
|
|
23f1ed4a48 | ||
|
|
ba0e7aeee5 | ||
|
|
5753efb0a4 | ||
|
|
12dad35cf9 | ||
|
|
997aed346f | ||
|
|
0c57eef621 | ||
|
|
1d46b6d738 | ||
|
|
03ff77544e | ||
|
|
0e5bda9a74 | ||
|
|
eee6432715 | ||
|
|
044c22de54 | ||
|
|
ce74f1b238 | ||
|
|
605cf42cbe | ||
|
|
97c120ab28 | ||
|
|
97a1f4afa9 | ||
|
|
439dd04ce9 | ||
|
|
a3457af36d | ||
|
|
d0d7b51ed5 | ||
|
|
ef90ebb395 | ||
|
|
194a8c189a | ||
|
|
1446fe4694 | ||
|
|
f29e6335f6 | ||
|
|
93dde52fa9 | ||
|
|
198763a24e | ||
|
|
b8de401195 | ||
|
|
2ee3e30b50 | ||
|
|
61c5e0d3e0 | ||
|
|
4bfa329fa4 | ||
|
|
f5c13deb24 | ||
|
|
70666a0f7b | ||
|
|
b0b2fc6c47 | ||
|
|
eb037a0cc6 | ||
|
|
99ca1babc6 | ||
|
|
13e050582b | ||
|
|
77871050b8 | ||
|
|
e04be4bf62 | ||
|
|
7037983de0 | ||
|
|
29ad1fcb77 | ||
|
|
2d2a52b00f | ||
|
|
3f35d36934 | ||
|
|
41167bfbeb | ||
|
|
ed44ec0a9c | ||
|
|
fa49e04cf8 | ||
|
|
f54e180370 | ||
|
|
c50f4237a4 | ||
|
|
b0d648bf30 | ||
|
|
8258d5c943 | ||
|
|
0f63db055b | ||
|
|
12fa4fd2f9 | ||
|
|
6dea111d28 | ||
|
|
26a10ed071 | ||
|
|
727fba7b1c | ||
|
|
c187bff581 | ||
|
|
00909ec5c4 | ||
|
|
2ec4d0c2ef |
12
.github/workflows/main.yml
vendored
12
.github/workflows/main.yml
vendored
@@ -180,6 +180,7 @@ jobs:
|
||||
- postgres-uuid
|
||||
- supabase
|
||||
- sqlite
|
||||
- sqlite-uuid
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
@@ -202,6 +203,13 @@ jobs:
|
||||
# needed because the postgres container does not provide a healthcheck
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
ports:
|
||||
- 6379:6379 # Expose Redis on port 6379
|
||||
|
||||
options: --health-cmd "redis-cli ping" --health-timeout 30s --health-retries 3
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -252,6 +260,10 @@ jobs:
|
||||
echo "POSTGRES_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres" >> $GITHUB_ENV
|
||||
if: matrix.database == 'supabase'
|
||||
|
||||
- name: Configure Redis
|
||||
run: |
|
||||
echo "REDIS_URL=redis://127.0.0.1:6379" >> $GITHUB_ENV
|
||||
|
||||
- name: Integration Tests
|
||||
uses: nick-fields/retry@v3
|
||||
env:
|
||||
|
||||
2
.github/workflows/pr-title.yml
vendored
2
.github/workflows/pr-title.yml
vendored
@@ -41,7 +41,9 @@ jobs:
|
||||
db-vercel-postgres
|
||||
db-sqlite
|
||||
drizzle
|
||||
email-\*
|
||||
email-nodemailer
|
||||
email-resend
|
||||
eslint
|
||||
graphql
|
||||
live-preview
|
||||
|
||||
16
.github/workflows/stale.yml
vendored
16
.github/workflows/stale.yml
vendored
@@ -25,19 +25,16 @@ jobs:
|
||||
with:
|
||||
debug-only: ${{ inputs.dry-run || false }}
|
||||
|
||||
days-before-stale: 23
|
||||
days-before-close: 7
|
||||
days-before-pr-close: -1 # Never close PRs
|
||||
days-before-stale: 30
|
||||
days-before-close: -1 # Disable closing
|
||||
ascending: true
|
||||
operations-per-run: 300
|
||||
exempt-all-assignees: true
|
||||
|
||||
# Issues
|
||||
stale-issue-label: stale
|
||||
exempt-issue-labels: 'prioritized,keep,created-by: Payload team,created-by: Contributor,status: verified'
|
||||
stale-issue-message: |
|
||||
This issue has been marked as stale due to lack of activity.
|
||||
|
||||
To keep this issue open, please indicate that it is still relevant in a comment below.
|
||||
stale-issue-message: ''
|
||||
|
||||
close-issue-message: |
|
||||
This issue was automatically closed due to lack of activity.
|
||||
@@ -45,10 +42,7 @@ jobs:
|
||||
# Pull Requests
|
||||
stale-pr-label: stale
|
||||
exempt-pr-labels: 'prioritized,keep,created-by: Payload team,created-by: Contributor'
|
||||
stale-pr-message: |
|
||||
This PR is stale due to lack of activity.
|
||||
|
||||
To keep the PR open, please indicate that it is still relevant in a comment below.
|
||||
stale-pr-message: ''
|
||||
close-pr-message: |
|
||||
This pull request was automatically closed due to lack of activity.
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ If a Collection supports [Versions](../versions/overview), the following additio
|
||||
|
||||
Returns a boolean which allows/denies access to the `create` request.
|
||||
|
||||
To add create Access Control to a Collection, use the `create` property in the [Collection Config](../collections/overview):
|
||||
To add create Access Control to a Collection, use the `create` property in the [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
@@ -104,7 +104,7 @@ The following arguments are provided to the `create` function:
|
||||
|
||||
Returns a boolean which allows/denies access to the `read` request.
|
||||
|
||||
To add read Access Control to a Collection, use the `read` property in the [Collection Config](../collections/overview):
|
||||
To add read Access Control to a Collection, use the `read` property in the [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
@@ -158,7 +158,7 @@ The following arguments are provided to the `read` function:
|
||||
|
||||
Returns a boolean which allows/denies access to the `update` request.
|
||||
|
||||
To add update Access Control to a Collection, use the `update` property in the [Collection Config](../collections/overview):
|
||||
To add update Access Control to a Collection, use the `update` property in the [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
@@ -208,7 +208,7 @@ The following arguments are provided to the `update` function:
|
||||
|
||||
Similarly to the Update function, returns a boolean or a [query constraint](/docs/queries/overview) to limit which documents can be deleted by which users.
|
||||
|
||||
To add delete Access Control to a Collection, use the `delete` property in the [Collection Config](../collections/overview):
|
||||
To add delete Access Control to a Collection, use the `delete` property in the [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
@@ -288,7 +288,7 @@ The following arguments are provided to the `admin` function:
|
||||
|
||||
Determines which users can [unlock](/docs/authentication/operations#unlock) other users who may be blocked from authenticating successfully due to [failing too many login attempts](/docs/authentication/overview#options).
|
||||
|
||||
To add Unlock Access Control to a Collection, use the `unlock` property in the [Collection Config](../collections/overview):
|
||||
To add Unlock Access Control to a Collection, use the `unlock` property in the [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Global-level Access Control is specified within each Global's `access` pro
|
||||
keywords: globals, access control, permissions, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Global Access Control is [Access Control](../overview) used to restrict access to [Global](../globals/overview) Documents, as well as what they can and cannot see within the [Admin Panel](../admin/overview) as it relates to that Global.
|
||||
Global Access Control is [Access Control](../overview) used to restrict access to [Global](../configuration/globals) Documents, as well as what they can and cannot see within the [Admin Panel](../admin/overview) as it relates to that Global.
|
||||
|
||||
To add Access Control to a Global, use the `access` property in your [Global Config](../configuration/globals):
|
||||
|
||||
@@ -25,7 +25,7 @@ export const GlobalWithAccessControl: GlobalConfig = {
|
||||
|
||||
Access Control is specific to the operation of the request.
|
||||
|
||||
To add Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../globals/overview):
|
||||
To add Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../configuration/globals):
|
||||
|
||||
```ts
|
||||
import { GlobalConfig } from 'payload'
|
||||
@@ -63,7 +63,7 @@ If a Global supports [Versions](../versions/overview), the following additional
|
||||
|
||||
Returns a boolean result or optionally a [query constraint](../queries/overview) which limits who can read this global based on its current properties.
|
||||
|
||||
To add read Access Control to a [Global](../configuration/globals), use the `read` property in the [Global Config](../globals/overview):
|
||||
To add read Access Control to a [Global](../configuration/globals), use the `read` property in the [Global Config](../configuration/globals):
|
||||
|
||||
```ts
|
||||
import { GlobalConfig } from 'payload'
|
||||
@@ -90,7 +90,7 @@ The following arguments are provided to the `read` function:
|
||||
|
||||
Returns a boolean result or optionally a [query constraint](../queries/overview) which limits who can update this global based on its current properties.
|
||||
|
||||
To add update Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../globals/overview):
|
||||
To add update Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../configuration/globals):
|
||||
|
||||
```ts
|
||||
import { GlobalConfig } from 'payload'
|
||||
@@ -118,7 +118,7 @@ The following arguments are provided to the `update` function:
|
||||
|
||||
If the Global has [Versions](../versions/overview) enabled, the `readVersions` Access Control function determines whether or not the currently logged in user can access the version history of a Document.
|
||||
|
||||
To add Read Versions Access Control to a Collection, use the `readVersions` property in the [Global Config](../globals/overview):
|
||||
To add Read Versions Access Control to a Collection, use the `readVersions` property in the [Global Config](../configuration/globals):
|
||||
|
||||
```ts
|
||||
import type { GlobalConfig } from 'payload'
|
||||
|
||||
@@ -198,7 +198,7 @@ The following options are available:
|
||||
|
||||
Global Views are views that are scoped under the `/globals` route, such as the Document Edit View.
|
||||
|
||||
To swap out Global Views with your own or [create entirely new ones](#adding-new-views), use the `admin.components.views` property in your [Global Config](../globals/overview):
|
||||
To swap out Global Views with your own or [create entirely new ones](#adding-new-views), use the `admin.components.views` property in your [Global Config](../configuration/globals):
|
||||
|
||||
```ts
|
||||
import type { SanitizedGlobalConfig } from 'payload'
|
||||
@@ -248,7 +248,7 @@ The following options are available:
|
||||
|
||||
Document Views are views that are scoped under the `/collections/:collectionSlug/:id` or the `/globals/:globalSlug` route, such as the Edit View or the API View. All Document Views keep their overall structure across navigation changes, such as their title and tabs, and replace only the content below.
|
||||
|
||||
To swap out Document Views with your own, or to [create entirely new ones](#adding-new-document-views), use the `admin.components.views.Edit[key]` property in your [Collection Config](../collections/overview) or [Global Config](../globals/overview):
|
||||
To swap out Document Views with your own, or to [create entirely new ones](#adding-new-document-views), use the `admin.components.views.Edit[key]` property in your [Collection Config](../collections/overview) or [Global Config](../configuration/globals):
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
@@ -45,7 +45,7 @@ import type { CollectionConfig } from 'payload'
|
||||
export const UsersWithoutJWTs: CollectionConfig = {
|
||||
slug: 'users-without-jwts',
|
||||
auth: {
|
||||
removeTokenFromRepsonse: true, // highlight-line
|
||||
removeTokenFromResponse: true, // highlight-line
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -79,7 +79,7 @@ The following options are available:
|
||||
| **`loggingLevels`** | An object to override the level to use in the logger for Payload's errors. |
|
||||
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
|
||||
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
|
||||
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/overview#csrf-protection). |
|
||||
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). |
|
||||
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
|
||||
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
|
||||
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
|
||||
|
||||
@@ -30,14 +30,15 @@ export default buildConfig({
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
| -------------------- | ----------- |
|
||||
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
|
||||
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
|
||||
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
|
||||
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
|
||||
| Option | Description |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
|
||||
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
|
||||
| `collectionsSchemaOptions` | Customize Mongoose schema options for collections. |
|
||||
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
|
||||
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
|
||||
|
||||
## Access to Mongoose models
|
||||
|
||||
|
||||
@@ -50,6 +50,11 @@ export default buildConfig({
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
<strong>Note:</strong>
|
||||
If when using `vercelPostgresAdapter` your `process.env.POSTGRES_URL` or `pool.connectionString` points to a local database (e.g hostname has `localhost` or `127.0.0.1`) we use the `pg` module for pooling instead of `@vercel/postgres`. This is because `@vercel/postgres` doesn't work with local databases, if you want to disable that behavior, you can pass `forceUseVercelPostgres: true` to adapter's 'args and follow [Vercel guide](https://vercel.com/docs/storage/vercel-postgres/local-development#option-2:-local-postgres-instance-with-docker) for a Docker Neon DB setup.
|
||||
</Banner>
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
@@ -60,21 +65,33 @@ export default buildConfig({
|
||||
| `schemaName` (experimental) | A string for the postgres schema to use, defaults to 'public'. |
|
||||
| `idType` | A string of 'serial', or 'uuid' that is used for the data type given to id columns. |
|
||||
| `transactionOptions` | A PgTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) |
|
||||
| `disableCreateDatabase` | Pass `true` to disable auto database creation if it doesn't exist. Defaults to `false`. |
|
||||
| `disableCreateDatabase` | Pass `true` to disable auto database creation if it doesn't exist. Defaults to `false`. |
|
||||
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
|
||||
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
|
||||
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
|
||||
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
|
||||
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
|
||||
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
|
||||
|
||||
## Access to Drizzle
|
||||
|
||||
After Payload is initialized, this adapter will expose the full power of Drizzle to you for use if you need it.
|
||||
|
||||
You can access Drizzle as follows:
|
||||
To ensure type-safety, you need to generate Drizzle schema first with:
|
||||
```sh
|
||||
npx payload generate:db-schema
|
||||
```
|
||||
|
||||
```text
|
||||
payload.db.drizzle
|
||||
Then, you can access Drizzle as follows:
|
||||
```ts
|
||||
import { posts } from './payload-generated-schema'
|
||||
// To avoid installing Drizzle, you can import everything that drizzle has from our re-export path.
|
||||
import { eq, sql, and } from '@payloadcms/db-postgres/drizzle'
|
||||
|
||||
// Drizzle's Querying API: https://orm.drizzle.team/docs/rqb
|
||||
const posts = await payload.db.drizzle.query.posts.findMany()
|
||||
// Drizzle's Select API https://orm.drizzle.team/docs/select
|
||||
const result = await payload.db.drizzle.select().from(posts).where(and(eq(posts.id, 50), sql`lower(${posts.title}) = 'example post title'`))
|
||||
```
|
||||
|
||||
## Tables, relations, and enums
|
||||
@@ -109,7 +126,7 @@ Runs before the schema is built. You can use this hook to extend your database s
|
||||
|
||||
```ts
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { integer, pgTable, serial } from 'drizzle-orm/pg-core'
|
||||
import { integer, pgTable, serial } from '@payloadcms/db-postgres/drizzle/pg-core'
|
||||
|
||||
postgresAdapter({
|
||||
beforeSchemaInit: [
|
||||
@@ -178,7 +195,7 @@ postgresAdapter({
|
||||
})
|
||||
```
|
||||
|
||||
Make sure Payload doesn't overlap table names with its collections. For example, if you already have a collection with slug "users", you should either change the slug or `dbName` to change the table name for this collection.
|
||||
Make sure Payload doesn't overlap table names with its collections. For example, if you already have a collection with slug "users", you should either change the slug or `dbName` to change the table name for this collection.
|
||||
|
||||
|
||||
### afterSchemaInit
|
||||
@@ -189,7 +206,7 @@ The following example adds the `extra_integer_column` column and a composite ind
|
||||
|
||||
```ts
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { index, integer } from 'drizzle-orm/pg-core'
|
||||
import { index, integer } from '@payloadcms/db-postgres/drizzle/pg-core'
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
@@ -231,3 +248,43 @@ export default buildConfig({
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
### Note for generated schema:
|
||||
Columns and tables, added in schema hooks won't be added to the generated via `payload generate:db-schema` Drizzle schema.
|
||||
If you want them to be there, you either have to edit this file manually or mutate the internal Payload "raw" SQL schema in the `beforeSchemaInit`:
|
||||
|
||||
```ts
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
|
||||
postgresAdapter({
|
||||
beforeSchemaInit: [
|
||||
({ schema, adapter }) => {
|
||||
// Add a new table
|
||||
schema.rawTables.myTable = {
|
||||
name: 'my_table',
|
||||
columns: [{
|
||||
name: 'my_id',
|
||||
type: 'serial',
|
||||
primaryKey: true
|
||||
}],
|
||||
}
|
||||
|
||||
// Add a new column to generated by Payload table:
|
||||
schema.rawTables.posts.columns.customColumn = {
|
||||
name: 'custom_column',
|
||||
// Note that Payload SQL doesn't support everything that Drizzle does.
|
||||
type: 'integer',
|
||||
notNull: true
|
||||
}
|
||||
// Add a new index to generated by Payload table:
|
||||
schema.rawTables.posts.indexes.customColumnIdx = {
|
||||
name: 'custom_column_idx',
|
||||
unique: true,
|
||||
on: ['custom_column']
|
||||
}
|
||||
|
||||
return schema
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
@@ -34,27 +34,42 @@ export default buildConfig({
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `client` \* | [Client connection options](https://orm.drizzle.team/docs/get-started-sqlite#turso) that will be passed to `createClient` from `@libsql/client`. |
|
||||
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. |
|
||||
| `transactionOptions` | A SQLiteTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) |
|
||||
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
|
||||
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
|
||||
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
|
||||
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
|
||||
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
|
||||
| Option | Description |
|
||||
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `client` \* | [Client connection options](https://orm.drizzle.team/docs/get-started-sqlite#turso) that will be passed to `createClient` from `@libsql/client`. |
|
||||
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. |
|
||||
| `idType` | A string of 'number', or 'uuid' that is used for the data type given to id columns. |
|
||||
| `transactionOptions` | A SQLiteTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) |
|
||||
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
|
||||
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
|
||||
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
|
||||
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
|
||||
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
|
||||
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
|
||||
| `autoIncrement` | Pass `true` to enable SQLite [AUTOINCREMENT](https://www.sqlite.org/autoinc.html) for primary keys to ensure the same ID cannot be reused from deleted rows |
|
||||
|
||||
## Access to Drizzle
|
||||
|
||||
After Payload is initialized, this adapter will expose the full power of Drizzle to you for use if you need it.
|
||||
|
||||
You can access Drizzle as follows:
|
||||
To ensure type-safety, you need to generate Drizzle schema first with:
|
||||
```sh
|
||||
npx payload generate:db-schema
|
||||
```
|
||||
|
||||
```text
|
||||
payload.db.drizzle
|
||||
Then, you can access Drizzle as follows:
|
||||
```ts
|
||||
// Import table from the generated file
|
||||
import { posts } from './payload-generated-schema'
|
||||
// To avoid installing Drizzle, you can import everything that drizzle has from our re-export path.
|
||||
import { eq, sql, and } from '@payloadcms/db-sqlite/drizzle'
|
||||
|
||||
// Drizzle's Querying API: https://orm.drizzle.team/docs/rqb
|
||||
const posts = await payload.db.drizzle.query.posts.findMany()
|
||||
// Drizzle's Select API https://orm.drizzle.team/docs/select
|
||||
const result = await payload.db.drizzle.select().from(posts).where(and(eq(posts.id, 50), sql`lower(${posts.title}) = 'example post title'`))
|
||||
```
|
||||
|
||||
## Tables and relations
|
||||
@@ -88,7 +103,7 @@ Runs before the schema is built. You can use this hook to extend your database s
|
||||
|
||||
```ts
|
||||
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
||||
import { integer, sqliteTable } from 'drizzle-orm/sqlite-core'
|
||||
import { integer, sqliteTable } from '@payloadcms/db-sqlite/drizzle/sqlite-core'
|
||||
|
||||
sqliteAdapter({
|
||||
beforeSchemaInit: [
|
||||
@@ -168,7 +183,7 @@ The following example adds the `extra_integer_column` column and a composite ind
|
||||
|
||||
```ts
|
||||
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
||||
import { index, integer } from 'drizzle-orm/sqlite-core'
|
||||
import { index, integer } from '@payloadcms/db-sqlite/drizzle/sqlite-core'
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
@@ -210,3 +225,43 @@ export default buildConfig({
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
### Note for generated schema:
|
||||
Columns and tables, added in schema hooks won't be added to the generated via `payload generate:db-schema` Drizzle schema.
|
||||
If you want them to be there, you either have to edit this file manually or mutate the internal Payload "raw" SQL schema in the `beforeSchemaInit`:
|
||||
|
||||
```ts
|
||||
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
||||
|
||||
sqliteAdapter({
|
||||
beforeSchemaInit: [
|
||||
({ schema, adapter }) => {
|
||||
// Add a new table
|
||||
schema.rawTables.myTable = {
|
||||
name: 'my_table',
|
||||
columns: [{
|
||||
name: 'my_id',
|
||||
type: 'integer',
|
||||
primaryKey: true
|
||||
}],
|
||||
}
|
||||
|
||||
// Add a new column to generated by Payload table:
|
||||
schema.rawTables.posts.columns.customColumn = {
|
||||
name: 'custom_column',
|
||||
// Note that Payload SQL doesn't support everything that Drizzle does.
|
||||
type: 'integer',
|
||||
notNull: true
|
||||
}
|
||||
// Add a new index to generated by Payload table:
|
||||
schema.rawTables.posts.indexes.customColumnIdx = {
|
||||
name: 'custom_column_idx',
|
||||
unique: true,
|
||||
on: ['custom_column']
|
||||
}
|
||||
|
||||
return schema
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
@@ -125,8 +125,8 @@ powerful Admin UI.
|
||||
|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`name`** \* | To be used as the property name when retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`collection`** \* | The `slug`s having the relationship field. |
|
||||
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
|
||||
| **`where`** \* | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
|
||||
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
|
||||
| **`where`** | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
|
||||
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/getting-started/concepts#field-level-max-depth). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
@@ -145,10 +145,11 @@ _\* An asterisk denotes that a property is required._
|
||||
|
||||
You can control the user experience of the join field using the `admin` config properties. The following options are supported:
|
||||
|
||||
| Option | Description |
|
||||
|------------------------|----------------------------------------------------------------------------------------|
|
||||
| **`allowCreate`** | Set to `false` to remove the controls for making new related documents from this field. |
|
||||
| **`components.Label`** | Override the default Label of the Field Component. [More details](../admin/fields#label) |
|
||||
| Option | Description |
|
||||
|------------------------|---------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`defaultColumns`** | Array of field names that correspond to which columns to show in the relationship table. Default is the collection config. |
|
||||
| **`allowCreate`** | Set to `false` to remove the controls for making new related documents from this field. |
|
||||
| **`components.Label`** | Override the default Label of the Field Component. [More details](../admin/fields#label) |
|
||||
|
||||
## Join Field Data
|
||||
|
||||
|
||||
@@ -212,6 +212,7 @@ Functions can be written to make use of the following argument properties:
|
||||
|
||||
- `user` - the authenticated user object
|
||||
- `locale` - the currently selected locale string
|
||||
- `req` - the `PayloadRequest` object
|
||||
|
||||
Here is an example of a `defaultValue` function:
|
||||
|
||||
@@ -227,7 +228,7 @@ export const myField: Field = {
|
||||
name: 'attribution',
|
||||
type: 'text',
|
||||
// highlight-start
|
||||
defaultValue: ({ user, locale }) =>
|
||||
defaultValue: ({ user, locale, req }) =>
|
||||
`${translation[locale]} ${user.name}`,
|
||||
// highlight-end
|
||||
}
|
||||
@@ -235,7 +236,7 @@ export const myField: Field = {
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
You can use async `defaultValue` functions to fill fields with data from API requests.
|
||||
You can use async `defaultValue` functions to fill fields with data from API requests or Local API using `req.payload`.
|
||||
</Banner>
|
||||
|
||||
### Validation
|
||||
|
||||
@@ -68,11 +68,12 @@ export const MyTextareaField: Field = {
|
||||
|
||||
The Textarea Field inherits all of the default options from the base [Field Admin Config](../admin/fields#admin-options), plus the following additional options:
|
||||
|
||||
| Option | Description |
|
||||
| -------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| **`placeholder`** | Set this property to define a placeholder string in the textarea. |
|
||||
| **`autoComplete`** | Set this property to a string that will be used for browser autocomplete. |
|
||||
| **`rtl`** | Override the default text direction of the Admin Panel for this field. Set to `true` to force right-to-left text direction. |
|
||||
| Option | Description |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`placeholder`** | Set this property to define a placeholder string in the textarea. |
|
||||
| **`autoComplete`** | Set this property to a string that will be used for browser autocomplete. |
|
||||
| **`rows`** | Set the number of visible text rows in the textarea. Defaults to `2` if not specified. |
|
||||
| **`rtl`** | Override the default text direction of the Admin Panel for this field. Set to `true` to force right-to-left text direction. |
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
@@ -141,3 +141,65 @@ export const createPostHandler: TaskHandler<'createPost'> = async ({ input, job,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuring task restoration
|
||||
|
||||
By default, if a task has passed previously and a workflow is re-run, the task will not be re-run. Instead, the output from the previous task run will be returned. This is to prevent unnecessary re-runs of tasks that have already passed.
|
||||
|
||||
You can configure this behavior through the `retries.shouldRestore` property. This property accepts a boolean or a function.
|
||||
|
||||
If `shouldRestore` is set to true, the task will only be re-run if it previously failed. This is the default behavior.
|
||||
|
||||
If `shouldRestore` this is set to false, the task will be re-run even if it previously succeeded, ignoring the maximum number of retries.
|
||||
|
||||
If `shouldRestore` is a function, the return value of the function will determine whether the task should be re-run. This can be used for more complex restore logic, e.g you may want to re-run a task up to X amount of times and then restore it for consecutive runs, or only re-run a task if the input has changed.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// ...
|
||||
jobs: {
|
||||
tasks: [
|
||||
{
|
||||
slug: 'myTask',
|
||||
retries: {
|
||||
shouldRestore: false,
|
||||
}
|
||||
// ...
|
||||
} as TaskConfig<'myTask'>,
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Example - determine whether a task should be restored based on the input data:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// ...
|
||||
jobs: {
|
||||
tasks: [
|
||||
{
|
||||
slug: 'myTask',
|
||||
inputSchema: [
|
||||
{
|
||||
name: 'someDate',
|
||||
type: 'date',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
retries: {
|
||||
shouldRestore: ({ input }) => {
|
||||
if(new Date(input.someDate) > new Date()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
// ...
|
||||
} as TaskConfig<'myTask'>,
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -196,7 +196,7 @@ const examplePageConfig: CollectionConfig = {
|
||||
},
|
||||
// Note: if you override the `filterOptions` of the `parent` field,
|
||||
// be sure to continue to prevent the document from referencing itself as the parent like this:
|
||||
// filterOptions: ({ id }) => ({ id: {not_equals: id }})`
|
||||
// filterOptions: ({ id }) => ({ id: {not_equals: id }})
|
||||
},
|
||||
),
|
||||
createBreadcrumbsField(
|
||||
|
||||
@@ -20,7 +20,7 @@ Querying a collection and automatically including related documents via `depth`
|
||||
|
||||
## Cross-Site Request Forgery (CSRF)
|
||||
|
||||
CSRF prevention will verify the authenticity of each request to your API to prevent a malicious action from another site from authorized users. See how to configure CSRF [here](/docs/authentication/overview#csrf-protection).
|
||||
CSRF prevention will verify the authenticity of each request to your API to prevent a malicious action from another site from authorized users. See how to configure CSRF [here](/docs/authentication/cookies#csrf-attacks).
|
||||
|
||||
## Cross Origin Resource Sharing (CORS)
|
||||
|
||||
@@ -38,7 +38,7 @@ If you do not need GraphQL it is advised that you disable it altogether with the
|
||||
|
||||
Payload does not execute uploaded files on the server, but depending on your setup it may be used to transmit and store potentially dangerous files. If your configuration allows file uploads there is the potential that a bad actor uploads a malicious file that is then served to other users. Consider the following ways to mitigate the risks.
|
||||
|
||||
First, enable email [verification](/docs/authentication/overview#email-verification) when users are allowed to register new accounts and add other bot prevention services.
|
||||
First, enable email [verification](/docs/authentication/email#email-verification) when users are allowed to register new accounts and add other bot prevention services.
|
||||
|
||||
Review that `create` and `update` access on file upload collections are as restrictive as your application needs allow. Consider limiting `read` access of uploaded user's files and how you might limit user uploaded files from being served outside of Payload.
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ This allows you to add i18n translations scoped to your feature. This specific e
|
||||
|
||||
### Markdown Transformers#server-feature-markdown-transformers
|
||||
|
||||
The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when [converting the editor from or to markdown](/docs/lexical/converters#markdown-lexical).
|
||||
The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when [converting the editor from or to markdown](/docs/rich-text/converters#markdown-lexical).
|
||||
|
||||
```ts
|
||||
import { createServerFeature } from '@payloadcms/richtext-lexical';
|
||||
|
||||
@@ -180,7 +180,7 @@ Notice how even the toolbars are features? That's how extensible our lexical edi
|
||||
|
||||
## Creating your own, custom Feature
|
||||
|
||||
You can find more information about creating your own feature in our [building custom feature docs](/docs/lexical/building-custom-features).
|
||||
You can find more information about creating your own feature in our [building custom feature docs](/docs/rich-text/building-custom-features).
|
||||
|
||||
## TypeScript
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# NOTE: Change port of `PAYLOAD_PUBLIC_SITE_URL` if front-end is running on another server
|
||||
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3000
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
# Database connection string
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-draft-preview-example
|
||||
|
||||
# Used to encrypt JWT tokens
|
||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||
|
||||
# Used to configure CORS, format links and more. No trailing slash
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-example-auth
|
||||
PAYLOAD_SECRET=PAYLOAD_AUTH_EXAMPLE_SECRET_KEY
|
||||
|
||||
# Used to share cookies across subdomains
|
||||
COOKIE_DOMAIN=localhost
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
module.exports = {
|
||||
extends: ['plugin:@next/next/core-web-vitals', '@payloadcms'],
|
||||
ignorePatterns: ['**/payload-types.ts'],
|
||||
overrides: [
|
||||
{
|
||||
extends: ['plugin:@typescript-eslint/disable-type-checked'],
|
||||
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
root: true,
|
||||
extends: ['@payloadcms'],
|
||||
}
|
||||
|
||||
3
examples/auth/.gitignore
vendored
3
examples/auth/.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
build
|
||||
dist
|
||||
node_modules
|
||||
package - lock.json.env
|
||||
package-lock.json
|
||||
.env
|
||||
|
||||
24
examples/auth/.swcrc
Normal file
24
examples/auth/.swcrc
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"sourceMaps": true,
|
||||
"jsc": {
|
||||
"target": "esnext",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true,
|
||||
"dts": true
|
||||
},
|
||||
"transform": {
|
||||
"react": {
|
||||
"runtime": "automatic",
|
||||
"pragmaFrag": "React.Fragment",
|
||||
"throwIfNamespace": true,
|
||||
"development": false,
|
||||
"useBuiltins": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,27 @@
|
||||
|
||||
This [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) demonstrates how to implement [Payload Authentication](https://payloadcms.com/docs/authentication/overview) into all types of applications. Follow the [Quick Start](#quick-start) to get up and running quickly.
|
||||
|
||||
**IMPORTANT—This example includes a fully integrated Next.js App Router front-end that runs on the same server as Payload.** If you are working on an application running on an entirely separate server, the principals are generally the same. To learn more about this, [check out how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
|
||||
|
||||
## Quick Start
|
||||
|
||||
To spin up this example locally, follow these steps:
|
||||
To spin up this example locally, follow the steps below:
|
||||
|
||||
1. Clone this repo
|
||||
1. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
1. Navigate into the project directory and install dependencies using your preferred package manager:
|
||||
|
||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
||||
- `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
|
||||
1. `cp .env.example .env` to copy the example environment variables
|
||||
> \*NOTE: The --ignore-workspace flag is needed if you are running this example within the Payload monorepo to avoid workspace conflicts.
|
||||
|
||||
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
|
||||
1. Start the server:
|
||||
- Depending on your package manager, run `pnpm dev`, `yarn dev` or `npm run dev`
|
||||
- When prompted, type `y` then `enter` to seed the database with sample data
|
||||
1. Access the application:
|
||||
- Open your browser and navigate to `http://localhost:3000` to access the homepage.
|
||||
- Open `http://localhost:3000/admin` to access the admin panel.
|
||||
1. Login:
|
||||
|
||||
1. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
- Press `y` when prompted to seed the database
|
||||
1. `open http://localhost:3000` to access the home page
|
||||
1. `open http://localhost:3000/admin` to access the admin panel
|
||||
- Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
|
||||
- Use the following credentials to log into the admin panel:
|
||||
> `Email: demo@payloadcms.com` > `Password: demo`
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -45,7 +44,7 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
|
||||
import config from '../../payload.config'
|
||||
|
||||
export default async function AccountPage({ searchParams }) {
|
||||
const headers = getHeaders()
|
||||
const headers = await getHeaders()
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
const { permissions, user } = await payload.auth({ headers })
|
||||
|
||||
|
||||
2
examples/auth/next-env.d.ts
vendored
2
examples/auth/next-env.d.ts
vendored
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -3,41 +3,39 @@
|
||||
"version": "1.0.0",
|
||||
"description": "Payload authentication example.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
"build": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts NODE_OPTIONS=--no-deprecation next build",
|
||||
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts && pnpm seed && cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
|
||||
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
|
||||
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src",
|
||||
"payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload",
|
||||
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
|
||||
"dev": "cross-env NODE_OPTIONS=--no-deprecation && pnpm seed && next dev",
|
||||
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
||||
"generate:schema": "payload-graphql generate:schema",
|
||||
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
|
||||
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||
"seed": "npm run payload migrate:fresh",
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/db-mongodb": "3.0.0-beta.24",
|
||||
"@payloadcms/next": "3.0.0-beta.24",
|
||||
"@payloadcms/richtext-slate": "3.0.0-beta.24",
|
||||
"@payloadcms/ui": "3.0.0-beta.24",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/next": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"@payloadcms/ui": "latest",
|
||||
"cross-env": "^7.0.3",
|
||||
"next": "14.3.0-canary.68",
|
||||
"payload": "3.0.0-beta.24",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"next": "^15.0.0",
|
||||
"payload": "latest",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-hook-form": "^7.51.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^13.1.6",
|
||||
"@payloadcms/eslint-config": "^1.1.1",
|
||||
"@swc/core": "^1.4.14",
|
||||
"@swc/types": "^0.1.6",
|
||||
"@types/node": "^20.11.25",
|
||||
"@types/react": "^18.2.64",
|
||||
"@types/react-dom": "^18.2.21",
|
||||
"dotenv": "^16.4.5",
|
||||
"@payloadcms/graphql": "latest",
|
||||
"@swc/core": "^1.6.13",
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.1",
|
||||
"eslint": "^8.57.0",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "5.4.4"
|
||||
"eslint-config-next": "^15.0.0",
|
||||
"tsx": "^4.16.2",
|
||||
"typescript": "5.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
10841
examples/auth/pnpm-lock.yaml
generated
10841
examples/auth/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
31
examples/auth/src/app/(app)/_components/Header/index.tsx
Normal file
31
examples/auth/src/app/(app)/_components/Header/index.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import { Gutter } from '../Gutter'
|
||||
import { HeaderNav } from './Nav'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const Header = () => {
|
||||
return (
|
||||
<header className={classes.header}>
|
||||
<Gutter className={classes.wrap}>
|
||||
<Link className={classes.logo} href="/">
|
||||
<picture>
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcSet="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-light.svg"
|
||||
/>
|
||||
<Image
|
||||
alt="Payload Logo"
|
||||
height={30}
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-dark.svg"
|
||||
width={150}
|
||||
/>
|
||||
</picture>
|
||||
</Link>
|
||||
<HeaderNav />
|
||||
</Gutter>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { AccountForm } from './AccountForm'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export default async function Account() {
|
||||
const headers = getHeaders()
|
||||
const headers = await getHeaders()
|
||||
const payload = await getPayload({ config })
|
||||
const { permissions, user } = await payload.auth({ headers })
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { CreateAccountForm } from './CreateAccountForm'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export default async function CreateAccount() {
|
||||
const headers = getHeaders()
|
||||
const headers = await getHeaders()
|
||||
const payload = await getPayload({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import classes from './index.module.scss'
|
||||
import { LoginForm } from './LoginForm'
|
||||
|
||||
export default async function Login() {
|
||||
const headers = getHeaders()
|
||||
const headers = await getHeaders()
|
||||
const payload = await getPayload({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import classes from './index.module.scss'
|
||||
import { LogoutPage } from './LogoutPage'
|
||||
|
||||
export default async function Logout() {
|
||||
const headers = getHeaders()
|
||||
const headers = await getHeaders()
|
||||
const payload = await getPayload({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Gutter } from './_components/Gutter'
|
||||
import { HydrateClientUser } from './_components/HydrateClientUser'
|
||||
|
||||
export default async function HomePage() {
|
||||
const headers = getHeaders()
|
||||
const headers = await getHeaders()
|
||||
const payload = await getPayload({ config })
|
||||
const { permissions, user } = await payload.auth({ headers })
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import classes from './index.module.scss'
|
||||
import { RecoverPasswordForm } from './RecoverPasswordForm'
|
||||
|
||||
export default async function RecoverPassword() {
|
||||
const headers = getHeaders()
|
||||
const headers = await getHeaders()
|
||||
const payload = await getPayload({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import classes from './index.module.scss'
|
||||
import { ResetPasswordForm } from './ResetPasswordForm'
|
||||
|
||||
export default async function ResetPassword() {
|
||||
const headers = getHeaders()
|
||||
const headers = await getHeaders()
|
||||
const payload = await getPayload({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
|
||||
@@ -5,18 +5,21 @@ import type { Metadata } from 'next'
|
||||
import config from '@payload-config'
|
||||
import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views'
|
||||
|
||||
import { importMap } from '../importMap.js'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}
|
||||
searchParams: {
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[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 })
|
||||
const NotFound = ({ params, searchParams }: Args) =>
|
||||
NotFoundPage({ config, importMap, params, searchParams })
|
||||
|
||||
export default NotFound
|
||||
|
||||
@@ -5,18 +5,21 @@ import type { Metadata } from 'next'
|
||||
import config from '@payload-config'
|
||||
import { generatePageMetadata, RootPage } from '@payloadcms/next/views'
|
||||
|
||||
import { importMap } from '../importMap.js'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}
|
||||
searchParams: {
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[]
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
|
||||
const Page = ({ params, searchParams }: Args) =>
|
||||
RootPage({ config, importMap, params, searchParams })
|
||||
|
||||
export default Page
|
||||
|
||||
5
examples/auth/src/app/(payload)/admin/importMap.js
Normal file
5
examples/auth/src/app/(payload)/admin/importMap.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { BeforeLogin as BeforeLogin_8a7ab0eb7ab5c511aba12e68480bfe5e } from '@/components/BeforeLogin'
|
||||
|
||||
export const importMap = {
|
||||
'@/components/BeforeLogin#BeforeLogin': BeforeLogin_8a7ab0eb7ab5c511aba12e68480bfe5e,
|
||||
}
|
||||
@@ -1,16 +1,32 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import configPromise from '@payload-config'
|
||||
import type { ServerFunctionClient } from 'payload'
|
||||
|
||||
import '@payloadcms/next/css'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
import config from '@payload-config'
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
|
||||
import { importMap } from './admin/importMap.js'
|
||||
import './custom.scss'
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => <RootLayout config={configPromise}>{children}</RootLayout>
|
||||
const serverFunction: ServerFunctionClient = async function (args) {
|
||||
'use server'
|
||||
return handleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
})
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
export default Layout
|
||||
|
||||
@@ -1,13 +1,62 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
import { admins } from './access/admins'
|
||||
import adminsAndUser from './access/adminsAndUser'
|
||||
import { anyone } from './access/anyone'
|
||||
import { checkRole } from './access/checkRole'
|
||||
import { loginAfterCreate } from './hooks/loginAfterCreate'
|
||||
import { protectRoles } from './hooks/protectRoles'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: {
|
||||
tokenExpiration: 28800, // 8 hours
|
||||
cookies: {
|
||||
sameSite: 'none',
|
||||
secure: true,
|
||||
domain: process.env.COOKIE_DOMAIN,
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
auth: true,
|
||||
access: {
|
||||
read: adminsAndUser,
|
||||
create: anyone,
|
||||
update: adminsAndUser,
|
||||
delete: admins,
|
||||
admin: ({ req: { user } }) => checkRole(['admin'], user),
|
||||
},
|
||||
hooks: {
|
||||
afterChange: [loginAfterCreate],
|
||||
},
|
||||
fields: [
|
||||
// Email added by default
|
||||
// Add more fields as needed
|
||||
{
|
||||
name: 'firstName',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'lastName',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'roles',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
saveToJWT: true,
|
||||
hooks: {
|
||||
beforeChange: [protectRoles],
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: 'User',
|
||||
value: 'user',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
const BeforeLogin: React.FC = () => {
|
||||
export const BeforeLogin: React.FC = () => {
|
||||
if (process.env.PAYLOAD_PUBLIC_SEED === 'true') {
|
||||
return (
|
||||
<p>
|
||||
@@ -13,5 +13,3 @@ const BeforeLogin: React.FC = () => {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export default BeforeLogin
|
||||
|
||||
@@ -7,16 +7,53 @@
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
collections: {
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
@@ -38,6 +75,24 @@ export interface User {
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null;
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
@@ -72,6 +127,63 @@ export interface PayloadMigration {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
firstName?: T;
|
||||
lastName?: T;
|
||||
roles?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
|
||||
@@ -2,28 +2,21 @@ import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
||||
import { slateEditor } from '@payloadcms/richtext-slate'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
import { Users } from './collections/Users'
|
||||
import BeforeLogin from './components/BeforeLogin'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
components: {
|
||||
beforeLogin: [BeforeLogin],
|
||||
beforeLogin: ['@/components/BeforeLogin#BeforeLogin'],
|
||||
},
|
||||
},
|
||||
collections: [Users],
|
||||
cors: [
|
||||
process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
|
||||
process.env.PAYLOAD_PUBLIC_SITE_URL || '',
|
||||
].filter(Boolean),
|
||||
csrf: [
|
||||
process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
|
||||
process.env.PAYLOAD_PUBLIC_SITE_URL || '',
|
||||
].filter(Boolean),
|
||||
cors: [process.env.NEXT_PUBLIC_SERVER_URL || ''].filter(Boolean),
|
||||
csrf: [process.env.NEXT_PUBLIC_SERVER_URL || ''].filter(Boolean),
|
||||
db: mongooseAdapter({
|
||||
url: process.env.DATABASE_URI || '',
|
||||
}),
|
||||
|
||||
@@ -23,10 +23,25 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@payload-config": ["src/payload.config.ts"]
|
||||
}
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"@payload-config": [
|
||||
"src/payload.config.ts"
|
||||
],
|
||||
"@payload-types": [
|
||||
"src/payload-types.ts"
|
||||
]
|
||||
},
|
||||
"target": "ES2022",
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { ServerFunctionClient } from 'payload'
|
||||
|
||||
import '@payloadcms/next/css'
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"dotenv": "^8.2.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"graphql": "^16.9.0",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"next": "^15.0.0",
|
||||
"payload": "latest",
|
||||
"payload-admin-bar": "^1.0.6",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { CollectionSlug } from 'payload'
|
||||
import type { CollectionSlug, PayloadRequest } from 'payload'
|
||||
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { draftMode } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
@@ -42,23 +41,20 @@ export async function GET(
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
if (!path.startsWith('/')) {
|
||||
new Response('This endpoint can only be used for internal previews', { status: 500 })
|
||||
return new Response('This endpoint can only be used for internal previews', { status: 500 })
|
||||
}
|
||||
|
||||
let user
|
||||
|
||||
try {
|
||||
user = jwt.verify(token, payload.secret)
|
||||
} catch (error) {
|
||||
payload.logger.error({
|
||||
err: error,
|
||||
msg: 'Error verifying token for live preview',
|
||||
user = await payload.auth({
|
||||
req: req as unknown as PayloadRequest,
|
||||
headers: req.headers,
|
||||
})
|
||||
} catch (error) {
|
||||
payload.logger.error({ err: error }, 'Error verifying token for live preview')
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
const draft = await draftMode()
|
||||
|
||||
20
examples/localization/.env.example
Normal file
20
examples/localization/.env.example
Normal file
@@ -0,0 +1,20 @@
|
||||
# Database connection string
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-template-website
|
||||
|
||||
# Used to encrypt JWT tokens
|
||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||
|
||||
# Used to format links and URLs
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
|
||||
# Allow robots to index the site (optional)
|
||||
NEXT_PUBLIC_IS_LIVE=
|
||||
|
||||
# Used to preview drafts
|
||||
PAYLOAD_PUBLIC_DRAFT_SECRET=demo-draft-secret
|
||||
NEXT_PRIVATE_DRAFT_SECRET=demo-draft-secret
|
||||
|
||||
# Used to revalidate static pages
|
||||
REVALIDATION_KEY=demo-revalation-key
|
||||
NEXT_PRIVATE_REVALIDATION_KEY=demo-revalation-key
|
||||
8
examples/localization/.eslintrc.cjs
Normal file
8
examples/localization/.eslintrc.cjs
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
extends: 'next',
|
||||
root: true,
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
}
|
||||
10
examples/localization/.gitignore
vendored
Normal file
10
examples/localization/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
build
|
||||
dist / media
|
||||
node_modules
|
||||
.DS_Store
|
||||
.env
|
||||
.next
|
||||
.vercel
|
||||
|
||||
# Payload default media upload directory
|
||||
public/media/
|
||||
14
examples/localization/.prettierignore
Normal file
14
examples/localization/.prettierignore
Normal file
@@ -0,0 +1,14 @@
|
||||
**/payload-types.ts
|
||||
.tmp
|
||||
**/.git
|
||||
**/.hg
|
||||
**/.pnp.*
|
||||
**/.svn
|
||||
**/.yarn/**
|
||||
**/build
|
||||
**/dist/**
|
||||
**/node_modules
|
||||
**/temp
|
||||
**/docs/**
|
||||
tsconfig.json
|
||||
|
||||
6
examples/localization/.prettierrc.json
Normal file
6
examples/localization/.prettierrc.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"semi": false
|
||||
}
|
||||
14
examples/localization/.vscode/launch.json
vendored
Normal file
14
examples/localization/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "yarn dev",
|
||||
"name": "Debug Website",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
24
examples/localization/Dockerfile
Normal file
24
examples/localization/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
FROM node:18.8-alpine as base
|
||||
|
||||
FROM base as builder
|
||||
|
||||
WORKDIR /home/node/app
|
||||
COPY package*.json ./
|
||||
|
||||
COPY . .
|
||||
RUN yarn install
|
||||
RUN yarn build
|
||||
|
||||
FROM base as runtime
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
WORKDIR /home/node/app
|
||||
COPY package*.json ./
|
||||
COPY yarn.lock ./
|
||||
|
||||
RUN yarn install --production
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "dist/server.js"]
|
||||
32
examples/localization/README.md
Normal file
32
examples/localization/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Payload Localization Example (i18n)
|
||||
|
||||
This example is built based on an old version of the website template.
|
||||
|
||||
The objective is to show how to implement localization in a website. There is no guarantee that it will be kept up to date with the website template or the latest Payload enhancements.
|
||||
|
||||
To facilitate the localization process, this example uses the next-intl library.
|
||||
|
||||
## Setup
|
||||
|
||||
1. `cp .env.example .env` (copy the .env.example file to .env)
|
||||
2. `pnpm install` (`pnpm i --ignore-workspaces` if you are running from the monorepo)
|
||||
3. `pnpm run dev`
|
||||
4. Seed your database in the admin panel (see below)
|
||||
|
||||
## Seed
|
||||
|
||||
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 a demo user for demonstration purposes only:
|
||||
|
||||
- Demo Author
|
||||
- Email: `demo-author@payloadcms.com`
|
||||
- Password: `password`
|
||||
|
||||
> 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.
|
||||
|
||||
## Important!
|
||||
|
||||
The seed script only creates translations in English and Spanish, so you will not see the website translated to other languages even if you see them in the dropdown menu.
|
||||
|
||||
You can translate documents to other languages from the admin panel.
|
||||
17
examples/localization/components.json
Normal file
17
examples/localization/components.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/app/(frontend)/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/utilities"
|
||||
}
|
||||
}
|
||||
31
examples/localization/docker-compose.yml
Normal file
31
examples/localization/docker-compose.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
payload:
|
||||
image: node:18-alpine
|
||||
ports:
|
||||
- '3000:3000'
|
||||
volumes:
|
||||
- .:/home/node/app
|
||||
- node_modules:/home/node/app/node_modules
|
||||
working_dir: /home/node/app/
|
||||
command: sh -c "yarn install && yarn dev"
|
||||
depends_on:
|
||||
- mongo
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
mongo:
|
||||
image: mongo:latest
|
||||
ports:
|
||||
- '27017:27017'
|
||||
command:
|
||||
- --storageEngine=wiredTiger
|
||||
volumes:
|
||||
- data:/data/db
|
||||
logging:
|
||||
driver: none
|
||||
|
||||
volumes:
|
||||
data:
|
||||
node_modules:
|
||||
5
examples/localization/next-env.d.ts
vendored
Normal file
5
examples/localization/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
28
examples/localization/next.config.js
Normal file
28
examples/localization/next.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { withPayload } from '@payloadcms/next/withPayload'
|
||||
import createNextIntlPlugin from 'next-intl/plugin'
|
||||
|
||||
const withNextIntl = createNextIntlPlugin()
|
||||
|
||||
import redirects from './redirects.js'
|
||||
|
||||
const NEXT_PUBLIC_SERVER_URL = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000'
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
...[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,
|
||||
}
|
||||
|
||||
export default withNextIntl(withPayload(nextConfig))
|
||||
74
examples/localization/package.json
Normal file
74
examples/localization/package.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "payload-localization-example",
|
||||
"version": "1.0.0",
|
||||
"description": "An example of how to use Payload's localization features",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
|
||||
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||
"dev:prod": "cross-env NODE_OPTIONS=--no-deprecation rm -rf .next && pnpm build && pnpm start",
|
||||
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
||||
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
|
||||
"ii": "cross-env NODE_OPTIONS=--no-deprecation pnpm --ignore-workspace install",
|
||||
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
|
||||
"lint:fix": "cross-env NODE_OPTIONS=--no-deprecation next lint --fix",
|
||||
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||
"reinstall": "cross-env NODE_OPTIONS=--no-deprecation rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install",
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/live-preview-react": "latest",
|
||||
"@payloadcms/next": "latest",
|
||||
"@payloadcms/payload-cloud": "latest",
|
||||
"@payloadcms/plugin-form-builder": "latest",
|
||||
"@payloadcms/plugin-nested-docs": "latest",
|
||||
"@payloadcms/plugin-redirects": "latest",
|
||||
"@payloadcms/plugin-search": "latest",
|
||||
"@payloadcms/plugin-seo": "latest",
|
||||
"@payloadcms/richtext-lexical": "latest",
|
||||
"@payloadcms/ui": "latest",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"geist": "^1.3.0",
|
||||
"graphql": "^16.8.2",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"lucide-react": "^0.378.0",
|
||||
"next": "^15.1.0",
|
||||
"next-intl": "^3.23.2",
|
||||
"payload": "latest",
|
||||
"payload-admin-bar": "^1.0.6",
|
||||
"prism-react-renderer": "^2.3.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "7.45.4",
|
||||
"sharp": "0.32.6",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/escape-html": "^1.0.2",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/node": "22.5.4",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"copyfiles": "^2.4.1",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-next": "15.1.0",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.4.2",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "5.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
}
|
||||
}
|
||||
10271
examples/localization/pnpm-lock.yaml
generated
Normal file
10271
examples/localization/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
examples/localization/postcss.config.js
Normal file
6
examples/localization/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
autoprefixer: {},
|
||||
tailwindcss: {},
|
||||
},
|
||||
}
|
||||
BIN
examples/localization/public/favicon.ico
Normal file
BIN
examples/localization/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
15
examples/localization/public/favicon.svg
Normal file
15
examples/localization/public/favicon.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="260" height="260" viewBox="0 0 260 260" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
path {
|
||||
fill: #0F0F0F;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path d="M120.59 8.5824L231.788 75.6142V202.829L148.039 251.418V124.203L36.7866 57.2249L120.59 8.5824Z" />
|
||||
<path d="M112.123 244.353V145.073L28.2114 193.769L112.123 244.353Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 437 B |
BIN
examples/localization/public/website-template-OG.webp
Normal file
BIN
examples/localization/public/website-template-OG.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
19
examples/localization/redirects.js
Normal file
19
examples/localization/redirects.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const redirects = async () => {
|
||||
return [
|
||||
{
|
||||
// internet explorer
|
||||
destination: '/ie-incompatible.html',
|
||||
has: [
|
||||
{
|
||||
type: 'header',
|
||||
key: 'user-agent',
|
||||
value: '(.*Trident.*)', // all ie browsers
|
||||
},
|
||||
],
|
||||
permanent: false,
|
||||
source: '/:path((?!ie-incompatible.html$).*)', // all pages except the incompatibility page
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export default redirects
|
||||
3
examples/localization/src/access/anyone.ts
Normal file
3
examples/localization/src/access/anyone.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { Access } from 'payload'
|
||||
|
||||
export const anyone: Access = () => true
|
||||
9
examples/localization/src/access/authenticated.ts
Normal file
9
examples/localization/src/access/authenticated.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { AccessArgs } from 'payload'
|
||||
|
||||
import type { User } from '@/payload-types'
|
||||
|
||||
type isAuthenticated = (args: AccessArgs<User>) => boolean
|
||||
|
||||
export const authenticated: isAuthenticated = ({ req: { user } }) => {
|
||||
return Boolean(user)
|
||||
}
|
||||
13
examples/localization/src/access/authenticatedOrPublished.ts
Normal file
13
examples/localization/src/access/authenticatedOrPublished.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Access } from 'payload'
|
||||
|
||||
export const authenticatedOrPublished: Access = ({ req: { user } }) => {
|
||||
if (user) {
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
_status: {
|
||||
equals: 'published',
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
'use client'
|
||||
import { useHeaderTheme } from '@/providers/HeaderTheme'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
const PageClient: React.FC = () => {
|
||||
/* Force the header to be dark mode while we have an image behind it */
|
||||
const { setHeaderTheme } = useHeaderTheme()
|
||||
|
||||
useEffect(() => {
|
||||
setHeaderTheme('light')
|
||||
}, [setHeaderTheme])
|
||||
return <React.Fragment />
|
||||
}
|
||||
|
||||
export default PageClient
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import { PayloadRedirects } from '@/components/PayloadRedirects'
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import { draftMode } from 'next/headers'
|
||||
import React, { cache } from 'react'
|
||||
import { homeStatic } from '@/endpoints/seed/home-static'
|
||||
|
||||
import type { Page as PageType } from '@/payload-types'
|
||||
|
||||
import { RenderBlocks } from '@/blocks/RenderBlocks'
|
||||
import { RenderHero } from '@/heros/RenderHero'
|
||||
import { generateMeta } from '@/utilities/generateMeta'
|
||||
import PageClient from './page.client'
|
||||
import { TypedLocale } from 'payload'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
const pages = await payload.find({
|
||||
collection: 'pages',
|
||||
draft: false,
|
||||
limit: 1000,
|
||||
overrideAccess: false,
|
||||
})
|
||||
|
||||
const params = pages.docs
|
||||
?.filter((doc) => {
|
||||
return doc.slug !== 'home'
|
||||
})
|
||||
.map(({ slug }) => {
|
||||
return { slug }
|
||||
})
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
slug?: string
|
||||
locale: TypedLocale
|
||||
}>
|
||||
}
|
||||
|
||||
export default async function Page({ params: paramsPromise }: Args) {
|
||||
const { slug = 'home', locale = 'en' } = await paramsPromise
|
||||
const url = '/' + slug
|
||||
|
||||
let page: PageType | null
|
||||
|
||||
page = await queryPage({
|
||||
slug,
|
||||
locale,
|
||||
})
|
||||
|
||||
// Remove this code once your website is seeded
|
||||
if (!page && slug === 'home') {
|
||||
page = homeStatic
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
return <PayloadRedirects url={url} />
|
||||
}
|
||||
|
||||
const { hero, layout } = page
|
||||
|
||||
return (
|
||||
<article className="pt-16 pb-24">
|
||||
<PageClient />
|
||||
{/* Allows redirects for valid pages too */}
|
||||
<PayloadRedirects disableNotFound url={url} />
|
||||
|
||||
<RenderHero {...hero} />
|
||||
<RenderBlocks blocks={layout} locale={locale} />
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params: paramsPromise }): Promise<Metadata> {
|
||||
const { slug = 'home', locale = 'en' } = await paramsPromise
|
||||
const page = await queryPage({
|
||||
slug,
|
||||
locale,
|
||||
})
|
||||
|
||||
return generateMeta({ doc: page })
|
||||
}
|
||||
|
||||
const queryPage = cache(async ({ slug, locale }: { slug: string; locale: TypedLocale }) => {
|
||||
const { isEnabled: draft } = await draftMode()
|
||||
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const result = await payload.find({
|
||||
collection: 'pages',
|
||||
draft,
|
||||
limit: 1,
|
||||
locale,
|
||||
overrideAccess: draft,
|
||||
where: {
|
||||
slug: {
|
||||
equals: slug,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return result.docs?.[0] || null
|
||||
})
|
||||
103
examples/localization/src/app/(frontend)/[locale]/globals.css
Normal file
103
examples/localization/src/app/(frontend)/[locale]/globals.css
Normal file
@@ -0,0 +1,103 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: auto;
|
||||
font-weight: auto;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 240 5% 96%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 240 6% 90%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.2rem;
|
||||
|
||||
--success: 196 52% 74%;
|
||||
--warning: 34 89% 85%;
|
||||
--error: 10 100% 86%;
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--background: 0 0% 0%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 240 6% 10%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 240 4% 16%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
|
||||
--success: 196 100% 14%;
|
||||
--warning: 34 51% 25%;
|
||||
--error: 10 39% 43%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
html[data-theme='dark'],
|
||||
html[data-theme='light'] {
|
||||
opacity: initial;
|
||||
}
|
||||
84
examples/localization/src/app/(frontend)/[locale]/layout.tsx
Normal file
84
examples/localization/src/app/(frontend)/[locale]/layout.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import { cn } from 'src/utilities/cn'
|
||||
import { GeistMono } from 'geist/font/mono'
|
||||
import { GeistSans } from 'geist/font/sans'
|
||||
import React from 'react'
|
||||
|
||||
import { AdminBar } from '@/components/AdminBar'
|
||||
import { Footer } from '@/globals/Footer/Component'
|
||||
import { Header } from '@/globals/Header/Component'
|
||||
import { LivePreviewListener } from '@/components/LivePreviewListener'
|
||||
import { Providers } from '@/providers'
|
||||
import { InitTheme } from '@/providers/Theme/InitTheme'
|
||||
import { mergeOpenGraph } from '@/utilities/mergeOpenGraph'
|
||||
import { draftMode } from 'next/headers'
|
||||
import { TypedLocale } from 'payload'
|
||||
|
||||
import './globals.css'
|
||||
import { getMessages, setRequestLocale } from 'next-intl/server'
|
||||
import { NextIntlClientProvider } from 'next-intl'
|
||||
import { routing } from '@/i18n/routing'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode
|
||||
params: Promise<{
|
||||
locale: TypedLocale
|
||||
}>
|
||||
}
|
||||
|
||||
export default async function RootLayout({ children, params }: Args) {
|
||||
const { locale } = await params
|
||||
|
||||
if (!routing.locales.includes(locale as any)) {
|
||||
notFound()
|
||||
}
|
||||
setRequestLocale(locale)
|
||||
|
||||
const { isEnabled } = await draftMode()
|
||||
const messages = await getMessages()
|
||||
|
||||
return (
|
||||
<html
|
||||
className={cn(GeistSans.variable, GeistMono.variable)}
|
||||
lang={locale}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<head>
|
||||
<InitTheme />
|
||||
<link href="/favicon.ico" rel="icon" sizes="32x32" />
|
||||
<link href="/favicon.svg" rel="icon" type="image/svg+xml" />
|
||||
</head>
|
||||
<body>
|
||||
<Providers>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<AdminBar
|
||||
adminBarProps={{
|
||||
preview: isEnabled,
|
||||
}}
|
||||
/>
|
||||
<LivePreviewListener />
|
||||
|
||||
<Header locale={locale} />
|
||||
{children}
|
||||
<Footer locale={locale} />
|
||||
</NextIntlClientProvider>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(process.env.NEXT_PUBLIC_SERVER_URL || 'https://payloadcms.com'),
|
||||
openGraph: mergeOpenGraph(),
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
creator: '@payloadcms',
|
||||
},
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({ locale }))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
export default function NotFound() {
|
||||
const t = useTranslations()
|
||||
|
||||
return (
|
||||
<div className="container py-28">
|
||||
<div className="prose max-w-none">
|
||||
<h1 style={{ marginBottom: 0 }}>404</h1>
|
||||
<p className="mb-4">{t('page-not-found')}</p>
|
||||
</div>
|
||||
<Button asChild variant="default">
|
||||
<Link href="/">{t('go-home')}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
86
examples/localization/src/app/(frontend)/[locale]/page.tsx
Normal file
86
examples/localization/src/app/(frontend)/[locale]/page.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Metadata } from 'next'
|
||||
// import PageTemplate from './[slug]/page'
|
||||
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import { draftMode } from 'next/headers'
|
||||
import React, { cache } from 'react'
|
||||
import { generateMeta } from '@/utilities/generateMeta'
|
||||
import { TypedLocale } from 'payload'
|
||||
import { PayloadRedirects } from '@/components/PayloadRedirects'
|
||||
import { homeStatic } from '@/endpoints/seed/home-static'
|
||||
import type { Page as PageType } from '@/payload-types'
|
||||
import { RenderBlocks } from '@/blocks/RenderBlocks'
|
||||
import { RenderHero } from '@/heros/RenderHero'
|
||||
import PageClient from './[slug]/page.client'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
slug?: string
|
||||
locale: TypedLocale
|
||||
}>
|
||||
}
|
||||
|
||||
export default async function Page({ params: paramsPromise }: Args) {
|
||||
const { slug = 'home', locale = 'en' } = await paramsPromise
|
||||
const url = '/' + slug
|
||||
|
||||
let page: PageType | null
|
||||
|
||||
page = await queryPage({
|
||||
slug,
|
||||
locale,
|
||||
})
|
||||
|
||||
// Remove this code once your website is seeded
|
||||
if (!page && slug === 'home') {
|
||||
page = homeStatic
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
return <PayloadRedirects url={url} />
|
||||
}
|
||||
|
||||
const { hero, layout } = page
|
||||
|
||||
return (
|
||||
<article className="pt-16 pb-24">
|
||||
<PageClient />
|
||||
<PayloadRedirects disableNotFound url={url} />
|
||||
|
||||
<RenderHero {...hero} />
|
||||
<RenderBlocks blocks={layout} locale={locale} />
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: Args): Promise<Metadata> {
|
||||
const { locale = 'en', slug = 'home' } = await params
|
||||
const page = await queryPage({
|
||||
locale,
|
||||
slug,
|
||||
})
|
||||
|
||||
return generateMeta({ doc: page })
|
||||
}
|
||||
|
||||
const queryPage = cache(async ({ locale, slug }: { locale: TypedLocale; slug: string }) => {
|
||||
const { isEnabled: draft } = await draftMode()
|
||||
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const result = await payload.find({
|
||||
collection: 'pages',
|
||||
draft,
|
||||
limit: 1,
|
||||
overrideAccess: draft,
|
||||
locale: locale,
|
||||
where: {
|
||||
slug: {
|
||||
equals: slug,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return result.docs?.[0] || null
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
'use client'
|
||||
import { useHeaderTheme } from '@/providers/HeaderTheme'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
const PageClient: React.FC = () => {
|
||||
/* Force the header to be dark mode while we have an image behind it */
|
||||
const { setHeaderTheme } = useHeaderTheme()
|
||||
|
||||
useEffect(() => {
|
||||
setHeaderTheme('dark')
|
||||
}, [setHeaderTheme])
|
||||
return <React.Fragment />
|
||||
}
|
||||
|
||||
export default PageClient
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import { RelatedPosts } from '@/blocks/RelatedPosts/Component'
|
||||
import { PayloadRedirects } from '@/components/PayloadRedirects'
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import { draftMode } from 'next/headers'
|
||||
import React, { cache } from 'react'
|
||||
import RichText from '@/components/RichText'
|
||||
|
||||
import type { Post } from '@/payload-types'
|
||||
|
||||
import { PostHero } from '@/heros/PostHero'
|
||||
import { generateMeta } from '@/utilities/generateMeta'
|
||||
import PageClient from './page.client'
|
||||
import { TypedLocale } from 'payload'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
draft: false,
|
||||
limit: 1000,
|
||||
overrideAccess: false,
|
||||
})
|
||||
|
||||
const params = posts.docs.map(({ slug }) => {
|
||||
return { slug }
|
||||
})
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
slug?: string
|
||||
locale?: TypedLocale
|
||||
}>
|
||||
}
|
||||
|
||||
export default async function Post({ params: paramsPromise }: Args) {
|
||||
const { slug = '', locale = 'en' } = await paramsPromise
|
||||
const url = '/posts/' + slug
|
||||
const post = await queryPost({ slug, locale })
|
||||
|
||||
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 items-center gap-4 pt-8">
|
||||
<div className="container lg:mx-0 lg:grid lg:grid-cols-[1fr_48rem_1fr] grid-rows-[1fr]">
|
||||
<RichText
|
||||
className="lg:grid lg:grid-cols-subgrid col-start-1 col-span-3 grid-rows-[1fr]"
|
||||
content={post.content}
|
||||
enableGutter={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{post.relatedPosts && post.relatedPosts.length > 0 && (
|
||||
<RelatedPosts
|
||||
className="mt-12"
|
||||
docs={post.relatedPosts.filter((post) => typeof post === 'object')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params: paramsPromise }: Args): Promise<Metadata> {
|
||||
const { slug = '', locale = 'en' } = await paramsPromise
|
||||
const post = await queryPost({ slug, locale })
|
||||
|
||||
return generateMeta({ doc: post })
|
||||
}
|
||||
|
||||
const queryPost = cache(async ({ slug, locale }: { slug: string; locale: TypedLocale }) => {
|
||||
const { isEnabled: draft } = await draftMode()
|
||||
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const result = await payload.find({
|
||||
collection: 'posts',
|
||||
draft,
|
||||
limit: 1,
|
||||
overrideAccess: draft,
|
||||
locale,
|
||||
where: {
|
||||
slug: {
|
||||
equals: slug,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return result.docs?.[0] || null
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
'use client'
|
||||
import { useHeaderTheme } from '@/providers/HeaderTheme'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
const PageClient: React.FC = () => {
|
||||
/* Force the header to be dark mode while we have an image behind it */
|
||||
const { setHeaderTheme } = useHeaderTheme()
|
||||
|
||||
useEffect(() => {
|
||||
setHeaderTheme('light')
|
||||
}, [setHeaderTheme])
|
||||
return <React.Fragment />
|
||||
}
|
||||
|
||||
export default PageClient
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { Metadata } from 'next/types'
|
||||
|
||||
import { CollectionArchive } from '@/components/CollectionArchive'
|
||||
import { PageRange } from '@/components/PageRange'
|
||||
import { Pagination } from '@/components/Pagination'
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import React from 'react'
|
||||
import PageClient from './page.client'
|
||||
import { TypedLocale } from 'payload'
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||
|
||||
export const revalidate = 600
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
locale: TypedLocale
|
||||
}>
|
||||
}
|
||||
|
||||
export default async function Page({ params }: Args) {
|
||||
const { locale = 'en' } = await params
|
||||
const t = await getTranslations()
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
locale,
|
||||
depth: 1,
|
||||
limit: 12,
|
||||
overrideAccess: false,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="pt-24 pb-24">
|
||||
<PageClient />
|
||||
<div className="container mb-16">
|
||||
<div className="prose dark:prose-invert max-w-none">
|
||||
<h1>{t('posts')}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mb-8">
|
||||
<PageRange
|
||||
collection="posts"
|
||||
currentPage={posts.page}
|
||||
limit={12}
|
||||
totalDocs={posts.totalDocs}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CollectionArchive posts={posts.docs} />
|
||||
|
||||
<div className="container">
|
||||
{posts.totalPages > 1 && posts.page && (
|
||||
<Pagination page={posts.page} totalPages={posts.totalPages} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
return {
|
||||
title: `Payload Website Template Posts`,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
'use client'
|
||||
import { useHeaderTheme } from '@/providers/HeaderTheme'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
const PageClient: React.FC = () => {
|
||||
/* Force the header to be dark mode while we have an image behind it */
|
||||
const { setHeaderTheme } = useHeaderTheme()
|
||||
|
||||
useEffect(() => {
|
||||
setHeaderTheme('light')
|
||||
}, [setHeaderTheme])
|
||||
return <React.Fragment />
|
||||
}
|
||||
|
||||
export default PageClient
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { Metadata } from 'next/types'
|
||||
|
||||
import { CollectionArchive } from '@/components/CollectionArchive'
|
||||
import { PageRange } from '@/components/PageRange'
|
||||
import { Pagination } from '@/components/Pagination'
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import React from 'react'
|
||||
import PageClient from './page.client'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import { TypedLocale } from 'payload'
|
||||
|
||||
export const revalidate = 600
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
pageNumber: string
|
||||
locale: TypedLocale
|
||||
}>
|
||||
}
|
||||
|
||||
export default async function Page({ params: paramsPromise }: Args) {
|
||||
const { pageNumber, locale } = await paramsPromise
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
const t = await getTranslations()
|
||||
|
||||
const sanitizedPageNumber = Number(pageNumber)
|
||||
|
||||
if (!Number.isInteger(sanitizedPageNumber)) notFound()
|
||||
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
depth: 1,
|
||||
limit: 12,
|
||||
locale,
|
||||
page: sanitizedPageNumber,
|
||||
overrideAccess: false,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="pt-24 pb-24">
|
||||
<PageClient />
|
||||
<div className="container mb-16">
|
||||
<div className="prose dark:prose-invert max-w-none">
|
||||
<h1>{t('posts')}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mb-8">
|
||||
<PageRange
|
||||
collection="posts"
|
||||
currentPage={posts.page}
|
||||
limit={12}
|
||||
totalDocs={posts.totalDocs}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CollectionArchive posts={posts.docs} />
|
||||
|
||||
<div className="container">
|
||||
{posts.totalPages > 1 && posts.page && (
|
||||
<Pagination page={posts.page} totalPages={posts.totalPages} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params: paramsPromise }: Args): Promise<Metadata> {
|
||||
const { pageNumber } = await paramsPromise
|
||||
return {
|
||||
title: `Payload Website Template Posts Page ${pageNumber || ''}`,
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
depth: 0,
|
||||
limit: 10,
|
||||
draft: false,
|
||||
overrideAccess: false,
|
||||
})
|
||||
|
||||
const pages: { pageNumber: string }[] = []
|
||||
|
||||
for (let i = 1; i <= posts.totalPages; i++) {
|
||||
pages.push({ pageNumber: String(i) })
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
'use client'
|
||||
import { useHeaderTheme } from '@/providers/HeaderTheme'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
const PageClient: React.FC = () => {
|
||||
/* Force the header to be dark mode while we have an image behind it */
|
||||
const { setHeaderTheme } = useHeaderTheme()
|
||||
|
||||
useEffect(() => {
|
||||
setHeaderTheme('light')
|
||||
}, [setHeaderTheme])
|
||||
return <React.Fragment />
|
||||
}
|
||||
|
||||
export default PageClient
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { Metadata } from 'next/types'
|
||||
|
||||
import { CollectionArchive } from '@/components/CollectionArchive'
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import React from 'react'
|
||||
import { Post } from '@/payload-types'
|
||||
import { Search } from '@/search/Component'
|
||||
import PageClient from './page.client'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import { TypedLocale } from 'payload'
|
||||
|
||||
type Args = {
|
||||
searchParams: Promise<{
|
||||
q: string
|
||||
}>
|
||||
params: Promise<{
|
||||
locale: TypedLocale
|
||||
}>
|
||||
}
|
||||
export default async function Page({
|
||||
searchParams: searchParamsPromise,
|
||||
params: paramsPromise,
|
||||
}: Args) {
|
||||
const { q: query } = await searchParamsPromise
|
||||
const { locale } = await paramsPromise
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
const t = await getTranslations()
|
||||
|
||||
const posts = await payload.find({
|
||||
collection: 'search',
|
||||
depth: 1,
|
||||
limit: 12,
|
||||
locale,
|
||||
...(query
|
||||
? {
|
||||
where: {
|
||||
or: [
|
||||
{
|
||||
title: {
|
||||
like: query,
|
||||
},
|
||||
},
|
||||
{
|
||||
'meta.description': {
|
||||
like: query,
|
||||
},
|
||||
},
|
||||
{
|
||||
'meta.title': {
|
||||
like: query,
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: {
|
||||
like: query,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="pt-24 pb-24">
|
||||
<PageClient />
|
||||
<div className="container mb-16">
|
||||
<div className="prose dark:prose-invert max-w-none">
|
||||
<h1 className="sr-only">{t('search')}</h1>
|
||||
<Search />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{posts.totalDocs > 0 ? (
|
||||
<CollectionArchive posts={posts.docs as unknown as Post[]} />
|
||||
) : (
|
||||
<div className="container">No results found.</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
return {
|
||||
title: `Payload Website Template Search`,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { draftMode } from 'next/headers'
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
const draft = await draftMode()
|
||||
draft.disable()
|
||||
return new Response('Draft mode is disabled')
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { draftMode } from 'next/headers'
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
const draft = await draftMode()
|
||||
draft.disable()
|
||||
return new Response('Draft mode is disabled')
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { draftMode } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
import configPromise from '@payload-config'
|
||||
import { CollectionSlug, TypedLocale } from 'payload'
|
||||
|
||||
const payloadToken = 'payload-token'
|
||||
|
||||
export async function GET(
|
||||
req: Request & {
|
||||
cookies: {
|
||||
get: (name: string) => {
|
||||
value: string
|
||||
}
|
||||
}
|
||||
},
|
||||
): Promise<Response> {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
const token = req.cookies.get(payloadToken)?.value
|
||||
const { searchParams } = new URL(req.url)
|
||||
const path = searchParams.get('path')
|
||||
const collection = searchParams.get('collection') as CollectionSlug
|
||||
const slug = searchParams.get('slug')
|
||||
|
||||
const previewSecret = searchParams.get('previewSecret')
|
||||
|
||||
if (previewSecret) {
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
} else {
|
||||
if (!path) {
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!collection) {
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!slug) {
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
if (!path.startsWith('/')) {
|
||||
new Response('This endpoint can only be used for internal previews', { status: 500 })
|
||||
}
|
||||
|
||||
let user
|
||||
|
||||
try {
|
||||
user = jwt.verify(token, payload.secret)
|
||||
} catch (error) {
|
||||
payload.logger.error('Error verifying token for live preview:', error)
|
||||
}
|
||||
|
||||
const draft = await draftMode()
|
||||
|
||||
// You can add additional checks here to see if the user is allowed to preview this page
|
||||
if (!user) {
|
||||
draft.disable()
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
// Verify the given slug exists
|
||||
try {
|
||||
const docs = await payload.find({
|
||||
collection: collection,
|
||||
draft: true,
|
||||
locale: path.split('/')[0] as TypedLocale,
|
||||
where: {
|
||||
slug: {
|
||||
equals: slug,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!docs.docs.length) {
|
||||
return new Response('Document not found', { status: 404 })
|
||||
}
|
||||
} catch (error) {
|
||||
payload.logger.error('Error verifying token for live preview:', error)
|
||||
}
|
||||
|
||||
draft.enable()
|
||||
|
||||
redirect(path)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
import { importMap } from '../importMap'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[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, importMap })
|
||||
|
||||
export default NotFound
|
||||
@@ -0,0 +1,24 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
import { importMap } from '../importMap'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const Page = ({ params, searchParams }: Args) =>
|
||||
RootPage({ config, params, searchParams, importMap })
|
||||
|
||||
export default Page
|
||||
61
examples/localization/src/app/(payload)/admin/importMap.js
Normal file
61
examples/localization/src/app/(payload)/admin/importMap.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { OverviewComponent as OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||
import { MetaTitleComponent as MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||
import { MetaImageComponent as MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||
import { MetaDescriptionComponent as MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||
import { PreviewComponent as PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||
import { SlugComponent as SlugComponent_92cc057d0a2abb4f6cf0307edf59f986 } from '@/fields/slug/SlugComponent'
|
||||
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { LinkToDoc as LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client'
|
||||
import { ReindexButton as ReindexButton_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client'
|
||||
import { default as default_035a063f0e4325a280e3cc815d2ec5d7 } from '@/components/AfterDashboard'
|
||||
import { default as default_8a7ab0eb7ab5c511aba12e68480bfe5e } from '@/components/BeforeLogin'
|
||||
|
||||
export const importMap = {
|
||||
'@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell':
|
||||
RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
'@payloadcms/richtext-lexical/rsc#RscEntryLexicalField':
|
||||
RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
'@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient':
|
||||
InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient':
|
||||
FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/richtext-lexical/client#HeadingFeatureClient':
|
||||
HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/richtext-lexical/client#UnderlineFeatureClient':
|
||||
UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/richtext-lexical/client#BoldFeatureClient':
|
||||
BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/richtext-lexical/client#ItalicFeatureClient':
|
||||
ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/richtext-lexical/client#LinkFeatureClient':
|
||||
LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/plugin-seo/client#OverviewComponent':
|
||||
OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
'@payloadcms/plugin-seo/client#MetaTitleComponent':
|
||||
MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
'@payloadcms/plugin-seo/client#MetaImageComponent':
|
||||
MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
'@payloadcms/plugin-seo/client#MetaDescriptionComponent':
|
||||
MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
'@payloadcms/plugin-seo/client#PreviewComponent':
|
||||
PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
'@/fields/slug/SlugComponent#SlugComponent': SlugComponent_92cc057d0a2abb4f6cf0307edf59f986,
|
||||
'@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient':
|
||||
HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/richtext-lexical/client#BlocksFeatureClient':
|
||||
BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/plugin-search/client#LinkToDoc': LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634,
|
||||
'@payloadcms/plugin-search/client#ReindexButton': ReindexButton_aead06e4cbf6b2620c5c51c9ab283634,
|
||||
'@/components/AfterDashboard#default': default_035a063f0e4325a280e3cc815d2ec5d7,
|
||||
'@/components/BeforeLogin#default': default_8a7ab0eb7ab5c511aba12e68480bfe5e,
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
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)
|
||||
@@ -0,0 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = GRAPHQL_PLAYGROUND_GET(config)
|
||||
@@ -0,0 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
0
examples/localization/src/app/(payload)/custom.scss
Normal file
0
examples/localization/src/app/(payload)/custom.scss
Normal file
32
examples/localization/src/app/(payload)/layout.tsx
Normal file
32
examples/localization/src/app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import configPromise from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import React from 'react'
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||
import type { ServerFunctionClient } from 'payload'
|
||||
import config from '@payload-config'
|
||||
|
||||
import './custom.scss'
|
||||
import { importMap } from './admin/importMap'
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const serverFunction: ServerFunctionClient = async function (args) {
|
||||
'use server'
|
||||
return handleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
})
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout importMap={importMap} config={configPromise} serverFunction={serverFunction}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
export default Layout
|
||||
76
examples/localization/src/blocks/ArchiveBlock/Component.tsx
Normal file
76
examples/localization/src/blocks/ArchiveBlock/Component.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { Post, ArchiveBlock as ArchiveBlockProps } from '@/payload-types'
|
||||
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import React from 'react'
|
||||
import RichText from '@/components/RichText'
|
||||
|
||||
import { CollectionArchive } from '@/components/CollectionArchive'
|
||||
import { TypedLocale } from 'payload'
|
||||
|
||||
export const ArchiveBlock: React.FC<
|
||||
ArchiveBlockProps & {
|
||||
id?: string
|
||||
locale: TypedLocale
|
||||
}
|
||||
> = async (props) => {
|
||||
const {
|
||||
id,
|
||||
categories,
|
||||
introContent,
|
||||
limit: limitFromProps,
|
||||
populateBy,
|
||||
selectedDocs,
|
||||
locale,
|
||||
} = props
|
||||
|
||||
const limit = limitFromProps || 3
|
||||
|
||||
let posts: Post[] = []
|
||||
|
||||
if (populateBy === 'collection') {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const flattenedCategories = categories?.map((category) => {
|
||||
if (typeof category === 'object') return category.id
|
||||
else return category
|
||||
})
|
||||
|
||||
const fetchedPosts = await payload.find({
|
||||
collection: 'posts',
|
||||
depth: 1,
|
||||
locale,
|
||||
limit,
|
||||
...(flattenedCategories && flattenedCategories.length > 0
|
||||
? {
|
||||
where: {
|
||||
categories: {
|
||||
in: flattenedCategories,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
|
||||
posts = fetchedPosts.docs
|
||||
} else {
|
||||
if (selectedDocs?.length) {
|
||||
const filteredSelectedPosts = selectedDocs.map((post) => {
|
||||
if (typeof post.value === 'object') return post.value
|
||||
}) as Post[]
|
||||
|
||||
posts = filteredSelectedPosts
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-16" id={`block-${id}`}>
|
||||
{introContent && (
|
||||
<div className="container mb-16">
|
||||
<RichText className="ml-0 max-w-[48rem]" content={introContent} enableGutter={false} />
|
||||
</div>
|
||||
)}
|
||||
<CollectionArchive posts={posts} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
95
examples/localization/src/blocks/ArchiveBlock/config.ts
Normal file
95
examples/localization/src/blocks/ArchiveBlock/config.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
HeadingFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const Archive: Block = {
|
||||
slug: 'archive',
|
||||
interfaceName: 'ArchiveBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'introContent',
|
||||
type: 'richText',
|
||||
localized: true,
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [
|
||||
...rootFeatures,
|
||||
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
]
|
||||
},
|
||||
}),
|
||||
label: 'Intro Content',
|
||||
},
|
||||
{
|
||||
name: 'populateBy',
|
||||
type: 'select',
|
||||
defaultValue: 'collection',
|
||||
options: [
|
||||
{
|
||||
label: 'Collection',
|
||||
value: 'collection',
|
||||
},
|
||||
{
|
||||
label: 'Individual Selection',
|
||||
value: 'selection',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'relationTo',
|
||||
type: 'select',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||
},
|
||||
defaultValue: 'posts',
|
||||
label: 'Collections To Show',
|
||||
options: [
|
||||
{
|
||||
label: 'Posts',
|
||||
value: 'posts',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'categories',
|
||||
type: 'relationship',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||
},
|
||||
hasMany: true,
|
||||
label: 'Categories To Show',
|
||||
relationTo: 'categories',
|
||||
},
|
||||
{
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||
step: 1,
|
||||
},
|
||||
defaultValue: 10,
|
||||
label: 'Limit',
|
||||
},
|
||||
{
|
||||
name: 'selectedDocs',
|
||||
type: 'relationship',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'selection',
|
||||
},
|
||||
hasMany: true,
|
||||
label: 'Selection',
|
||||
relationTo: ['posts'],
|
||||
},
|
||||
],
|
||||
labels: {
|
||||
plural: 'Archives',
|
||||
singular: 'Archive',
|
||||
},
|
||||
}
|
||||
26
examples/localization/src/blocks/Banner/Component.tsx
Normal file
26
examples/localization/src/blocks/Banner/Component.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { BannerBlock as BannerBlockProps } from 'src/payload-types'
|
||||
|
||||
import { cn } from 'src/utilities/cn'
|
||||
import React from 'react'
|
||||
import RichText from '@/components/RichText'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
} & BannerBlockProps
|
||||
|
||||
export const BannerBlock: React.FC<Props> = ({ className, content, style }) => {
|
||||
return (
|
||||
<div className={cn('mx-auto my-8 w-full', className)}>
|
||||
<div
|
||||
className={cn('border py-3 px-6 flex items-center rounded', {
|
||||
'border-border bg-card': style === 'info',
|
||||
'border-error bg-error/30': style === 'error',
|
||||
'border-success bg-success/30': style === 'success',
|
||||
'border-warning bg-warning/30': style === 'warning',
|
||||
})}
|
||||
>
|
||||
<RichText content={content} enableGutter={false} enableProse={false} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
38
examples/localization/src/blocks/Banner/config.ts
Normal file
38
examples/localization/src/blocks/Banner/config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const Banner: Block = {
|
||||
slug: 'banner',
|
||||
fields: [
|
||||
{
|
||||
name: 'style',
|
||||
type: 'select',
|
||||
defaultValue: 'info',
|
||||
options: [
|
||||
{ label: 'Info', value: 'info' },
|
||||
{ label: 'Warning', value: 'warning' },
|
||||
{ label: 'Error', value: 'error' },
|
||||
{ label: 'Success', value: 'success' },
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
localized: true,
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [...rootFeatures, FixedToolbarFeature(), InlineToolbarFeature()]
|
||||
},
|
||||
}),
|
||||
label: false,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
interfaceName: 'BannerBlock',
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user