templates: add support for scheduled publish to the website template [no lint] (#10455)

Adds configuration for vercel cron jobs and scheduled publish/unpublish
in website templates
This commit is contained in:
Paul
2025-01-08 13:43:47 -06:00
committed by GitHub
parent 17e7ee2315
commit 7321f9f3d5
14 changed files with 432 additions and 7 deletions

View File

@@ -95,7 +95,7 @@ async function main() {
encodeURI(
`${templateRepoUrlBase}/with-vercel-website` +
'&project-name=payload-project' +
'&env=PAYLOAD_SECRET' +
'&env=PAYLOAD_SECRET%2CCRON_SECRET' +
'&build-command=pnpm run ci' +
'&stores=[{"type":"postgres"},{"type":"blob"}]', // Postgres and Vercel Blob Storage
),

View File

@@ -9,3 +9,6 @@ PAYLOAD_SECRET=YOUR_SECRET_HERE
# Used to configure CORS, format links and more. No trailing slash
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
# Secret used to authenticate cron jobs
CRON_SECRET=YOUR_CRON_SECRET_HERE

View File

@@ -179,6 +179,38 @@ Although Next.js includes a robust set of caching strategies out of the box, Pay
To spin up this example locally, follow the [Quick Start](#quick-start). Then [Seed](#seed) the database with a few pages, posts, and projects.
### Working with Postgres
Postgres and other SQL-based databases follow a strict schema for managing your data. In comparison to our MongoDB adapter, this means that there's a few extra steps to working with Postgres.
Note that often times when making big schema changes you can run the risk of losing data if you're not manually migrating it.
#### Local development
Ideally we recommend running a local copy of your database so that schema updates are as fast as possible. By default the Postgres adapter has `push: true` for development environments. This will let you add, modify and remove fields and collections without needing to run any data migrations.
If your database is pointed to production you will want to set `push: false` otherwise you will risk losing data or having your migrations out of sync.
#### Migrations
[Migrations](https://payloadcms.com/docs/database/migrations) are essentially SQL code versions that keeps track of your schema. When deploy with Postgres you will need to make sure you create and then run your migrations.
Locally create a migration
```bash
pnpm payload migrate:create
```
This creates the migration files you will need to push alongside with your new configuration.
On the server after building and before running `pnpm start` you will want to run your migrations
```bash
pnpm payload migrate
```
This command will check for any migrations that have not yet been run and try to run them and it will keep a record of migrations that have been run in the database.
### Docker
Alternatively, you can use [Docker](https://www.docker.com) to spin up this template locally. To do so, follow these steps:

View File

@@ -132,6 +132,7 @@ export const Pages: CollectionConfig<'pages'> = {
autosave: {
interval: 100, // We set this interval for optimal live preview
},
schedulePublish: true,
},
maxPerDoc: 50,
},

View File

@@ -229,6 +229,7 @@ export const Posts: CollectionConfig<'posts'> = {
autosave: {
interval: 100, // We set this interval for optimal live preview
},
schedulePublish: true,
},
maxPerDoc: 50,
},

View File

@@ -20,6 +20,7 @@ export interface Config {
forms: Form;
'form-submissions': FormSubmission;
search: Search;
'payload-jobs': PayloadJob;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
@@ -35,6 +36,7 @@ export interface Config {
forms: FormsSelect<false> | FormsSelect<true>;
'form-submissions': FormSubmissionsSelect<false> | FormSubmissionsSelect<true>;
search: SearchSelect<false> | SearchSelect<true>;
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@@ -55,7 +57,13 @@ export interface Config {
collection: 'users';
};
jobs: {
tasks: unknown;
tasks: {
schedulePublish: TaskSchedulePublish;
inline: {
input: unknown;
output: unknown;
};
};
workflows: unknown;
};
}
@@ -735,6 +743,98 @@ export interface Search {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-jobs".
*/
export interface PayloadJob {
id: string;
/**
* Input data provided to the job
*/
input?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
taskStatus?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
completedAt?: string | null;
totalTried?: number | null;
/**
* If hasError is true this job will not be retried
*/
hasError?: boolean | null;
/**
* If hasError is true, this is the error that caused it
*/
error?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
/**
* Task execution log
*/
log?:
| {
executedAt: string;
completedAt: string;
taskSlug: 'inline' | 'schedulePublish';
taskID: string;
input?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
output?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
state: 'failed' | 'succeeded';
error?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
id?: string | null;
}[]
| null;
taskSlug?: ('inline' | 'schedulePublish') | null;
queue?: string | null;
waitUntil?: string | null;
processing?: boolean | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
@@ -777,6 +877,10 @@ export interface PayloadLockedDocument {
| ({
relationTo: 'search';
value: string | Search;
} | null)
| ({
relationTo: 'payload-jobs';
value: string | PayloadJob;
} | null);
globalSlug?: string | null;
user: {
@@ -1305,6 +1409,37 @@ export interface SearchSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-jobs_select".
*/
export interface PayloadJobsSelect<T extends boolean = true> {
input?: T;
taskStatus?: T;
completedAt?: T;
totalTried?: T;
hasError?: T;
error?: T;
log?:
| T
| {
executedAt?: T;
completedAt?: T;
taskSlug?: T;
taskID?: T;
input?: T;
output?: T;
state?: T;
error?: T;
id?: T;
};
taskSlug?: T;
queue?: T;
waitUntil?: T;
processing?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
@@ -1441,6 +1576,28 @@ export interface FooterSelect<T extends boolean = true> {
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "TaskSchedulePublish".
*/
export interface TaskSchedulePublish {
input: {
type?: ('publish' | 'unpublish') | null;
locale?: string | null;
doc?:
| ({
relationTo: 'pages';
value: string | Page;
} | null)
| ({
relationTo: 'posts';
value: string | Post;
} | null);
global?: string | null;
user?: (string | null) | User;
};
output?: unknown;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "BannerBlock".

View File

@@ -3,7 +3,7 @@ import { mongooseAdapter } from '@payloadcms/db-mongodb' // database-adapter-imp
import sharp from 'sharp' // sharp-import
import path from 'path'
import { buildConfig } from 'payload'
import { buildConfig, PayloadRequest } from 'payload'
import { fileURLToPath } from 'url'
import { Categories } from './collections/Categories'
@@ -76,4 +76,19 @@ export default buildConfig({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
jobs: {
access: {
run: ({ req }: { req: PayloadRequest }): boolean => {
// Allow logged in users to execute this endpoint (default)
if (req.user) return true
// If there is no logged in user, then check
// for the Vercel Cron secret to be present as an
// Authorization header:
const authHeader = req.headers.get('authorization')
return authHeader === `Bearer ${process.env.CRON_SECRET}`
},
},
tasks: []
},
})

View File

@@ -3,4 +3,6 @@ POSTGRES_URL=postgresql://127.0.0.1:5432/your-database-name
# 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
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
# Secret used to authenticate cron jobs
CRON_SECRET=YOUR_CRON_SECRET_HERE

View File

@@ -4,7 +4,7 @@ This is the official [Payload Website Template](https://github.com/payloadcms/pa
You can deploy to Vercel, using Neon and Vercel Blob Storage with one click:
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/payloadcms/payload/tree/main/templates/with-vercel-website&project-name=payload-project&env=PAYLOAD_SECRET&build-command=pnpm%20run%20ci&stores=%5B%7B%22type%22:%22postgres%22%7D,%7B%22type%22:%22blob%22%7D%5D)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/payloadcms/payload/tree/main/templates/with-vercel-website&project-name=payload-project&env=PAYLOAD_SECRET%2CCRON_SECRET&build-command=pnpm%20run%20ci&stores=%5B%7B%22type%22:%22postgres%22%7D,%7B%22type%22:%22blob%22%7D%5D)
This template is right for you if you are working on:
@@ -172,6 +172,38 @@ Core features:
To spin up this example locally, follow the [Quick Start](#quick-start). Then [Seed](#seed) the database with a few pages, posts, and projects.
### Working with Postgres
Postgres and other SQL-based databases follow a strict schema for managing your data. In comparison to our MongoDB adapter, this means that there's a few extra steps to working with Postgres.
Note that often times when making big schema changes you can run the risk of losing data if you're not manually migrating it.
#### Local development
Ideally we recommend running a local copy of your database so that schema updates are as fast as possible. By default the Postgres adapter has `push: true` for development environments. This will let you add, modify and remove fields and collections without needing to run any data migrations.
If your database is pointed to production you will want to set `push: false` otherwise you will risk losing data or having your migrations out of sync.
#### Migrations
[Migrations](https://payloadcms.com/docs/database/migrations) are essentially SQL code versions that keeps track of your schema. When deploy with Postgres you will need to make sure you create and then run your migrations.
Locally create a migration
```bash
pnpm payload migrate:create
```
This creates the migration files you will need to push alongside with your new configuration.
On the server after building and before running `pnpm start` you will want to run your migrations
```bash
pnpm payload migrate
```
This command will check for any migrations that have not yet been run and try to run them and it will keep a record of migrations that have been run in the database.
### Docker
Alternatively, you can use [Docker](https://www.docker.com) to spin up this template locally. To do so, follow these steps:

View File

@@ -132,6 +132,7 @@ export const Pages: CollectionConfig<'pages'> = {
autosave: {
interval: 100, // We set this interval for optimal live preview
},
schedulePublish: true,
},
maxPerDoc: 50,
},

View File

@@ -229,6 +229,7 @@ export const Posts: CollectionConfig<'posts'> = {
autosave: {
interval: 100, // We set this interval for optimal live preview
},
schedulePublish: true,
},
maxPerDoc: 50,
},

View File

@@ -20,6 +20,7 @@ export interface Config {
forms: Form;
'form-submissions': FormSubmission;
search: Search;
'payload-jobs': PayloadJob;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
@@ -35,6 +36,7 @@ export interface Config {
forms: FormsSelect<false> | FormsSelect<true>;
'form-submissions': FormSubmissionsSelect<false> | FormSubmissionsSelect<true>;
search: SearchSelect<false> | SearchSelect<true>;
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@@ -55,7 +57,13 @@ export interface Config {
collection: 'users';
};
jobs: {
tasks: unknown;
tasks: {
schedulePublish: TaskSchedulePublish;
inline: {
input: unknown;
output: unknown;
};
};
workflows: unknown;
};
}
@@ -735,6 +743,98 @@ export interface Search {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-jobs".
*/
export interface PayloadJob {
id: number;
/**
* Input data provided to the job
*/
input?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
taskStatus?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
completedAt?: string | null;
totalTried?: number | null;
/**
* If hasError is true this job will not be retried
*/
hasError?: boolean | null;
/**
* If hasError is true, this is the error that caused it
*/
error?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
/**
* Task execution log
*/
log?:
| {
executedAt: string;
completedAt: string;
taskSlug: 'inline' | 'schedulePublish';
taskID: string;
input?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
output?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
state: 'failed' | 'succeeded';
error?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
id?: string | null;
}[]
| null;
taskSlug?: ('inline' | 'schedulePublish') | null;
queue?: string | null;
waitUntil?: string | null;
processing?: boolean | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
@@ -777,6 +877,10 @@ export interface PayloadLockedDocument {
| ({
relationTo: 'search';
value: number | Search;
} | null)
| ({
relationTo: 'payload-jobs';
value: number | PayloadJob;
} | null);
globalSlug?: string | null;
user: {
@@ -1305,6 +1409,37 @@ export interface SearchSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-jobs_select".
*/
export interface PayloadJobsSelect<T extends boolean = true> {
input?: T;
taskStatus?: T;
completedAt?: T;
totalTried?: T;
hasError?: T;
error?: T;
log?:
| T
| {
executedAt?: T;
completedAt?: T;
taskSlug?: T;
taskID?: T;
input?: T;
output?: T;
state?: T;
error?: T;
id?: T;
};
taskSlug?: T;
queue?: T;
waitUntil?: T;
processing?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
@@ -1441,6 +1576,28 @@ export interface FooterSelect<T extends boolean = true> {
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "TaskSchedulePublish".
*/
export interface TaskSchedulePublish {
input: {
type?: ('publish' | 'unpublish') | null;
locale?: string | null;
doc?:
| ({
relationTo: 'pages';
value: number | Page;
} | null)
| ({
relationTo: 'posts';
value: number | Post;
} | null);
global?: string | null;
user?: (number | null) | User;
};
output?: unknown;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "BannerBlock".

View File

@@ -3,7 +3,7 @@ import { vercelPostgresAdapter } from '@payloadcms/db-vercel-postgres'
import sharp from 'sharp' // sharp-import
import path from 'path'
import { buildConfig } from 'payload'
import { buildConfig, PayloadRequest } from 'payload'
import { fileURLToPath } from 'url'
import { Categories } from './collections/Categories'
@@ -81,4 +81,19 @@ export default buildConfig({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
jobs: {
access: {
run: ({ req }: { req: PayloadRequest }): boolean => {
// Allow logged in users to execute this endpoint (default)
if (req.user) return true
// If there is no logged in user, then check
// for the Vercel Cron secret to be present as an
// Authorization header:
const authHeader = req.headers.get('authorization')
return authHeader === `Bearer ${process.env.CRON_SECRET}`
},
},
tasks: [],
},
})

View File

@@ -0,0 +1,8 @@
{
"crons": [
{
"path": "/api/payload-jobs/run",
"schedule": "*/5 * * * *"
}
]
}