chore(templates): clean up templates dependencies (#7139)

- use react 19 types
- no need for dotenv - next has their own dotenv file loader
- disable deprecation warnings by default (newer node version spam you
with it)
- disable turbo by default as hmr is broken and we cannot test against
it yet
- remove ts-node mention in tsconfig as it's not used anymore
- remove unused packages
- [fix: potential seed issues due to parallel payload operations being
on the same
transaction](f899f6a408)
and
b3b565dd75
@DanRibbens can you sense-check this? I do remember that anything
running in parallel should never be on the same transaction

---------

Co-authored-by: Paul Popus <paul@nouance.io>
This commit is contained in:
Alessio Gravili
2024-07-16 22:43:58 -04:00
committed by GitHub
parent fe23ca5b1a
commit 4ff8b20ddb
27 changed files with 825 additions and 2348 deletions

View File

@@ -22,8 +22,8 @@
"graphql": "^16.8.1", "graphql": "^16.8.1",
"next": "15.0.0-canary.53", "next": "15.0.0-canary.53",
"payload": "beta", "payload": "beta",
"react": "^19.0.0-rc-6230622a1a-20240610", "react": "19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0-rc-6230622a1a-20240610", "react-dom": "19.0.0-rc-6230622a1a-20240610",
"sharp": "0.32.6" "sharp": "0.32.6"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -22,8 +22,8 @@
"graphql": "^16.8.1", "graphql": "^16.8.1",
"next": "15.0.0-canary.53", "next": "15.0.0-canary.53",
"payload": "beta", "payload": "beta",
"react": "^19.0.0-rc-6230622a1a-20240610", "react": "19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0-rc-6230622a1a-20240610", "react-dom": "19.0.0-rc-6230622a1a-20240610",
"sharp": "0.32.6" "sharp": "0.32.6"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -7,4 +7,4 @@ node_modules
.vercel .vercel
# Payload default media upload directory # Payload default media upload directory
./public/media public/media/

View File

@@ -5,24 +5,19 @@
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "next build", "build": "cross-env NODE_OPTIONS=--no-deprecation next build",
"dev": "next dev --turbo", "dev": "cross-env NODE_OPTIONS=--no-deprecation next dev",
"dev:prod": "rm -rf .next && pnpm build && pnpm serve", "dev:prod": "cross-env NODE_OPTIONS=--no-deprecation rm -rf .next && pnpm build && pnpm serve",
"generate:types": "payload generate:types", "generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"ii": "pnpm --ignore-workspace install", "ii": "cross-env NODE_OPTIONS=--no-deprecation pnpm --ignore-workspace install",
"lint": "next lint", "lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
"lint:fix": "next lint --fix", "lint:fix": "cross-env NODE_OPTIONS=--no-deprecation next lint --fix",
"payload": "payload", "payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"reinstall": "rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install", "reinstall": "cross-env NODE_OPTIONS=--no-deprecation rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install",
"start": "next start" "start": "cross-env NODE_OPTIONS=--no-deprecation next start"
}, },
"dependencies": { "dependencies": {
"@lexical/list": "0.16.1",
"@lexical/react": "0.16.1",
"@lexical/rich-text": "0.16.1",
"@lexical/utils": "0.16.1",
"@payloadcms/db-mongodb": "3.0.0-beta.63", "@payloadcms/db-mongodb": "3.0.0-beta.63",
"@payloadcms/db-postgres": "3.0.0-beta.63",
"@payloadcms/live-preview-react": "3.0.0-beta.63", "@payloadcms/live-preview-react": "3.0.0-beta.63",
"@payloadcms/next": "3.0.0-beta.63", "@payloadcms/next": "3.0.0-beta.63",
"@payloadcms/plugin-cloud": "3.0.0-beta.63", "@payloadcms/plugin-cloud": "3.0.0-beta.63",
@@ -39,8 +34,6 @@
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^8.2.0",
"escape-html": "^1.0.3",
"geist": "^1.3.0", "geist": "^1.3.0",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"jsonwebtoken": "9.0.1", "jsonwebtoken": "9.0.1",
@@ -50,11 +43,9 @@
"payload": "3.0.0-beta.63", "payload": "3.0.0-beta.63",
"payload-admin-bar": "^1.0.6", "payload-admin-bar": "^1.0.6",
"prism-react-renderer": "^2.3.1", "prism-react-renderer": "^2.3.1",
"qs": "6.11.2", "react": "19.0.0-rc-6230622a1a-20240610",
"react": "beta", "react-dom": "19.0.0-rc-6230622a1a-20240610",
"react-dom": "beta",
"react-hook-form": "7.45.4", "react-hook-form": "7.45.4",
"react-router-dom": "5.3.4",
"sharp": "0.32.6", "sharp": "0.32.6",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7"
@@ -65,8 +56,8 @@
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.13",
"@types/escape-html": "^1.0.2", "@types/escape-html": "^1.0.2",
"@types/node": "18.11.3", "@types/node": "18.11.3",
"@types/qs": "^6.9.8", "@types/react": "npm:types-react@19.0.0-rc.0",
"@types/react": "^18.3.0", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"eslint": "^8", "eslint": "^8",
@@ -79,7 +70,14 @@
"engines": { "engines": {
"node": "^18.20.2 || >=20.9.0" "node": "^18.20.2 || >=20.9.0"
}, },
"pnpm": {
"overrides": { "overrides": {
"@types/react": "18.2.74" "@types/react": "npm:types-react@19.0.0-rc.0",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.0"
}
},
"overrides": {
"@types/react": "npm:types-react@19.0.0-rc.0",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.0"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -19,15 +19,11 @@ export const RelatedPosts: React.FC<RelatedPostsProps> = (props) => {
<div className={clsx('container', className)}> <div className={clsx('container', className)}>
{introContent && <RichText content={introContent} enableGutter={false} />} {introContent && <RichText content={introContent} enableGutter={false} />}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-8 items-stretch">
{docs?.map((doc, index) => { {docs?.map((doc, index) => {
if (typeof doc === 'string') return null if (typeof doc === 'string') return null
return ( return <Card key={index} doc={doc} relationTo="posts" showCategories />
<div key={index}>
<Card doc={doc} relationTo="posts" showCategories />
</div>
)
})} })}
</div> </div>
</div> </div>

View File

@@ -223,8 +223,8 @@ export interface Page {
)[]; )[];
meta?: { meta?: {
title?: string | null; title?: string | null;
description?: string | null;
image?: string | Media | null; image?: string | Media | null;
description?: string | null;
}; };
publishedAt?: string | null; publishedAt?: string | null;
slug?: string | null; slug?: string | null;
@@ -272,7 +272,7 @@ export interface Media {
*/ */
export interface Category { export interface Category {
id: string; id: string;
title?: string | null; title: string;
parent?: (string | null) | Category; parent?: (string | null) | Category;
breadcrumbs?: breadcrumbs?:
| { | {
@@ -291,6 +291,7 @@ export interface Category {
*/ */
export interface Post { export interface Post {
id: string; id: string;
title: string;
content: { content: {
root: { root: {
type: string; type: string;
@@ -310,10 +311,9 @@ export interface Post {
categories?: (string | Category)[] | null; categories?: (string | Category)[] | null;
meta?: { meta?: {
title?: string | null; title?: string | null;
description?: string | null;
image?: string | Media | null; image?: string | Media | null;
description?: string | null;
}; };
title: string;
publishedAt?: string | null; publishedAt?: string | null;
authors?: (string | User)[] | null; authors?: (string | User)[] | null;
populatedAuthors?: populatedAuthors?:

View File

@@ -16,7 +16,6 @@ import {
} from '@payloadcms/richtext-lexical' } from '@payloadcms/richtext-lexical'
import sharp from 'sharp' // editor-import import sharp from 'sharp' // editor-import
import { UnderlineFeature } from '@payloadcms/richtext-lexical' import { UnderlineFeature } from '@payloadcms/richtext-lexical'
import dotenv from 'dotenv'
import path from 'path' import path from 'path'
import { buildConfig } from 'payload' import { buildConfig } from 'payload'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
@@ -32,17 +31,21 @@ import { seed } from './payload/endpoints/seed'
import { Footer } from './payload/globals/Footer/Footer' import { Footer } from './payload/globals/Footer/Footer'
import { Header } from './payload/globals/Header/Header' import { Header } from './payload/globals/Header/Header'
import { revalidateRedirects } from './payload/hooks/revalidateRedirects' import { revalidateRedirects } from './payload/hooks/revalidateRedirects'
import { GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types'
import { Page, Post } from 'src/payload-types'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
const generateTitle = () => { const generateTitle: GenerateTitle<Post | Page> = ({ doc }) => {
return 'My Website' return doc?.title ? `${doc.title} | Payload Website Template` : 'Payload Website Template'
} }
dotenv.config({ const generateURL: GenerateURL<Post | Page> = ({ doc }) => {
path: path.resolve(dirname, '../../.env'), return doc?.slug
}) ? `${process.env.NEXT_PUBLIC_SERVER_URL}/${doc.slug}`
: process.env.NEXT_PUBLIC_SERVER_URL
}
export default buildConfig({ export default buildConfig({
admin: { admin: {
@@ -133,10 +136,8 @@ export default buildConfig({
collections: ['categories'], collections: ['categories'],
}), }),
seoPlugin({ seoPlugin({
collections: ['pages', 'posts'],
generateTitle, generateTitle,
tabbedUI: true, generateURL,
uploadsCollection: 'media',
}), }),
formBuilderPlugin({ formBuilderPlugin({
fields: { fields: {

View File

@@ -18,6 +18,7 @@ const Categories: CollectionConfig = {
{ {
name: 'title', name: 'title',
type: 'text', type: 'text',
required: true,
}, },
], ],
} }

View File

@@ -39,6 +39,7 @@ export const Media: CollectionConfig = {
}, },
], ],
upload: { upload: {
staticDir: path.resolve(dirname, '../../public/media'), // Upload to the public/media directory in Next.js making them publicly accessible even outside of Payload
staticDir: path.resolve(dirname, '../../../public/media'),
}, },
} }

View File

@@ -13,6 +13,13 @@ import { populatePublishedAt } from '../../hooks/populatePublishedAt'
import { generatePreviewPath } from '../../utilities/generatePreviewPath' import { generatePreviewPath } from '../../utilities/generatePreviewPath'
import { revalidatePage } from './hooks/revalidatePage' import { revalidatePage } from './hooks/revalidatePage'
import {
MetaDescriptionField,
MetaImageField,
MetaTitleField,
OverviewField,
PreviewField,
} from '@payloadcms/plugin-seo/fields'
export const Pages: CollectionConfig = { export const Pages: CollectionConfig = {
slug: 'pages', slug: 'pages',
access: { access: {
@@ -35,19 +42,17 @@ export const Pages: CollectionConfig = {
generatePreviewPath({ path: `/${typeof doc?.slug === 'string' ? doc.slug : ''}` }), generatePreviewPath({ path: `/${typeof doc?.slug === 'string' ? doc.slug : ''}` }),
useAsTitle: 'title', useAsTitle: 'title',
}, },
fields: [
{
type: 'tabs',
tabs: [
{
fields: [ fields: [
{ {
name: 'title', name: 'title',
type: 'text', type: 'text',
required: true, required: true,
}, },
hero, {
], type: 'tabs',
tabs: [
{
fields: [hero],
label: 'Hero', label: 'Hero',
}, },
{ {
@@ -61,6 +66,33 @@ export const Pages: CollectionConfig = {
], ],
label: 'Content', label: 'Content',
}, },
{
name: 'meta',
label: 'SEO',
fields: [
OverviewField({
titlePath: 'meta.title',
descriptionPath: 'meta.description',
imagePath: 'meta.image',
}),
MetaTitleField({
hasGenerateFn: true,
}),
MetaImageField({
relationTo: 'media',
}),
MetaDescriptionField({}),
PreviewField({
// if the `generateUrl` function is configured
hasGenerateFn: true,
// field paths to match the target field for data
titlePath: 'meta.title',
descriptionPath: 'meta.description',
}),
],
},
], ],
}, },
{ {
@@ -79,7 +111,7 @@ export const Pages: CollectionConfig = {
versions: { versions: {
drafts: { drafts: {
autosave: { autosave: {
interval: 350, // We set this interval for optimal live preview interval: 100, // We set this interval for optimal live preview
}, },
}, },
maxPerDoc: 50, maxPerDoc: 50,

View File

@@ -4,18 +4,20 @@ import type { CollectionAfterReadHook } from 'payload'
// This means that we need to populate the authors manually here to protect user privacy // This means that we need to populate the authors manually here to protect user privacy
// GraphQL will not return mutated user data that differs from the underlying schema // GraphQL will not return mutated user data that differs from the underlying schema
// So we use an alternative `populatedAuthors` field to populate the user data, hidden from the admin UI // So we use an alternative `populatedAuthors` field to populate the user data, hidden from the admin UI
export const populateAuthors: CollectionAfterReadHook = async ({ doc, req: { payload } }) => { export const populateAuthors: CollectionAfterReadHook = async ({ doc, req, req: { payload } }) => {
if (doc?.authors) { if (doc?.authors) {
const authorDocs = await Promise.all( const authorDocs = []
doc.authors.map(
async (author) => for (const author of doc.authors) {
await payload.findByID({ const authorDoc = await payload.findByID({
id: typeof author === 'object' ? author?.id : author, id: typeof author === 'object' ? author?.id : author,
collection: 'users', collection: 'users',
depth: 0, depth: 0,
}), req,
), })
)
authorDocs.push(authorDoc)
}
doc.populatedAuthors = authorDocs.map((authorDoc) => ({ doc.populatedAuthors = authorDocs.map((authorDoc) => ({
id: authorDoc.id, id: authorDoc.id,

View File

@@ -19,6 +19,14 @@ import { generatePreviewPath } from '../../utilities/generatePreviewPath'
import { populateAuthors } from './hooks/populateAuthors' import { populateAuthors } from './hooks/populateAuthors'
import { revalidatePost } from './hooks/revalidatePost' import { revalidatePost } from './hooks/revalidatePost'
import {
MetaDescriptionField,
MetaImageField,
MetaTitleField,
OverviewField,
PreviewField,
} from '@payloadcms/plugin-seo/fields'
export const Posts: CollectionConfig = { export const Posts: CollectionConfig = {
slug: 'posts', slug: 'posts',
access: { access: {
@@ -42,6 +50,11 @@ export const Posts: CollectionConfig = {
useAsTitle: 'title', useAsTitle: 'title',
}, },
fields: [ fields: [
{
name: 'title',
type: 'text',
required: true,
},
{ {
type: 'tabs', type: 'tabs',
tabs: [ tabs: [
@@ -98,15 +111,34 @@ export const Posts: CollectionConfig = {
], ],
label: 'Meta', label: 'Meta',
}, },
{
name: 'meta',
label: 'SEO',
fields: [
OverviewField({
titlePath: 'meta.title',
descriptionPath: 'meta.description',
imagePath: 'meta.image',
}),
MetaTitleField({
hasGenerateFn: true,
}),
MetaImageField({
relationTo: 'media',
}),
MetaDescriptionField({}),
PreviewField({
// if the `generateUrl` function is configured
hasGenerateFn: true,
// field paths to match the target field for data
titlePath: 'meta.title',
descriptionPath: 'meta.description',
}),
], ],
}, },
{ ],
name: 'title',
type: 'text',
admin: {
position: 'sidebar',
},
required: true,
}, },
{ {
name: 'publishedAt', name: 'publishedAt',
@@ -169,7 +201,9 @@ export const Posts: CollectionConfig = {
}, },
versions: { versions: {
drafts: { drafts: {
autosave: true, autosave: {
interval: 100, // We set this interval for optimal live preview
},
}, },
maxPerDoc: 50, maxPerDoc: 50,
}, },

View File

@@ -1,6 +1,16 @@
'use client' 'use client'
import React, { Fragment, useCallback, useState } from 'react' import React, { Fragment, useCallback, useState } from 'react'
import { toast } from '@payloadcms/ui'
const SuccessMessage: React.FC = () => (
<div>
Database seeded! You can now{' '}
<a target="_blank" href="/">
visit your website
</a>
</div>
)
export const SeedButton: React.FC = () => { export const SeedButton: React.FC = () => {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@@ -17,6 +27,7 @@ export const SeedButton: React.FC = () => {
try { try {
await fetch('/api/seed') await fetch('/api/seed')
setSeeded(true) setSeeded(true)
toast.success(<SuccessMessage />, { duration: 5000 })
} catch (err) { } catch (err) {
setError(err) setError(err)
} }

View File

@@ -17,7 +17,9 @@ const BeforeDashboard: React.FC = () => {
<li> <li>
<SeedButton /> <SeedButton />
{' with a few pages, posts, and projects to jump-start your new site, then '} {' with a few pages, posts, and projects to jump-start your new site, then '}
<a href="/">visit your website</a> <a href="/" target="_blank">
visit your website
</a>
{' to see the results.'} {' to see the results.'}
</li> </li>
<li> <li>

View File

@@ -10,9 +10,14 @@ export const seed: PayloadHandler = async (req): Promise<Response> => {
} }
try { try {
// Create a transaction so that all seeding happens in one transaction
await initTransaction(req) await initTransaction(req)
await seedScript({ payload, req }) await seedScript({ payload, req })
// Finalise transactiojn
await commitTransaction(req) await commitTransaction(req)
return Response.json({ success: true }) return Response.json({ success: true })
} catch (error: unknown) { } catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error' const message = error instanceof Error ? error.message : 'Unknown error'

View File

@@ -10,6 +10,7 @@ export const contact: Partial<Page> = {
{ {
blockType: 'formBlock', blockType: 'formBlock',
enableIntro: true, enableIntro: true,
// @ts-ignore
form: '{{CONTACT_FORM_ID}}', form: '{{CONTACT_FORM_ID}}',
introContent: { introContent: {
root: { root: {

View File

@@ -80,552 +80,6 @@ export const homeStatic: Page = {
}, },
}, },
}, },
/* layout: [
{
blockName: 'Content Block',
blockType: 'content',
columns: [
{
richText: {
root: {
type: 'root',
children: [
{
type: 'heading',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Core features',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
tag: 'h2',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
version: 1,
},
},
size: 'full',
},
{
enableLink: false,
link: {
label: '',
reference: null,
url: '',
},
richText: {
root: {
type: 'root',
children: [
{
type: 'heading',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Admin Dashboard',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
tag: 'h3',
version: 1,
},
{
type: 'paragraph',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: "Manage this site's pages and posts from the ",
version: 1,
},
{
type: 'link',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'admin dashboard',
version: 1,
},
],
direction: 'ltr',
fields: {
linkType: 'custom',
newTab: false,
url: '/admin',
},
format: '',
indent: 0,
version: 2,
},
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: '.',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
textFormat: 0,
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
version: 1,
},
},
size: 'oneThird',
},
{
enableLink: false,
link: {
label: '',
reference: null,
url: '',
},
richText: {
root: {
type: 'root',
children: [
{
type: 'heading',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Preview',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
tag: 'h3',
version: 1,
},
{
type: 'paragraph',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Using versions, drafts, and preview, editors can review and share their changes before publishing them.',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
textFormat: 0,
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
version: 1,
},
},
size: 'oneThird',
},
{
enableLink: false,
link: {
label: '',
reference: null,
url: '',
},
richText: {
root: {
type: 'root',
children: [
{
type: 'heading',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Page Builder',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
tag: 'h3',
version: 1,
},
{
type: 'paragraph',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Custom page builder allows you to create unique page, post, and project layouts for any type of content.',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
textFormat: 0,
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
version: 1,
},
},
size: 'oneThird',
},
{
enableLink: false,
link: {
label: '',
reference: null,
url: '',
},
richText: {
root: {
type: 'root',
children: [
{
type: 'heading',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'SEO',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
tag: 'h3',
version: 1,
},
{
type: 'paragraph',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Editors have complete control over SEO data and site content directly from the ',
version: 1,
},
{
type: 'link',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'admin dashboard',
version: 1,
},
],
direction: 'ltr',
fields: {
linkType: 'custom',
newTab: false,
url: '/admin',
},
format: '',
indent: 0,
version: 2,
},
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: '.',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
textFormat: 0,
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
version: 1,
},
},
size: 'oneThird',
},
{
enableLink: false,
link: {
label: '',
reference: null,
url: '',
},
richText: {
root: {
type: 'root',
children: [
{
type: 'heading',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Dark Mode',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
tag: 'h3',
version: 1,
},
{
type: 'paragraph',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Users will experience this site in their preferred color scheme and each block can be inverted.',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
textFormat: 0,
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
version: 1,
},
},
size: 'oneThird',
},
],
},
{
blockName: 'Archive Block',
blockType: 'archive',
categories: [],
introContent: {
root: {
type: 'root',
children: [
{
type: 'heading',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Recent posts',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
tag: 'h3',
version: 1,
},
{
type: 'paragraph',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'The posts below are displayed in an "Archive" layout building block which is an extremely powerful way to display documents on a page. It can be auto-populated by collection or by category, or posts can be individually selected. Pagination controls will automatically appear if the number of results exceeds the number of items per page.',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
textFormat: 0,
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
version: 1,
},
},
populateBy: 'collection',
relationTo: 'posts',
},
{
blockName: 'CTA',
blockType: 'cta',
links: [
{
link: {
type: 'custom',
appearance: 'default',
label: 'All posts',
url: '/posts',
},
},
],
richText: {
root: {
type: 'root',
children: [
{
type: 'heading',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'This is a call to action',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
tag: 'h3',
version: 1,
},
{
type: 'paragraph',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'This is a custom layout building block ',
version: 1,
},
{
type: 'link',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'configured in the admin dashboard',
version: 1,
},
],
direction: 'ltr',
fields: {
linkType: 'custom',
newTab: false,
url: '/admin',
},
format: '',
indent: 0,
version: 2,
},
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: '.',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
textFormat: 0,
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
version: 1,
},
},
},
], */
meta: { meta: {
description: 'An open-source website built with Payload and Next.js.', description: 'An open-source website built with Payload and Next.js.',
title: 'Payload Website Template', title: 'Payload Website Template',

View File

@@ -1,6 +1,6 @@
import type { Page } from '../../payload-types' import type { RequiredDataFromCollectionSlug } from 'payload'
export const home: Partial<Page> = { export const home: RequiredDataFromCollectionSlug<'pages'> = {
slug: 'home', slug: 'home',
_status: 'published', _status: 'published',
hero: { hero: {
@@ -23,6 +23,7 @@ export const home: Partial<Page> = {
}, },
}, },
], ],
// @ts-ignore
media: '{{IMAGE_1}}', media: '{{IMAGE_1}}',
richText: { richText: {
root: { root: {
@@ -501,6 +502,7 @@ export const home: Partial<Page> = {
{ {
blockName: 'Media Block', blockName: 'Media Block',
blockType: 'mediaBlock', blockType: 'mediaBlock',
// @ts-ignore
media: '{{IMAGE_2}}', media: '{{IMAGE_2}}',
position: 'default', position: 'default',
}, },
@@ -658,6 +660,7 @@ export const home: Partial<Page> = {
], ],
meta: { meta: {
description: 'An open-source website built with Payload and Next.js.', description: 'An open-source website built with Payload and Next.js.',
// @ts-ignore
image: '{{IMAGE_1}}', image: '{{IMAGE_1}}',
title: 'Payload Website Template', title: 'Payload Website Template',
}, },

View File

@@ -1,4 +1,4 @@
import type { Payload, PayloadRequest } from 'payload' import type { CollectionSlug, GlobalSlug, Payload, PayloadRequest } from 'payload'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
@@ -12,12 +12,20 @@ import { image2 } from './image-2'
import { post1 } from './post-1' import { post1 } from './post-1'
import { post2 } from './post-2' import { post2 } from './post-2'
import { post3 } from './post-3' import { post3 } from './post-3'
import { exists } from 'fs-extra'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
const collections = ['categories', 'media', 'pages', 'posts', 'forms', 'form-submissions'] const collections: CollectionSlug[] = [
const globals = ['header', 'footer'] 'categories',
'media',
'pages',
'posts',
'forms',
'form-submissions',
]
const globals: GlobalSlug[] = ['header', 'footer']
// Next.js revalidation errors are normal when seeding the database without a server running // Next.js revalidation errors are normal when seeding the database without a server running
// i.e. running `yarn seed` locally instead of using the admin UI within an active app // i.e. running `yarn seed` locally instead of using the admin UI within an active app
@@ -47,41 +55,50 @@ export const seed = async ({
payload.logger.info(`— Clearing collections and globals...`) payload.logger.info(`— Clearing collections and globals...`)
// clear the database // clear the database
await Promise.all([ for (const global of globals) {
...collections.map((collection) => await payload.updateGlobal({
payload.delete({ slug: global,
collection: collection as 'media', data: {
navItems: [],
},
req, req,
})
}
for (const collection of collections) {
console.log('delete', collection)
await payload.delete({
collection: collection,
where: {
id: {
exists: true,
},
},
req,
})
}
const pages = await payload.delete({
collection: 'pages',
where: {}, where: {},
}),
),
...globals.map((global) =>
payload.updateGlobal({
slug: global as 'header',
data: {},
req, req,
}), })
),
]) console.log({ pages })
payload.logger.info(`— Seeding demo author and user...`) payload.logger.info(`— Seeding demo author and user...`)
await Promise.all(
['demo-author@payloadcms.com'].map(async (email) => {
await payload.delete({ await payload.delete({
collection: 'users', collection: 'users',
req,
where: { where: {
email: { email: {
equals: email, equals: 'demo-author@payloadcms.com',
}, },
}, },
req,
}) })
}),
)
const [demoAuthor] = await Promise.all([ const demoAuthor = await payload.create({
await payload.create({
collection: 'users', collection: 'users',
data: { data: {
name: 'Demo Author', name: 'Demo Author',
@@ -89,91 +106,89 @@ export const seed = async ({
password: 'password', password: 'password',
}, },
req, req,
}), })
])
let demoAuthorID = demoAuthor.id let demoAuthorID: number | string = demoAuthor.id
payload.logger.info(`— Seeding media...`) payload.logger.info(`— Seeding media...`)
const image1Doc = await payload.create({
const [image1Doc, image2Doc, image3Doc, imageHomeDoc] = await Promise.all([
await payload.create({
collection: 'media', collection: 'media',
data: image1, data: image1,
filePath: path.resolve(dirname, 'image-post1.webp'), filePath: path.resolve(dirname, 'image-post1.webp'),
req, req,
}), })
await payload.create({ const image2Doc = await payload.create({
collection: 'media', collection: 'media',
data: image2, data: image2,
filePath: path.resolve(dirname, 'image-post2.webp'), filePath: path.resolve(dirname, 'image-post2.webp'),
req, req,
}), })
await payload.create({ const image3Doc = await payload.create({
collection: 'media', collection: 'media',
data: image2, data: image2,
filePath: path.resolve(dirname, 'image-post3.webp'), filePath: path.resolve(dirname, 'image-post3.webp'),
req, req,
}), })
await payload.create({ const imageHomeDoc = await payload.create({
collection: 'media', collection: 'media',
data: image2, data: image2,
filePath: path.resolve(dirname, 'image-hero1.webp'), filePath: path.resolve(dirname, 'image-hero1.webp'),
req, req,
}), })
])
payload.logger.info(`— Seeding categories...`) payload.logger.info(`— Seeding categories...`)
const technologyCategory = await payload.create({
const [technologyCategory, newsCategory, financeCategory] = await Promise.all([
await payload.create({
collection: 'categories', collection: 'categories',
data: { data: {
title: 'Technology', title: 'Technology',
}, },
req, req,
}), })
await payload.create({
const newsCategory = await payload.create({
collection: 'categories', collection: 'categories',
data: { data: {
title: 'News', title: 'News',
}, },
req, req,
}), })
await payload.create({
const financeCategory = await payload.create({
collection: 'categories', collection: 'categories',
data: { data: {
title: 'Finance', title: 'Finance',
}, },
req, req,
}), })
await payload.create({ await payload.create({
collection: 'categories', collection: 'categories',
data: { data: {
title: 'Design', title: 'Design',
}, },
req, req,
}), })
await payload.create({ await payload.create({
collection: 'categories', collection: 'categories',
data: { data: {
title: 'Software', title: 'Software',
}, },
req, req,
}), })
await payload.create({ await payload.create({
collection: 'categories', collection: 'categories',
data: { data: {
title: 'Engineering', title: 'Engineering',
}, },
req, req,
}), })
])
let image1ID = image1Doc.id let image1ID: number | string = image1Doc.id
let image2ID = image2Doc.id let image2ID: number | string = image2Doc.id
let image3ID = image3Doc.id let image3ID: number | string = image3Doc.id
let imageHomeID = imageHomeDoc.id let imageHomeID: number | string = imageHomeDoc.id
if (payload.db.defaultIDType === 'text') { if (payload.db.defaultIDType === 'text') {
image1ID = `"${image1Doc.id}"` image1ID = `"${image1Doc.id}"`
@@ -191,9 +206,9 @@ export const seed = async ({
collection: 'posts', collection: 'posts',
data: JSON.parse( data: JSON.parse(
JSON.stringify({ ...post1, categories: [technologyCategory.id] }) JSON.stringify({ ...post1, categories: [technologyCategory.id] })
.replace(/"\{\{IMAGE_1\}\}"/g, image1ID) .replace(/"\{\{IMAGE_1\}\}"/g, String(image1ID))
.replace(/"\{\{IMAGE_2\}\}"/g, image2ID) .replace(/"\{\{IMAGE_2\}\}"/g, String(image2ID))
.replace(/"\{\{AUTHOR\}\}"/g, demoAuthorID), .replace(/"\{\{AUTHOR\}\}"/g, String(demoAuthorID)),
), ),
req, req,
}) })
@@ -202,9 +217,9 @@ export const seed = async ({
collection: 'posts', collection: 'posts',
data: JSON.parse( data: JSON.parse(
JSON.stringify({ ...post2, categories: [newsCategory.id] }) JSON.stringify({ ...post2, categories: [newsCategory.id] })
.replace(/"\{\{IMAGE_1\}\}"/g, image2ID) .replace(/"\{\{IMAGE_1\}\}"/g, String(image2ID))
.replace(/"\{\{IMAGE_2\}\}"/g, image3ID) .replace(/"\{\{IMAGE_2\}\}"/g, String(image3ID))
.replace(/"\{\{AUTHOR\}\}"/g, demoAuthorID), .replace(/"\{\{AUTHOR\}\}"/g, String(demoAuthorID)),
), ),
req, req,
}) })
@@ -213,16 +228,14 @@ export const seed = async ({
collection: 'posts', collection: 'posts',
data: JSON.parse( data: JSON.parse(
JSON.stringify({ ...post3, categories: [financeCategory.id] }) JSON.stringify({ ...post3, categories: [financeCategory.id] })
.replace(/"\{\{IMAGE_1\}\}"/g, image3ID) .replace(/"\{\{IMAGE_1\}\}"/g, String(image3ID))
.replace(/"\{\{IMAGE_2\}\}"/g, image1ID) .replace(/"\{\{IMAGE_2\}\}"/g, String(image1ID))
.replace(/"\{\{AUTHOR\}\}"/g, demoAuthorID), .replace(/"\{\{AUTHOR\}\}"/g, String(demoAuthorID)),
), ),
req, req,
}) })
// update each post with related posts // update each post with related posts
await Promise.all([
await payload.update({ await payload.update({
id: post1Doc.id, id: post1Doc.id,
collection: 'posts', collection: 'posts',
@@ -230,7 +243,7 @@ export const seed = async ({
relatedPosts: [post2Doc.id, post3Doc.id], relatedPosts: [post2Doc.id, post3Doc.id],
}, },
req, req,
}), })
await payload.update({ await payload.update({
id: post2Doc.id, id: post2Doc.id,
collection: 'posts', collection: 'posts',
@@ -238,7 +251,7 @@ export const seed = async ({
relatedPosts: [post1Doc.id, post3Doc.id], relatedPosts: [post1Doc.id, post3Doc.id],
}, },
req, req,
}), })
await payload.update({ await payload.update({
id: post3Doc.id, id: post3Doc.id,
collection: 'posts', collection: 'posts',
@@ -246,8 +259,7 @@ export const seed = async ({
relatedPosts: [post1Doc.id, post2Doc.id], relatedPosts: [post1Doc.id, post2Doc.id],
}, },
req, req,
}), })
])
payload.logger.info(`— Seeding home page...`) payload.logger.info(`— Seeding home page...`)
@@ -255,8 +267,8 @@ export const seed = async ({
collection: 'pages', collection: 'pages',
data: JSON.parse( data: JSON.parse(
JSON.stringify(home) JSON.stringify(home)
.replace(/"\{\{IMAGE_1\}\}"/g, imageHomeID) .replace(/"\{\{IMAGE_1\}\}"/g, String(imageHomeID))
.replace(/"\{\{IMAGE_2\}\}"/g, image2ID), .replace(/"\{\{IMAGE_2\}\}"/g, String(image2ID)),
), ),
req, req,
}) })
@@ -269,7 +281,7 @@ export const seed = async ({
req, req,
}) })
let contactFormID = contactForm.id let contactFormID: number | string = contactForm.id
if (payload.db.defaultIDType === 'text') { if (payload.db.defaultIDType === 'text') {
contactFormID = `"${contactFormID}"` contactFormID = `"${contactFormID}"`
@@ -280,7 +292,7 @@ export const seed = async ({
const contactPage = await payload.create({ const contactPage = await payload.create({
collection: 'pages', collection: 'pages',
data: JSON.parse( data: JSON.parse(
JSON.stringify(contactPageData).replace(/"\{\{CONTACT_FORM_ID\}\}"/g, contactFormID), JSON.stringify(contactPageData).replace(/"\{\{CONTACT_FORM_ID\}\}"/g, String(contactFormID)),
), ),
req, req,
}) })

View File

@@ -3,6 +3,7 @@ import type { Post } from '../../payload-types'
export const post1: Partial<Post> = { export const post1: Partial<Post> = {
slug: 'digital-horizons', slug: 'digital-horizons',
_status: 'published', _status: 'published',
// @ts-ignore
authors: ['{{AUTHOR}}'], authors: ['{{AUTHOR}}'],
content: { content: {
root: { root: {
@@ -295,6 +296,7 @@ export const post1: Partial<Post> = {
meta: { meta: {
description: description:
'Dive into the marvels of modern innovation, where the only constant is change. A journey where pixels and data converge to craft the future.', 'Dive into the marvels of modern innovation, where the only constant is change. A journey where pixels and data converge to craft the future.',
// @ts-ignore
image: '{{IMAGE_1}}', image: '{{IMAGE_1}}',
title: 'Digital Horizons: A Glimpse into Tomorrow', title: 'Digital Horizons: A Glimpse into Tomorrow',
}, },

View File

@@ -3,6 +3,7 @@ import type { Post } from '../../payload-types'
export const post2: Partial<Post> = { export const post2: Partial<Post> = {
slug: 'global-gaze', slug: 'global-gaze',
_status: 'published', _status: 'published',
// @ts-ignore
authors: ['{{AUTHOR}}'], authors: ['{{AUTHOR}}'],
content: { content: {
root: { root: {
@@ -217,6 +218,7 @@ export const post2: Partial<Post> = {
meta: { meta: {
description: description:
'Explore the untold and overlooked. A magnified view into the corners of the world, where every story deserves its spotlight.', 'Explore the untold and overlooked. A magnified view into the corners of the world, where every story deserves its spotlight.',
// @ts-ignore
image: '{{IMAGE_1}}', image: '{{IMAGE_1}}',
title: 'Global Gaze: Beyond the Headlines', title: 'Global Gaze: Beyond the Headlines',
}, },

View File

@@ -3,6 +3,7 @@ import type { Post } from '../../payload-types'
export const post3: Partial<Post> = { export const post3: Partial<Post> = {
slug: 'dollar-and-sense-the-financial-forecast', slug: 'dollar-and-sense-the-financial-forecast',
_status: 'published', _status: 'published',
// @ts-ignore
authors: ['{{AUTHOR}}'], authors: ['{{AUTHOR}}'],
content: { content: {
root: { root: {
@@ -253,6 +254,7 @@ export const post3: Partial<Post> = {
}, },
meta: { meta: {
description: `Money isn't just currency; it's a language. Dive deep into its nuances, where strategy meets intuition in the vast sea of finance.`, description: `Money isn't just currency; it's a language. Dive deep into its nuances, where strategy meets intuition in the vast sea of finance.`,
// @ts-ignore
image: '{{IMAGE_1}}', image: '{{IMAGE_1}}',
title: 'Dollar and Sense: The Financial Forecast', title: 'Dollar and Sense: The Financial Forecast',
}, },

View File

@@ -2,8 +2,12 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"esModuleInterop": true, "esModuleInterop": true,
"target": "es5", "target": "es6",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": false, "strict": false,
@@ -22,15 +26,25 @@
} }
], ],
"paths": { "paths": {
"@payload-config": ["./src/payload.config.ts"], "@payload-config": [
"react": ["./node_modules/@types/react"], "./src/payload.config.ts"
"@/*": ["./src/app/*"] ],
"react": [
"./node_modules/@types/react"
],
"@/*": [
"./src/app/*"
]
} }
}, },
"include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "redirects.js", "next.config.js"], "include": [
"exclude": ["node_modules"], "**/*.ts",
"ts-node": { "**/*.tsx",
"transpileOnly": true, ".next/types/**/*.ts",
"swc": true "redirects.js",
} "next.config.js"
],
"exclude": [
"node_modules"
]
} }

View File

@@ -22,8 +22,8 @@
"graphql": "^16.8.1", "graphql": "^16.8.1",
"next": "15.0.0-canary.53", "next": "15.0.0-canary.53",
"payload": "beta", "payload": "beta",
"react": "^19.0.0-rc-6230622a1a-20240610", "react": "19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0-rc-6230622a1a-20240610", "react-dom": "19.0.0-rc-6230622a1a-20240610",
"sharp": "0.32.6" "sharp": "0.32.6"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -23,8 +23,8 @@
"graphql": "^16.8.1", "graphql": "^16.8.1",
"next": "15.0.0-canary.53", "next": "15.0.0-canary.53",
"payload": "beta", "payload": "beta",
"react": "^19.0.0-rc-6230622a1a-20240610", "react": "19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0-rc-6230622a1a-20240610" "react-dom": "19.0.0-rc-6230622a1a-20240610"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.14.9", "@types/node": "^20.14.9",

View File

@@ -23,8 +23,8 @@
"graphql": "^16.8.1", "graphql": "^16.8.1",
"next": "15.0.0-canary.53", "next": "15.0.0-canary.53",
"payload": "beta", "payload": "beta",
"react": "^19.0.0-rc-6230622a1a-20240610", "react": "19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0-rc-6230622a1a-20240610" "react-dom": "19.0.0-rc-6230622a1a-20240610"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.14.9", "@types/node": "^20.14.9",