diff --git a/bun.lock b/bun.lock index 0a2bfef..2f22785 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "@payloadcms/db-postgres": "3.59.1", "@payloadcms/next": "3.59.1", "@payloadcms/payload-cloud": "3.59.1", + "@payloadcms/plugin-seo": "^3.59.1", "@payloadcms/richtext-lexical": "3.59.1", "@payloadcms/ui": "3.59.1", "cross-env": "^7.0.3", @@ -471,6 +472,8 @@ "@payloadcms/payload-cloud": ["@payloadcms/payload-cloud@3.59.1", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "^3.614.0", "@aws-sdk/client-s3": "^3.614.0", "@aws-sdk/credential-providers": "^3.614.0", "@aws-sdk/lib-storage": "^3.614.0", "@payloadcms/email-nodemailer": "3.59.1", "amazon-cognito-identity-js": "^6.1.2", "nodemailer": "6.9.16" }, "peerDependencies": { "payload": "3.59.1" } }, "sha512-ZuP7ZPsu+GE4+07a1wrs/EaLQBhoA4Ij0en9YL+qHWAiLCPo8NF2L4cSdWrN+XKKA6G53gqLBaLotCJ8veR6uA=="], + "@payloadcms/plugin-seo": ["@payloadcms/plugin-seo@3.59.1", "", { "dependencies": { "@payloadcms/translations": "3.59.1", "@payloadcms/ui": "3.59.1" }, "peerDependencies": { "payload": "3.59.1", "react": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020", "react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020" } }, "sha512-Dc/tR/Vx+sprsY8lQhf/MdCasCY7B5ScTJfAgaYf00rQqY2Y1L8zFtg0tuwOIgtSXvRh/GbRS3+vZcPldagHIA=="], + "@payloadcms/richtext-lexical": ["@payloadcms/richtext-lexical@3.59.1", "", { "dependencies": { "@lexical/headless": "0.35.0", "@lexical/html": "0.35.0", "@lexical/link": "0.35.0", "@lexical/list": "0.35.0", "@lexical/mark": "0.35.0", "@lexical/react": "0.35.0", "@lexical/rich-text": "0.35.0", "@lexical/selection": "0.35.0", "@lexical/table": "0.35.0", "@lexical/utils": "0.35.0", "@payloadcms/translations": "3.59.1", "@payloadcms/ui": "3.59.1", "@types/uuid": "10.0.0", "acorn": "8.12.1", "bson-objectid": "2.0.4", "csstype": "3.1.3", "dequal": "2.0.3", "escape-html": "1.0.3", "jsox": "1.2.121", "lexical": "0.35.0", "mdast-util-from-markdown": "2.0.2", "mdast-util-mdx-jsx": "3.1.3", "micromark-extension-mdx-jsx": "3.0.1", "qs-esm": "7.0.2", "react-error-boundary": "4.1.2", "ts-essentials": "10.0.3", "uuid": "10.0.0" }, "peerDependencies": { "@faceless-ui/modal": "3.0.0", "@faceless-ui/scroll-info": "2.0.0", "@payloadcms/next": "3.59.1", "payload": "3.59.1", "react": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020", "react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020" } }, "sha512-bVuLAnOxR1kL8IYD5iD269eO47ptX9/dxR2mFbJCYyG5qWFSU9gH3ocgJc/vVcU4SmghFADIxQzrpinPYmtveQ=="], "@payloadcms/translations": ["@payloadcms/translations@3.59.1", "", { "dependencies": { "date-fns": "4.1.0" } }, "sha512-kBuYV4tGOUpVkh6es6cBhbJn14dGtNnYkGtHhScbtGVX6ZJyVudY9ypKQhljAEzdQq0n+kkmi1sfwlaRps+t6w=="], diff --git a/package.json b/package.json index 00d574e..a969966 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@payloadcms/db-postgres": "3.59.1", "@payloadcms/next": "3.59.1", "@payloadcms/payload-cloud": "3.59.1", + "@payloadcms/plugin-seo": "^3.59.1", "@payloadcms/richtext-lexical": "3.59.1", "@payloadcms/ui": "3.59.1", "cross-env": "^7.0.3", diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index 881803e..f1e090e 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -22,6 +22,11 @@ import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93 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 { 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 { SlugField as SlugField_3817bf644402e67bfe6577f60ef982de } from '@payloadcms/ui' export const importMap = { @@ -49,5 +54,10 @@ export const importMap = { "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_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, "@payloadcms/ui#SlugField": SlugField_3817bf644402e67bfe6577f60ef982de } diff --git a/src/collections/Nav/index.ts b/src/collections/Nav/index.ts new file mode 100644 index 0000000..79eb6d6 --- /dev/null +++ b/src/collections/Nav/index.ts @@ -0,0 +1,22 @@ +import { GlobalConfig } from 'payload'; + +export const Nav: GlobalConfig = { + slug: 'nav', + label: 'Navigation', + fields: [ + { + name: 'items', + type: 'array', + required: true, + maxRows: 8, + fields: [ + { + name: 'page', + type: 'relationship', + relationTo: 'pages', + required: true, + } + ] + } + ] +} \ No newline at end of file diff --git a/src/collections/Page.ts b/src/collections/Page.ts index c6bc8ad..2ae9e7d 100644 --- a/src/collections/Page.ts +++ b/src/collections/Page.ts @@ -1,3 +1,4 @@ +import { MetaDescriptionField, MetaImageField, MetaTitleField, OverviewField, PreviewField } from '@payloadcms/plugin-seo/fields' import type { CollectionConfig } from 'payload' import { slugField } from 'payload' @@ -17,9 +18,48 @@ export const Page: CollectionConfig = { required: true, }, { - name: 'richText', - type: 'richText' + type: 'tabs', + tabs: [ + { + label: 'Content', + fields: [ + { + name: 'richText', + type: 'richText' + }, + ] + }, + { + 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', + }), + ], + }, + ] }, + slugField(), + ], } diff --git a/src/collections/Projects.ts b/src/collections/Projects.ts index 81b2f96..ff4233b 100644 --- a/src/collections/Projects.ts +++ b/src/collections/Projects.ts @@ -1,6 +1,5 @@ import type { CollectionConfig } from 'payload' import { slugField } from 'payload' -import { fa } from 'payload/i18n/fa' export const Projects: CollectionConfig = { slug: 'projects', @@ -72,7 +71,7 @@ export const Projects: CollectionConfig = { { name: 'order', type: 'number', - defaultValue: 0, + defaultValue: undefined, admin: { position: 'sidebar' } diff --git a/src/collections/Settings/index.ts b/src/collections/Settings/index.ts new file mode 100644 index 0000000..4589b41 --- /dev/null +++ b/src/collections/Settings/index.ts @@ -0,0 +1,8 @@ +import type { CollectionConfig } from 'payload'; + +export const Settings: CollectionConfig = { + slug: 'settings', + fields: [ + + ] +} \ No newline at end of file diff --git a/src/payload-types.ts b/src/payload-types.ts index 7b1c3d5..876c68b 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -90,8 +90,12 @@ export interface Config { db: { defaultIDType: number; }; - globals: {}; - globalsSelect: {}; + globals: { + nav: Nav; + }; + globalsSelect: { + nav: NavSelect | NavSelect; + }; locale: null; user: User & { collection: 'users'; @@ -141,6 +145,14 @@ export interface Page { }; [k: string]: unknown; } | null; + meta?: { + title?: string | null; + /** + * Maximum upload file size: 12MB. Recommended file size for images is <500KB. + */ + image?: (number | null) | Media; + description?: string | null; + }; /** * When enabled, the slug will auto-generate from the title field on save and autosave. */ @@ -149,6 +161,25 @@ export interface Page { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "media". + */ +export interface Media { + id: number; + alt: string; + updatedAt: string; + createdAt: string; + url?: string | null; + thumbnailURL?: string | null; + filename?: string | null; + mimeType?: string | null; + filesize?: number | null; + width?: number | null; + height?: number | null; + focalX?: number | null; + focalY?: number | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "projects". @@ -226,25 +257,6 @@ export interface User { | null; password?: string | null; } -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "media". - */ -export interface Media { - id: number; - alt: string; - updatedAt: string; - createdAt: string; - url?: string | null; - thumbnailURL?: string | null; - filename?: string | null; - mimeType?: string | null; - filesize?: number | null; - width?: number | null; - height?: number | null; - focalX?: number | null; - focalY?: number | null; -} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-locked-documents". @@ -321,6 +333,13 @@ export interface PayloadMigration { export interface PagesSelect { title?: T; richText?: T; + meta?: + | T + | { + title?: T; + image?: T; + description?: T; + }; generateSlug?: T; slug?: T; updatedAt?: T; @@ -429,6 +448,34 @@ export interface PayloadMigrationsSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "nav". + */ +export interface Nav { + id: number; + items: { + page: number | Page; + id?: string | null; + }[]; + updatedAt?: string | null; + createdAt?: string | null; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "nav_select". + */ +export interface NavSelect { + items?: + | T + | { + page?: T; + id?: T; + }; + updatedAt?: T; + createdAt?: T; + globalType?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "auth". diff --git a/src/payload.config.ts b/src/payload.config.ts index e483fb0..1ac4b15 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -12,6 +12,10 @@ import { Media } from './collections/Media' import { Page } from './collections/Page' import { Projects } from './collections/Projects' import { Categories } from './collections/Categories' +import { Nav } from './collections/Nav' +import { seoPlugin } from '@payloadcms/plugin-seo' +import { GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types' +import { plugins } from './plugins' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) @@ -23,7 +27,16 @@ export default buildConfig({ baseDir: path.resolve(dirname), }, }, - collections: [Page, Projects, Categories, Users, Media], + collections: [ + Page, + Projects, + Categories, + Users, + Media, + ], + globals: [ + Nav, + ], editor: lexicalEditor({ features: ({ defaultFeatures, rootFeatures }) => [ ...defaultFeatures, @@ -62,7 +75,7 @@ export default buildConfig({ }), sharp, plugins: [ - payloadCloudPlugin(), + ...plugins // storage-adapter-placeholder ], }) diff --git a/src/plugins/index.ts b/src/plugins/index.ts new file mode 100644 index 0000000..2b7b253 --- /dev/null +++ b/src/plugins/index.ts @@ -0,0 +1,24 @@ +import { payloadCloudPlugin } from "@payloadcms/payload-cloud"; +import { seoPlugin } from "@payloadcms/plugin-seo"; +import { GenerateTitle, GenerateURL } from "@payloadcms/plugin-seo/types"; +import { Plugin } from "payload"; +import { Page, Project } from "../payload-types"; +import { getServerSideURL } from "@/utilities/getURL"; + +const generateTitle: GenerateTitle = ({ doc }) => { + return doc?.title ? `${doc.title} | Payload Website Template` : 'Payload Website Template' +} + +const generateURL: GenerateURL = ({ doc }) => { + const url = getServerSideURL() + + return doc?.slug ? `${url}/${doc.slug}` : url +} + +export const plugins: Plugin[] = [ + payloadCloudPlugin(), + seoPlugin({ + generateTitle, + generateURL, + }), +] \ No newline at end of file