feat(plugin-seo): export fields from plugin seo so that they can be imported freely in a collection fields config (#6996)
Exports the fields from the SEO plugin so that they can be used anywhere
inside a collection, new exports:
```ts
import { MetaDescriptionField, MetaImageField, MetaTitleField, OverviewField, PreviewField } from '@payloadcms/plugin-seo/fields'
// Used as fields
MetaImageField({
relationTo: 'media',
hasGenerateFn: true,
})
MetaDescriptionField({
hasGenerateFn: true,
})
MetaTitleField({
hasGenerateFn: true,
})
PreviewField({
hasGenerateFn: true,
titlePath: 'meta.title',
descriptionPath: 'meta.description',
})
OverviewField({
titlePath: 'meta.title',
descriptionPath: 'meta.description',
imagePath: 'meta.image',
})
```
This commit is contained in:
@@ -68,7 +68,7 @@ const config = buildConfig({
|
|||||||
'pages',
|
'pages',
|
||||||
],
|
],
|
||||||
uploadsCollection: 'media',
|
uploadsCollection: 'media',
|
||||||
generateTitle: ({ doc }) => `Website.com — ${doc.title.value}`,
|
generateTitle: ({ doc }) => `Website.com — ${doc.title}`,
|
||||||
generateDescription: ({ doc }) => doc.excerpt
|
generateDescription: ({ doc }) => doc.excerpt
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
@@ -119,7 +119,7 @@ A function that allows you to return any meta title, including from document's c
|
|||||||
{
|
{
|
||||||
// ...
|
// ...
|
||||||
seoPlugin({
|
seoPlugin({
|
||||||
generateTitle: ({ ...docInfo, doc, locale }) => `Website.com — ${doc?.title?.value}`,
|
generateTitle: ({ ...docInfo, doc, locale }) => `Website.com — ${doc?.title}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -133,7 +133,7 @@ A function that allows you to return any meta description, including from docume
|
|||||||
{
|
{
|
||||||
// ...
|
// ...
|
||||||
seoPlugin({
|
seoPlugin({
|
||||||
generateDescription: ({ ...docInfo, doc, locale }) => doc?.excerpt?.value,
|
generateDescription: ({ ...docInfo, doc, locale }) => doc?.excerpt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -147,7 +147,7 @@ A function that allows you to return any meta image, including from document's c
|
|||||||
{
|
{
|
||||||
// ...
|
// ...
|
||||||
seoPlugin({
|
seoPlugin({
|
||||||
generateImage: ({ ...docInfo, doc, locale }) => doc?.featuredImage?.value,
|
generateImage: ({ ...docInfo, doc, locale }) => doc?.featuredImage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -162,7 +162,7 @@ A function called by the search preview component to display the actual URL of y
|
|||||||
// ...
|
// ...
|
||||||
seoPlugin({
|
seoPlugin({
|
||||||
generateURL: ({ ...docInfo, doc, locale }) =>
|
generateURL: ({ ...docInfo, doc, locale }) =>
|
||||||
`https://yoursite.com/${collection?.slug}/${doc?.slug?.value}`,
|
`https://yoursite.com/${collection?.slug}/${doc?.slug}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -200,6 +200,54 @@ seoPlugin({
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Direct use of fields
|
||||||
|
|
||||||
|
There is the option to directly import any of the fields from the plugin so that you can include them anywhere as needed.
|
||||||
|
|
||||||
|
<Banner type="info">
|
||||||
|
You will still need to configure the plugin in the Payload config in order to configure the generation functions.
|
||||||
|
Since these fields are imported and used directly, they don't have access to the plugin config so they may need additional arguments to work the same way.
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { MetaDescriptionField, MetaImageField, MetaTitleField, OverviewField, PreviewField } from '@payloadcms/plugin-seo/fields'
|
||||||
|
|
||||||
|
// Used as fields
|
||||||
|
MetaImageField({
|
||||||
|
// the upload collection slug
|
||||||
|
relationTo: 'media',
|
||||||
|
|
||||||
|
// if the `generateImage` function is configured
|
||||||
|
hasGenerateFn: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
MetaDescriptionField({
|
||||||
|
// if the `generateDescription` function is configured
|
||||||
|
hasGenerateFn: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
MetaTitleField({
|
||||||
|
// if the `generateTitle` function is configured
|
||||||
|
hasGenerateFn: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
PreviewField({
|
||||||
|
// if the `generateUrl` function is configured
|
||||||
|
hasGenerateFn: true,
|
||||||
|
|
||||||
|
// field paths to match the target field for data
|
||||||
|
titlePath: 'meta.title',
|
||||||
|
descriptionPath: 'meta.description',
|
||||||
|
})
|
||||||
|
|
||||||
|
OverviewField({
|
||||||
|
// field paths to match the target field for data
|
||||||
|
titlePath: 'meta.title',
|
||||||
|
descriptionPath: 'meta.description',
|
||||||
|
imagePath: 'meta.image',
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
## TypeScript
|
## TypeScript
|
||||||
|
|
||||||
All types can be directly imported:
|
All types can be directly imported:
|
||||||
@@ -213,6 +261,18 @@ import {
|
|||||||
} from '@payloadcms/plugin-seo/types';
|
} from '@payloadcms/plugin-seo/types';
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can then pass the collections from your generated Payload types into the generation types, for example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Page } from './payload-types.ts';
|
||||||
|
|
||||||
|
import { GenerateTitle } from '@payloadcms/plugin-seo/types';
|
||||||
|
|
||||||
|
const generateTitle: GenerateTitle<Page> = async ({ doc, locale }) => {
|
||||||
|
return `Website.com — ${doc?.title}`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) contains an official [Website Template](https://github.com/payloadcms/payload/tree/main/templates/website) and [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommere) which demonstrates exactly how to configure this plugin in Payload and implement it on your front-end.
|
The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) contains an official [Website Template](https://github.com/payloadcms/payload/tree/main/templates/website) and [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommere) which demonstrates exactly how to configure this plugin in Payload and implement it on your front-end.
|
||||||
|
|||||||
@@ -29,6 +29,11 @@
|
|||||||
"import": "./src/exports/types.ts",
|
"import": "./src/exports/types.ts",
|
||||||
"types": "./src/exports/types.ts",
|
"types": "./src/exports/types.ts",
|
||||||
"default": "./src/exports/types.ts"
|
"default": "./src/exports/types.ts"
|
||||||
|
},
|
||||||
|
"./fields": {
|
||||||
|
"import": "./src/exports/fields.ts",
|
||||||
|
"types": "./src/exports/fields.ts",
|
||||||
|
"default": "./src/exports/fields.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"main": "./src/index.tsx",
|
"main": "./src/index.tsx",
|
||||||
@@ -73,6 +78,11 @@
|
|||||||
"import": "./dist/exports/types.js",
|
"import": "./dist/exports/types.js",
|
||||||
"types": "./dist/exports/types.d.ts",
|
"types": "./dist/exports/types.d.ts",
|
||||||
"default": "./dist/exports/types.js"
|
"default": "./dist/exports/types.js"
|
||||||
|
},
|
||||||
|
"./fields": {
|
||||||
|
"import": "./dist/exports/fields.js",
|
||||||
|
"types": "./dist/exports/fields.d.ts",
|
||||||
|
"default": "./dist/exports/fields.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
5
packages/plugin-seo/src/exports/fields.ts
Normal file
5
packages/plugin-seo/src/exports/fields.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export { MetaDescriptionField } from '../fields/MetaDescription/index.js'
|
||||||
|
export { MetaImageField } from '../fields/MetaImage/index.js'
|
||||||
|
export { MetaTitleField } from '../fields/MetaTitle/index.js'
|
||||||
|
export { OverviewField } from '../fields/Overview/index.js'
|
||||||
|
export { PreviewField } from '../fields/Preview/index.js'
|
||||||
@@ -14,11 +14,11 @@ import {
|
|||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
|
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../../translations/index.js'
|
||||||
import type { GenerateDescription } from '../types.js'
|
import type { GenerateDescription } from '../../types.js'
|
||||||
|
|
||||||
import { defaults } from '../defaults.js'
|
import { defaults } from '../../defaults.js'
|
||||||
import { LengthIndicator } from '../ui/LengthIndicator.js'
|
import { LengthIndicator } from '../../ui/LengthIndicator.js'
|
||||||
|
|
||||||
const { maxLength, minLength } = defaults.description
|
const { maxLength, minLength } = defaults.description
|
||||||
|
|
||||||
@@ -28,8 +28,8 @@ type MetaDescriptionProps = FormFieldBase & {
|
|||||||
path: string
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
|
export const MetaDescriptionComponent: React.FC<MetaDescriptionProps> = (props) => {
|
||||||
const { CustomLabel, hasGenerateDescriptionFn, label, labelProps, path, required } = props
|
const { CustomLabel, hasGenerateDescriptionFn, label, labelProps, required } = props
|
||||||
const { path: pathFromContext } = useFieldProps()
|
const { path: pathFromContext } = useFieldProps()
|
||||||
|
|
||||||
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
||||||
@@ -39,7 +39,7 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
|
|||||||
const docInfo = useDocumentInfo()
|
const docInfo = useDocumentInfo()
|
||||||
|
|
||||||
const field: FieldType<string> = useField({
|
const field: FieldType<string> = useField({
|
||||||
path,
|
path: pathFromContext,
|
||||||
} as Options)
|
} as Options)
|
||||||
|
|
||||||
const { errorMessage, setValue, showError, value } = field
|
const { errorMessage, setValue, showError, value } = field
|
||||||
35
packages/plugin-seo/src/fields/MetaDescription/index.ts
Normal file
35
packages/plugin-seo/src/fields/MetaDescription/index.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { TextareaField } from 'payload'
|
||||||
|
|
||||||
|
import { withMergedProps } from '@payloadcms/ui/shared'
|
||||||
|
|
||||||
|
import { MetaDescriptionComponent } from './MetaDescriptionComponent.js'
|
||||||
|
|
||||||
|
interface FieldFunctionProps {
|
||||||
|
/**
|
||||||
|
* Tell the component if the generate function is available as configured in the plugin config
|
||||||
|
*/
|
||||||
|
hasGenerateFn?: boolean
|
||||||
|
overrides?: Partial<TextareaField>
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldFunction = ({ hasGenerateFn, overrides }: FieldFunctionProps) => TextareaField
|
||||||
|
|
||||||
|
export const MetaDescriptionField: FieldFunction = ({ hasGenerateFn = false, overrides }) => {
|
||||||
|
return {
|
||||||
|
name: 'description',
|
||||||
|
type: 'textarea',
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
Field: withMergedProps({
|
||||||
|
Component: MetaDescriptionComponent,
|
||||||
|
sanitizeServerOnlyProps: true,
|
||||||
|
toMergeIntoProps: {
|
||||||
|
hasGenerateDescriptionFn: hasGenerateFn,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
localized: true,
|
||||||
|
...((overrides as unknown as TextareaField) ?? {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,26 +8,28 @@ import {
|
|||||||
useConfig,
|
useConfig,
|
||||||
useDocumentInfo,
|
useDocumentInfo,
|
||||||
useField,
|
useField,
|
||||||
|
useFieldProps,
|
||||||
useForm,
|
useForm,
|
||||||
useLocale,
|
useLocale,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
|
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../../translations/index.js'
|
||||||
import type { GenerateImage } from '../types.js'
|
import type { GenerateImage } from '../../types.js'
|
||||||
|
|
||||||
import { Pill } from '../ui/Pill.js'
|
import { Pill } from '../../ui/Pill.js'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||||
type MetaImageProps = UploadInputProps & {
|
type MetaImageProps = UploadInputProps & {
|
||||||
hasGenerateImageFn: boolean
|
hasGenerateImageFn: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MetaImage: React.FC<MetaImageProps> = (props) => {
|
export const MetaImageComponent: React.FC<MetaImageProps> = (props) => {
|
||||||
const { CustomLabel, hasGenerateImageFn, label, labelProps, relationTo, required } = props || {}
|
const { CustomLabel, hasGenerateImageFn, label, labelProps, relationTo, required } = props || {}
|
||||||
|
const { path: pathFromContext } = useFieldProps()
|
||||||
|
|
||||||
const field: FieldType<string> = useField(props as Options)
|
const field: FieldType<string> = useField({ ...props, path: pathFromContext } as Options)
|
||||||
|
|
||||||
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
||||||
|
|
||||||
39
packages/plugin-seo/src/fields/MetaImage/index.ts
Normal file
39
packages/plugin-seo/src/fields/MetaImage/index.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { UploadField } from 'payload'
|
||||||
|
|
||||||
|
import { withMergedProps } from '@payloadcms/ui/shared'
|
||||||
|
|
||||||
|
import { MetaImageComponent } from './MetaImageComponent.js'
|
||||||
|
|
||||||
|
interface FieldFunctionProps {
|
||||||
|
/**
|
||||||
|
* Tell the component if the generate function is available as configured in the plugin config
|
||||||
|
*/
|
||||||
|
hasGenerateFn?: boolean
|
||||||
|
overrides?: Partial<UploadField>
|
||||||
|
relationTo: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldFunction = ({ hasGenerateFn, overrides }: FieldFunctionProps) => UploadField
|
||||||
|
|
||||||
|
export const MetaImageField: FieldFunction = ({ hasGenerateFn = false, overrides, relationTo }) => {
|
||||||
|
return {
|
||||||
|
name: 'image',
|
||||||
|
type: 'upload',
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
Field: withMergedProps({
|
||||||
|
Component: MetaImageComponent,
|
||||||
|
sanitizeServerOnlyProps: true,
|
||||||
|
toMergeIntoProps: {
|
||||||
|
hasGenerateImageFn: hasGenerateFn,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
description: 'Maximum upload file size: 12MB. Recommended file size for images is <500KB.',
|
||||||
|
},
|
||||||
|
label: 'Meta Image',
|
||||||
|
localized: true,
|
||||||
|
relationTo,
|
||||||
|
...((overrides as unknown as UploadField) ?? {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,12 +14,12 @@ import {
|
|||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
|
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../../translations/index.js'
|
||||||
import type { GenerateTitle } from '../types.js'
|
import type { GenerateTitle } from '../../types.js'
|
||||||
|
|
||||||
import { defaults } from '../defaults.js'
|
import { defaults } from '../../defaults.js'
|
||||||
import { LengthIndicator } from '../ui/LengthIndicator.js'
|
import { LengthIndicator } from '../../ui/LengthIndicator.js'
|
||||||
import './index.scss'
|
import '../index.scss'
|
||||||
|
|
||||||
const { maxLength, minLength } = defaults.title
|
const { maxLength, minLength } = defaults.title
|
||||||
|
|
||||||
@@ -28,14 +28,14 @@ type MetaTitleProps = FormFieldBase & {
|
|||||||
hasGenerateTitleFn: boolean
|
hasGenerateTitleFn: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
|
export const MetaTitleComponent: React.FC<MetaTitleProps> = (props) => {
|
||||||
const { CustomLabel, hasGenerateTitleFn, label, labelProps, path, required } = props || {}
|
const { CustomLabel, hasGenerateTitleFn, label, labelProps, required } = props || {}
|
||||||
const { path: pathFromContext } = useFieldProps()
|
const { path: pathFromContext } = useFieldProps()
|
||||||
|
|
||||||
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
||||||
|
|
||||||
const field: FieldType<string> = useField({
|
const field: FieldType<string> = useField({
|
||||||
path,
|
path: pathFromContext,
|
||||||
} as Options)
|
} as Options)
|
||||||
|
|
||||||
const locale = useLocale()
|
const locale = useLocale()
|
||||||
35
packages/plugin-seo/src/fields/MetaTitle/index.ts
Normal file
35
packages/plugin-seo/src/fields/MetaTitle/index.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { TextField } from 'payload'
|
||||||
|
|
||||||
|
import { withMergedProps } from '@payloadcms/ui/shared'
|
||||||
|
|
||||||
|
import { MetaTitleComponent } from './MetaTitleComponent.js'
|
||||||
|
|
||||||
|
interface FieldFunctionProps {
|
||||||
|
/**
|
||||||
|
* Tell the component if the generate function is available as configured in the plugin config
|
||||||
|
*/
|
||||||
|
hasGenerateFn?: boolean
|
||||||
|
overrides?: Partial<TextField>
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldFunction = ({ hasGenerateFn, overrides }: FieldFunctionProps) => TextField
|
||||||
|
|
||||||
|
export const MetaTitleField: FieldFunction = ({ hasGenerateFn = false, overrides }) => {
|
||||||
|
return {
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
Field: withMergedProps({
|
||||||
|
Component: MetaTitleComponent,
|
||||||
|
sanitizeServerOnlyProps: true,
|
||||||
|
toMergeIntoProps: {
|
||||||
|
hasGenerateTitleFn: hasGenerateFn,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
localized: true,
|
||||||
|
...((overrides as unknown as TextField) ?? {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +1,44 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { FormField } from 'payload'
|
import type { FormField, UIField } from 'payload'
|
||||||
|
|
||||||
import { useAllFormFields, useForm, useTranslation } from '@payloadcms/ui'
|
import { useAllFormFields, useForm, useTranslation } from '@payloadcms/ui'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
|
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../../translations/index.js'
|
||||||
|
|
||||||
import { defaults } from '../defaults.js'
|
import { defaults } from '../../defaults.js'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
description: { maxLength: maxDesc, minLength: minDesc },
|
description: { maxLength: maxDesc, minLength: minDesc },
|
||||||
title: { maxLength: maxTitle, minLength: minTitle },
|
title: { maxLength: maxTitle, minLength: minTitle },
|
||||||
} = defaults
|
} = defaults
|
||||||
|
|
||||||
export const Overview: React.FC = () => {
|
type OverviewProps = UIField & {
|
||||||
|
descriptionPath?: string
|
||||||
|
imagePath?: string
|
||||||
|
titlePath?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OverviewComponent: React.FC<OverviewProps> = ({
|
||||||
|
descriptionPath: descriptionPathFromContext,
|
||||||
|
imagePath: imagePathFromContext,
|
||||||
|
titlePath: titlePathFromContext,
|
||||||
|
}) => {
|
||||||
const {
|
const {
|
||||||
// dispatchFields,
|
// dispatchFields,
|
||||||
getFields,
|
getFields,
|
||||||
} = useForm()
|
} = useForm()
|
||||||
|
|
||||||
|
const descriptionPath = descriptionPathFromContext || 'meta.description'
|
||||||
|
const titlePath = titlePathFromContext || 'meta.title'
|
||||||
|
const imagePath = imagePathFromContext || 'meta.image'
|
||||||
|
|
||||||
const [
|
const [
|
||||||
{
|
{
|
||||||
'meta.description': { value: metaDesc } = {} as FormField,
|
[descriptionPath]: { value: metaDesc } = {} as FormField,
|
||||||
'meta.image': { value: metaImage } = {} as FormField,
|
[imagePath]: { value: metaImage } = {} as FormField,
|
||||||
'meta.title': { value: metaTitle } = {} as FormField,
|
[titlePath]: { value: metaTitle } = {} as FormField,
|
||||||
},
|
},
|
||||||
] = useAllFormFields()
|
] = useAllFormFields()
|
||||||
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
||||||
56
packages/plugin-seo/src/fields/Overview/index.tsx
Normal file
56
packages/plugin-seo/src/fields/Overview/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import type { UIField } from 'payload'
|
||||||
|
|
||||||
|
import { withMergedProps } from '@payloadcms/ui/shared'
|
||||||
|
|
||||||
|
import { OverviewComponent } from './OverviewComponent.js'
|
||||||
|
|
||||||
|
interface FieldFunctionProps {
|
||||||
|
/**
|
||||||
|
* Path to the description field to use for the preview
|
||||||
|
*
|
||||||
|
* @default 'meta.description'
|
||||||
|
*/
|
||||||
|
descriptionPath?: string
|
||||||
|
/**
|
||||||
|
* Path to the image field to use for the preview
|
||||||
|
*
|
||||||
|
* @default 'meta.image'
|
||||||
|
*/
|
||||||
|
imagePath?: string
|
||||||
|
overrides?: Partial<UIField>
|
||||||
|
/**
|
||||||
|
* Path to the title field to use for the preview
|
||||||
|
*
|
||||||
|
* @default 'meta.title'
|
||||||
|
*/
|
||||||
|
titlePath?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldFunction = ({ overrides }: FieldFunctionProps) => UIField
|
||||||
|
|
||||||
|
export const OverviewField: FieldFunction = ({
|
||||||
|
descriptionPath,
|
||||||
|
imagePath,
|
||||||
|
overrides,
|
||||||
|
titlePath,
|
||||||
|
}) => {
|
||||||
|
return {
|
||||||
|
name: 'overview',
|
||||||
|
type: 'ui',
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
Field: withMergedProps({
|
||||||
|
Component: OverviewComponent,
|
||||||
|
sanitizeServerOnlyProps: true,
|
||||||
|
toMergeIntoProps: {
|
||||||
|
descriptionPath,
|
||||||
|
imagePath,
|
||||||
|
titlePath,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label: 'Overview',
|
||||||
|
...((overrides as unknown as UIField) ?? {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,16 +9,23 @@ import {
|
|||||||
useLocale,
|
useLocale,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
|
import { get } from 'http'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
|
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../../translations/index.js'
|
||||||
import type { GenerateURL } from '../types.js'
|
import type { GenerateURL } from '../../types.js'
|
||||||
|
|
||||||
type PreviewProps = UIField & {
|
type PreviewProps = UIField & {
|
||||||
|
descriptionPath?: string
|
||||||
hasGenerateURLFn: boolean
|
hasGenerateURLFn: boolean
|
||||||
|
titlePath?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Preview: React.FC<PreviewProps> = ({ hasGenerateURLFn }) => {
|
export const PreviewComponent: React.FC<PreviewProps> = ({
|
||||||
|
descriptionPath: descriptionPathFromContext,
|
||||||
|
hasGenerateURLFn,
|
||||||
|
titlePath: titlePathFromContext,
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
||||||
|
|
||||||
const locale = useLocale()
|
const locale = useLocale()
|
||||||
@@ -26,9 +33,12 @@ export const Preview: React.FC<PreviewProps> = ({ hasGenerateURLFn }) => {
|
|||||||
const { getData } = useForm()
|
const { getData } = useForm()
|
||||||
const docInfo = useDocumentInfo()
|
const docInfo = useDocumentInfo()
|
||||||
|
|
||||||
|
const descriptionPath = descriptionPathFromContext || 'meta.description'
|
||||||
|
const titlePath = titlePathFromContext || 'meta.title'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
'meta.description': { value: metaDescription } = {} as FormField,
|
[descriptionPath]: { value: metaDescription } = {} as FormField,
|
||||||
'meta.title': { value: metaTitle } = {} as FormField,
|
[titlePath]: { value: metaTitle } = {} as FormField,
|
||||||
} = fields
|
} = fields
|
||||||
|
|
||||||
const [href, setHref] = useState<string>()
|
const [href, setHref] = useState<string>()
|
||||||
54
packages/plugin-seo/src/fields/Preview/index.tsx
Normal file
54
packages/plugin-seo/src/fields/Preview/index.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { UIField } from 'payload'
|
||||||
|
|
||||||
|
import { withMergedProps } from '@payloadcms/ui/shared'
|
||||||
|
|
||||||
|
import { PreviewComponent } from './PreviewComponent.js'
|
||||||
|
|
||||||
|
interface FieldFunctionProps {
|
||||||
|
/**
|
||||||
|
* Path to the description field to use for the preview
|
||||||
|
*
|
||||||
|
* @default 'meta.description'
|
||||||
|
*/
|
||||||
|
descriptionPath?: string
|
||||||
|
/**
|
||||||
|
* Tell the component if the generate function is available as configured in the plugin config
|
||||||
|
*/
|
||||||
|
hasGenerateFn?: boolean
|
||||||
|
overrides?: Partial<UIField>
|
||||||
|
/**
|
||||||
|
* Path to the title field to use for the preview
|
||||||
|
*
|
||||||
|
* @default 'meta.title'
|
||||||
|
*/
|
||||||
|
titlePath?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldFunction = ({ hasGenerateFn, overrides }: FieldFunctionProps) => UIField
|
||||||
|
|
||||||
|
export const PreviewField: FieldFunction = ({
|
||||||
|
descriptionPath,
|
||||||
|
hasGenerateFn = false,
|
||||||
|
overrides,
|
||||||
|
titlePath,
|
||||||
|
}) => {
|
||||||
|
return {
|
||||||
|
name: 'preview',
|
||||||
|
type: 'ui',
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
Field: withMergedProps({
|
||||||
|
Component: PreviewComponent,
|
||||||
|
sanitizeServerOnlyProps: true,
|
||||||
|
toMergeIntoProps: {
|
||||||
|
descriptionPath,
|
||||||
|
hasGenerateURLFn: hasGenerateFn,
|
||||||
|
titlePath,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label: 'Preview',
|
||||||
|
...((overrides as unknown as UIField) ?? {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,12 +12,12 @@ import type {
|
|||||||
SEOPluginConfig,
|
SEOPluginConfig,
|
||||||
} from './types.js'
|
} from './types.js'
|
||||||
|
|
||||||
import { MetaDescription } from './fields/MetaDescription.js'
|
import { MetaDescriptionComponent } from './fields/MetaDescription/MetaDescriptionComponent.js'
|
||||||
import { MetaImage } from './fields/MetaImage.js'
|
import { MetaImageComponent } from './fields/MetaImage/MetaImageComponent.js'
|
||||||
import { MetaTitle } from './fields/MetaTitle.js'
|
import { MetaTitleComponent } from './fields/MetaTitle/MetaTitleComponent.js'
|
||||||
|
import { OverviewComponent } from './fields/Overview/OverviewComponent.js'
|
||||||
|
import { PreviewComponent } from './fields/Preview/PreviewComponent.js'
|
||||||
import { translations } from './translations/index.js'
|
import { translations } from './translations/index.js'
|
||||||
import { Overview } from './ui/Overview.js'
|
|
||||||
import { Preview } from './ui/Preview.js'
|
|
||||||
|
|
||||||
export const seoPlugin =
|
export const seoPlugin =
|
||||||
(pluginConfig: SEOPluginConfig) =>
|
(pluginConfig: SEOPluginConfig) =>
|
||||||
@@ -32,7 +32,7 @@ export const seoPlugin =
|
|||||||
type: 'ui',
|
type: 'ui',
|
||||||
admin: {
|
admin: {
|
||||||
components: {
|
components: {
|
||||||
Field: Overview,
|
Field: OverviewComponent,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
label: 'Overview',
|
label: 'Overview',
|
||||||
@@ -43,7 +43,7 @@ export const seoPlugin =
|
|||||||
admin: {
|
admin: {
|
||||||
components: {
|
components: {
|
||||||
Field: withMergedProps({
|
Field: withMergedProps({
|
||||||
Component: MetaTitle,
|
Component: MetaTitleComponent,
|
||||||
sanitizeServerOnlyProps: true,
|
sanitizeServerOnlyProps: true,
|
||||||
toMergeIntoProps: {
|
toMergeIntoProps: {
|
||||||
hasGenerateTitleFn: typeof pluginConfig?.generateTitle === 'function',
|
hasGenerateTitleFn: typeof pluginConfig?.generateTitle === 'function',
|
||||||
@@ -60,7 +60,7 @@ export const seoPlugin =
|
|||||||
admin: {
|
admin: {
|
||||||
components: {
|
components: {
|
||||||
Field: withMergedProps({
|
Field: withMergedProps({
|
||||||
Component: MetaDescription,
|
Component: MetaDescriptionComponent,
|
||||||
sanitizeServerOnlyProps: true,
|
sanitizeServerOnlyProps: true,
|
||||||
toMergeIntoProps: {
|
toMergeIntoProps: {
|
||||||
hasGenerateDescriptionFn:
|
hasGenerateDescriptionFn:
|
||||||
@@ -81,7 +81,7 @@ export const seoPlugin =
|
|||||||
admin: {
|
admin: {
|
||||||
components: {
|
components: {
|
||||||
Field: withMergedProps({
|
Field: withMergedProps({
|
||||||
Component: MetaImage,
|
Component: MetaImageComponent,
|
||||||
sanitizeServerOnlyProps: true,
|
sanitizeServerOnlyProps: true,
|
||||||
toMergeIntoProps: {
|
toMergeIntoProps: {
|
||||||
hasGenerateImageFn: typeof pluginConfig?.generateImage === 'function',
|
hasGenerateImageFn: typeof pluginConfig?.generateImage === 'function',
|
||||||
@@ -105,7 +105,7 @@ export const seoPlugin =
|
|||||||
admin: {
|
admin: {
|
||||||
components: {
|
components: {
|
||||||
Field: withMergedProps({
|
Field: withMergedProps({
|
||||||
Component: Preview,
|
Component: PreviewComponent,
|
||||||
sanitizeServerOnlyProps: true,
|
sanitizeServerOnlyProps: true,
|
||||||
toMergeIntoProps: {
|
toMergeIntoProps: {
|
||||||
hasGenerateURLFn: typeof pluginConfig?.generateURL === 'function',
|
hasGenerateURLFn: typeof pluginConfig?.generateURL === 'function',
|
||||||
|
|||||||
98
test/plugin-seo/collections/PagesWithImportedFields.ts
Normal file
98
test/plugin-seo/collections/PagesWithImportedFields.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
import {
|
||||||
|
MetaDescriptionField,
|
||||||
|
MetaImageField,
|
||||||
|
MetaTitleField,
|
||||||
|
OverviewField,
|
||||||
|
PreviewField,
|
||||||
|
} from '@payloadcms/plugin-seo/fields'
|
||||||
|
|
||||||
|
import { pagesWithImportedFieldsSlug } from '../shared.js'
|
||||||
|
|
||||||
|
export const PagesWithImportedFields: CollectionConfig = {
|
||||||
|
slug: pagesWithImportedFieldsSlug,
|
||||||
|
labels: {
|
||||||
|
singular: 'Page with imported fields',
|
||||||
|
plural: 'Pages with imported fields',
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'title',
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
drafts: true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
label: 'Title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
OverviewField({
|
||||||
|
titlePath: 'metaAndSEO.title',
|
||||||
|
descriptionPath: 'metaAndSEO.innerMeta.description',
|
||||||
|
imagePath: 'metaAndSEO.innerMedia.image',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
type: 'tabs',
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
label: 'General',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'excerpt',
|
||||||
|
label: 'Excerpt',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'slug',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
// NOTE: in order for position: 'sidebar' to work here,
|
||||||
|
// the first field of this config must be of type `tabs`,
|
||||||
|
// and this field must be a sibling of it
|
||||||
|
// See `./Posts` or the `../../README.md` for more info
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Meta',
|
||||||
|
name: 'metaAndSEO',
|
||||||
|
fields: [
|
||||||
|
MetaTitleField({
|
||||||
|
hasGenerateFn: true,
|
||||||
|
}),
|
||||||
|
PreviewField({
|
||||||
|
hasGenerateFn: true,
|
||||||
|
titlePath: 'metaAndSEO.title',
|
||||||
|
descriptionPath: 'metaAndSEO.innerMeta.description',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
type: 'group',
|
||||||
|
name: 'innerMeta',
|
||||||
|
fields: [
|
||||||
|
MetaDescriptionField({
|
||||||
|
hasGenerateFn: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'group',
|
||||||
|
name: 'innerMedia',
|
||||||
|
fields: [
|
||||||
|
MetaImageField({
|
||||||
|
relationTo: 'media',
|
||||||
|
hasGenerateFn: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
|||||||
import { devUser } from '../credentials.js'
|
import { devUser } from '../credentials.js'
|
||||||
import { Media } from './collections/Media.js'
|
import { Media } from './collections/Media.js'
|
||||||
import { Pages } from './collections/Pages.js'
|
import { Pages } from './collections/Pages.js'
|
||||||
|
import { PagesWithImportedFields } from './collections/PagesWithImportedFields.js'
|
||||||
import { Users } from './collections/Users.js'
|
import { Users } from './collections/Users.js'
|
||||||
import { seed } from './seed/index.js'
|
import { seed } from './seed/index.js'
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ const generateURL: GenerateURL<Page> = ({ doc, locale }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default buildConfigWithDefaults({
|
export default buildConfigWithDefaults({
|
||||||
collections: [Users, Pages, Media],
|
collections: [Users, Pages, Media, PagesWithImportedFields],
|
||||||
i18n: {
|
i18n: {
|
||||||
supportedLanguages: {
|
supportedLanguages: {
|
||||||
en,
|
en,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface Config {
|
|||||||
users: User;
|
users: User;
|
||||||
pages: Page;
|
pages: Page;
|
||||||
media: Media;
|
media: Media;
|
||||||
|
pagesWithImportedFields: PagesWithImportedField;
|
||||||
'payload-preferences': PayloadPreference;
|
'payload-preferences': PayloadPreference;
|
||||||
'payload-migrations': PayloadMigration;
|
'payload-migrations': PayloadMigration;
|
||||||
};
|
};
|
||||||
@@ -90,6 +91,28 @@ export interface Media {
|
|||||||
focalX?: number | null;
|
focalX?: number | null;
|
||||||
focalY?: number | null;
|
focalY?: number | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "pagesWithImportedFields".
|
||||||
|
*/
|
||||||
|
export interface PagesWithImportedField {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
excerpt?: string | null;
|
||||||
|
slug: string;
|
||||||
|
metaAndSEO?: {
|
||||||
|
title?: string | null;
|
||||||
|
innerMeta?: {
|
||||||
|
description?: string | null;
|
||||||
|
};
|
||||||
|
innerMedia?: {
|
||||||
|
image?: string | Media | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
_status?: ('draft' | 'published') | null;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-preferences".
|
* via the `definition` "payload-preferences".
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
export const pagesSlug = 'pages'
|
export const pagesSlug = 'pages'
|
||||||
|
|
||||||
|
export const pagesWithImportedFieldsSlug = 'pagesWithImportedFields'
|
||||||
|
|
||||||
export const mediaSlug = 'media'
|
export const mediaSlug = 'media'
|
||||||
|
|||||||
Reference in New Issue
Block a user