From 25d368a7db16c9d5fa54a4f9abb9152c04fac971 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 2 Jul 2024 09:53:52 -0400 Subject: [PATCH] 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', }) ``` --- docs/plugins/seo.mdx | 70 ++++++++++++- packages/plugin-seo/package.json | 10 ++ packages/plugin-seo/src/exports/fields.ts | 5 + .../MetaDescriptionComponent.tsx} | 14 +-- .../src/fields/MetaDescription/index.ts | 35 +++++++ .../MetaImageComponent.tsx} | 12 ++- .../plugin-seo/src/fields/MetaImage/index.ts | 39 ++++++++ .../MetaTitleComponent.tsx} | 16 +-- .../plugin-seo/src/fields/MetaTitle/index.ts | 35 +++++++ .../Overview/OverviewComponent.tsx} | 28 ++++-- .../plugin-seo/src/fields/Overview/index.tsx | 56 +++++++++++ .../Preview/PreviewComponent.tsx} | 20 +++- .../plugin-seo/src/fields/Preview/index.tsx | 54 ++++++++++ packages/plugin-seo/src/index.tsx | 20 ++-- .../collections/PagesWithImportedFields.ts | 98 +++++++++++++++++++ test/plugin-seo/config.ts | 3 +- test/plugin-seo/payload-types.ts | 23 +++++ test/plugin-seo/shared.ts | 2 + 18 files changed, 492 insertions(+), 48 deletions(-) create mode 100644 packages/plugin-seo/src/exports/fields.ts rename packages/plugin-seo/src/fields/{MetaDescription.tsx => MetaDescription/MetaDescriptionComponent.tsx} (91%) create mode 100644 packages/plugin-seo/src/fields/MetaDescription/index.ts rename packages/plugin-seo/src/fields/{MetaImage.tsx => MetaImage/MetaImageComponent.tsx} (91%) create mode 100644 packages/plugin-seo/src/fields/MetaImage/index.ts rename packages/plugin-seo/src/fields/{MetaTitle.tsx => MetaTitle/MetaTitleComponent.tsx} (90%) create mode 100644 packages/plugin-seo/src/fields/MetaTitle/index.ts rename packages/plugin-seo/src/{ui/Overview.tsx => fields/Overview/OverviewComponent.tsx} (69%) create mode 100644 packages/plugin-seo/src/fields/Overview/index.tsx rename packages/plugin-seo/src/{ui/Preview.tsx => fields/Preview/PreviewComponent.tsx} (80%) create mode 100644 packages/plugin-seo/src/fields/Preview/index.tsx create mode 100644 test/plugin-seo/collections/PagesWithImportedFields.ts diff --git a/docs/plugins/seo.mdx b/docs/plugins/seo.mdx index 20a377338a..72355528af 100644 --- a/docs/plugins/seo.mdx +++ b/docs/plugins/seo.mdx @@ -68,7 +68,7 @@ const config = buildConfig({ 'pages', ], uploadsCollection: 'media', - generateTitle: ({ doc }) => `Website.com — ${doc.title.value}`, + generateTitle: ({ doc }) => `Website.com — ${doc.title}`, generateDescription: ({ doc }) => doc.excerpt }) ] @@ -119,7 +119,7 @@ A function that allows you to return any meta title, including from document's c { // ... 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({ - 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({ - 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({ 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. + + + 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. + + +```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 All types can be directly imported: @@ -213,6 +261,18 @@ import { } 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 = async ({ doc, locale }) => { + return `Website.com — ${doc?.title}` +} +``` + ## 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. diff --git a/packages/plugin-seo/package.json b/packages/plugin-seo/package.json index a7157daca8..e7b8611877 100644 --- a/packages/plugin-seo/package.json +++ b/packages/plugin-seo/package.json @@ -29,6 +29,11 @@ "import": "./src/exports/types.ts", "types": "./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", @@ -73,6 +78,11 @@ "import": "./dist/exports/types.js", "types": "./dist/exports/types.d.ts", "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", diff --git a/packages/plugin-seo/src/exports/fields.ts b/packages/plugin-seo/src/exports/fields.ts new file mode 100644 index 0000000000..37d469f30a --- /dev/null +++ b/packages/plugin-seo/src/exports/fields.ts @@ -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' diff --git a/packages/plugin-seo/src/fields/MetaDescription.tsx b/packages/plugin-seo/src/fields/MetaDescription/MetaDescriptionComponent.tsx similarity index 91% rename from packages/plugin-seo/src/fields/MetaDescription.tsx rename to packages/plugin-seo/src/fields/MetaDescription/MetaDescriptionComponent.tsx index 49284ea1d2..71cc934cce 100644 --- a/packages/plugin-seo/src/fields/MetaDescription.tsx +++ b/packages/plugin-seo/src/fields/MetaDescription/MetaDescriptionComponent.tsx @@ -14,11 +14,11 @@ import { } from '@payloadcms/ui' import React, { useCallback } from 'react' -import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js' -import type { GenerateDescription } from '../types.js' +import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../../translations/index.js' +import type { GenerateDescription } from '../../types.js' -import { defaults } from '../defaults.js' -import { LengthIndicator } from '../ui/LengthIndicator.js' +import { defaults } from '../../defaults.js' +import { LengthIndicator } from '../../ui/LengthIndicator.js' const { maxLength, minLength } = defaults.description @@ -28,8 +28,8 @@ type MetaDescriptionProps = FormFieldBase & { path: string } -export const MetaDescription: React.FC = (props) => { - const { CustomLabel, hasGenerateDescriptionFn, label, labelProps, path, required } = props +export const MetaDescriptionComponent: React.FC = (props) => { + const { CustomLabel, hasGenerateDescriptionFn, label, labelProps, required } = props const { path: pathFromContext } = useFieldProps() const { t } = useTranslation() @@ -39,7 +39,7 @@ export const MetaDescription: React.FC = (props) => { const docInfo = useDocumentInfo() const field: FieldType = useField({ - path, + path: pathFromContext, } as Options) const { errorMessage, setValue, showError, value } = field diff --git a/packages/plugin-seo/src/fields/MetaDescription/index.ts b/packages/plugin-seo/src/fields/MetaDescription/index.ts new file mode 100644 index 0000000000..983ffd577f --- /dev/null +++ b/packages/plugin-seo/src/fields/MetaDescription/index.ts @@ -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 +} + +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) ?? {}), + } +} diff --git a/packages/plugin-seo/src/fields/MetaImage.tsx b/packages/plugin-seo/src/fields/MetaImage/MetaImageComponent.tsx similarity index 91% rename from packages/plugin-seo/src/fields/MetaImage.tsx rename to packages/plugin-seo/src/fields/MetaImage/MetaImageComponent.tsx index dca98e75b3..b3b11c605d 100644 --- a/packages/plugin-seo/src/fields/MetaImage.tsx +++ b/packages/plugin-seo/src/fields/MetaImage/MetaImageComponent.tsx @@ -8,26 +8,28 @@ import { useConfig, useDocumentInfo, useField, + useFieldProps, useForm, useLocale, useTranslation, } from '@payloadcms/ui' import React, { useCallback } from 'react' -import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js' -import type { GenerateImage } from '../types.js' +import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../../translations/index.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 type MetaImageProps = UploadInputProps & { hasGenerateImageFn: boolean } -export const MetaImage: React.FC = (props) => { +export const MetaImageComponent: React.FC = (props) => { const { CustomLabel, hasGenerateImageFn, label, labelProps, relationTo, required } = props || {} + const { path: pathFromContext } = useFieldProps() - const field: FieldType = useField(props as Options) + const field: FieldType = useField({ ...props, path: pathFromContext } as Options) const { t } = useTranslation() diff --git a/packages/plugin-seo/src/fields/MetaImage/index.ts b/packages/plugin-seo/src/fields/MetaImage/index.ts new file mode 100644 index 0000000000..0333e98e4b --- /dev/null +++ b/packages/plugin-seo/src/fields/MetaImage/index.ts @@ -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 + 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) ?? {}), + } +} diff --git a/packages/plugin-seo/src/fields/MetaTitle.tsx b/packages/plugin-seo/src/fields/MetaTitle/MetaTitleComponent.tsx similarity index 90% rename from packages/plugin-seo/src/fields/MetaTitle.tsx rename to packages/plugin-seo/src/fields/MetaTitle/MetaTitleComponent.tsx index 486927d1b6..3b071ea580 100644 --- a/packages/plugin-seo/src/fields/MetaTitle.tsx +++ b/packages/plugin-seo/src/fields/MetaTitle/MetaTitleComponent.tsx @@ -14,12 +14,12 @@ import { } from '@payloadcms/ui' import React, { useCallback } from 'react' -import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js' -import type { GenerateTitle } from '../types.js' +import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../../translations/index.js' +import type { GenerateTitle } from '../../types.js' -import { defaults } from '../defaults.js' -import { LengthIndicator } from '../ui/LengthIndicator.js' -import './index.scss' +import { defaults } from '../../defaults.js' +import { LengthIndicator } from '../../ui/LengthIndicator.js' +import '../index.scss' const { maxLength, minLength } = defaults.title @@ -28,14 +28,14 @@ type MetaTitleProps = FormFieldBase & { hasGenerateTitleFn: boolean } -export const MetaTitle: React.FC = (props) => { - const { CustomLabel, hasGenerateTitleFn, label, labelProps, path, required } = props || {} +export const MetaTitleComponent: React.FC = (props) => { + const { CustomLabel, hasGenerateTitleFn, label, labelProps, required } = props || {} const { path: pathFromContext } = useFieldProps() const { t } = useTranslation() const field: FieldType = useField({ - path, + path: pathFromContext, } as Options) const locale = useLocale() diff --git a/packages/plugin-seo/src/fields/MetaTitle/index.ts b/packages/plugin-seo/src/fields/MetaTitle/index.ts new file mode 100644 index 0000000000..fe952fc460 --- /dev/null +++ b/packages/plugin-seo/src/fields/MetaTitle/index.ts @@ -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 +} + +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) ?? {}), + } +} diff --git a/packages/plugin-seo/src/ui/Overview.tsx b/packages/plugin-seo/src/fields/Overview/OverviewComponent.tsx similarity index 69% rename from packages/plugin-seo/src/ui/Overview.tsx rename to packages/plugin-seo/src/fields/Overview/OverviewComponent.tsx index e789c59222..9e79cf87f1 100644 --- a/packages/plugin-seo/src/ui/Overview.tsx +++ b/packages/plugin-seo/src/fields/Overview/OverviewComponent.tsx @@ -1,30 +1,44 @@ 'use client' -import type { FormField } from 'payload' +import type { FormField, UIField } from 'payload' import { useAllFormFields, useForm, useTranslation } from '@payloadcms/ui' 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 { description: { maxLength: maxDesc, minLength: minDesc }, title: { maxLength: maxTitle, minLength: minTitle }, } = defaults -export const Overview: React.FC = () => { +type OverviewProps = UIField & { + descriptionPath?: string + imagePath?: string + titlePath?: string +} + +export const OverviewComponent: React.FC = ({ + descriptionPath: descriptionPathFromContext, + imagePath: imagePathFromContext, + titlePath: titlePathFromContext, +}) => { const { // dispatchFields, getFields, } = useForm() + const descriptionPath = descriptionPathFromContext || 'meta.description' + const titlePath = titlePathFromContext || 'meta.title' + const imagePath = imagePathFromContext || 'meta.image' + const [ { - 'meta.description': { value: metaDesc } = {} as FormField, - 'meta.image': { value: metaImage } = {} as FormField, - 'meta.title': { value: metaTitle } = {} as FormField, + [descriptionPath]: { value: metaDesc } = {} as FormField, + [imagePath]: { value: metaImage } = {} as FormField, + [titlePath]: { value: metaTitle } = {} as FormField, }, ] = useAllFormFields() const { t } = useTranslation() diff --git a/packages/plugin-seo/src/fields/Overview/index.tsx b/packages/plugin-seo/src/fields/Overview/index.tsx new file mode 100644 index 0000000000..7c980b1ace --- /dev/null +++ b/packages/plugin-seo/src/fields/Overview/index.tsx @@ -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 + /** + * 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) ?? {}), + } +} diff --git a/packages/plugin-seo/src/ui/Preview.tsx b/packages/plugin-seo/src/fields/Preview/PreviewComponent.tsx similarity index 80% rename from packages/plugin-seo/src/ui/Preview.tsx rename to packages/plugin-seo/src/fields/Preview/PreviewComponent.tsx index c8e7352c7e..3f081be2cf 100644 --- a/packages/plugin-seo/src/ui/Preview.tsx +++ b/packages/plugin-seo/src/fields/Preview/PreviewComponent.tsx @@ -9,16 +9,23 @@ import { useLocale, useTranslation, } from '@payloadcms/ui' +import { get } from 'http' import React, { useEffect, useState } from 'react' -import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js' -import type { GenerateURL } from '../types.js' +import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../../translations/index.js' +import type { GenerateURL } from '../../types.js' type PreviewProps = UIField & { + descriptionPath?: string hasGenerateURLFn: boolean + titlePath?: string } -export const Preview: React.FC = ({ hasGenerateURLFn }) => { +export const PreviewComponent: React.FC = ({ + descriptionPath: descriptionPathFromContext, + hasGenerateURLFn, + titlePath: titlePathFromContext, +}) => { const { t } = useTranslation() const locale = useLocale() @@ -26,9 +33,12 @@ export const Preview: React.FC = ({ hasGenerateURLFn }) => { const { getData } = useForm() const docInfo = useDocumentInfo() + const descriptionPath = descriptionPathFromContext || 'meta.description' + const titlePath = titlePathFromContext || 'meta.title' + const { - 'meta.description': { value: metaDescription } = {} as FormField, - 'meta.title': { value: metaTitle } = {} as FormField, + [descriptionPath]: { value: metaDescription } = {} as FormField, + [titlePath]: { value: metaTitle } = {} as FormField, } = fields const [href, setHref] = useState() diff --git a/packages/plugin-seo/src/fields/Preview/index.tsx b/packages/plugin-seo/src/fields/Preview/index.tsx new file mode 100644 index 0000000000..d609c55b12 --- /dev/null +++ b/packages/plugin-seo/src/fields/Preview/index.tsx @@ -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 + /** + * 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) ?? {}), + } +} diff --git a/packages/plugin-seo/src/index.tsx b/packages/plugin-seo/src/index.tsx index 403fee6087..b030223c6a 100644 --- a/packages/plugin-seo/src/index.tsx +++ b/packages/plugin-seo/src/index.tsx @@ -12,12 +12,12 @@ import type { SEOPluginConfig, } from './types.js' -import { MetaDescription } from './fields/MetaDescription.js' -import { MetaImage } from './fields/MetaImage.js' -import { MetaTitle } from './fields/MetaTitle.js' +import { MetaDescriptionComponent } from './fields/MetaDescription/MetaDescriptionComponent.js' +import { MetaImageComponent } from './fields/MetaImage/MetaImageComponent.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 { Overview } from './ui/Overview.js' -import { Preview } from './ui/Preview.js' export const seoPlugin = (pluginConfig: SEOPluginConfig) => @@ -32,7 +32,7 @@ export const seoPlugin = type: 'ui', admin: { components: { - Field: Overview, + Field: OverviewComponent, }, }, label: 'Overview', @@ -43,7 +43,7 @@ export const seoPlugin = admin: { components: { Field: withMergedProps({ - Component: MetaTitle, + Component: MetaTitleComponent, sanitizeServerOnlyProps: true, toMergeIntoProps: { hasGenerateTitleFn: typeof pluginConfig?.generateTitle === 'function', @@ -60,7 +60,7 @@ export const seoPlugin = admin: { components: { Field: withMergedProps({ - Component: MetaDescription, + Component: MetaDescriptionComponent, sanitizeServerOnlyProps: true, toMergeIntoProps: { hasGenerateDescriptionFn: @@ -81,7 +81,7 @@ export const seoPlugin = admin: { components: { Field: withMergedProps({ - Component: MetaImage, + Component: MetaImageComponent, sanitizeServerOnlyProps: true, toMergeIntoProps: { hasGenerateImageFn: typeof pluginConfig?.generateImage === 'function', @@ -105,7 +105,7 @@ export const seoPlugin = admin: { components: { Field: withMergedProps({ - Component: Preview, + Component: PreviewComponent, sanitizeServerOnlyProps: true, toMergeIntoProps: { hasGenerateURLFn: typeof pluginConfig?.generateURL === 'function', diff --git a/test/plugin-seo/collections/PagesWithImportedFields.ts b/test/plugin-seo/collections/PagesWithImportedFields.ts new file mode 100644 index 0000000000..7474282eec --- /dev/null +++ b/test/plugin-seo/collections/PagesWithImportedFields.ts @@ -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, + }), + ], + }, + ], + }, + ], + }, + ], +} diff --git a/test/plugin-seo/config.ts b/test/plugin-seo/config.ts index 3dd619f727..aa978b497f 100644 --- a/test/plugin-seo/config.ts +++ b/test/plugin-seo/config.ts @@ -13,6 +13,7 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import { devUser } from '../credentials.js' import { Media } from './collections/Media.js' import { Pages } from './collections/Pages.js' +import { PagesWithImportedFields } from './collections/PagesWithImportedFields.js' import { Users } from './collections/Users.js' import { seed } from './seed/index.js' @@ -29,7 +30,7 @@ const generateURL: GenerateURL = ({ doc, locale }) => { } export default buildConfigWithDefaults({ - collections: [Users, Pages, Media], + collections: [Users, Pages, Media, PagesWithImportedFields], i18n: { supportedLanguages: { en, diff --git a/test/plugin-seo/payload-types.ts b/test/plugin-seo/payload-types.ts index 6bdb006e7a..513c1a29c6 100644 --- a/test/plugin-seo/payload-types.ts +++ b/test/plugin-seo/payload-types.ts @@ -11,6 +11,7 @@ export interface Config { users: User; pages: Page; media: Media; + pagesWithImportedFields: PagesWithImportedField; 'payload-preferences': PayloadPreference; 'payload-migrations': PayloadMigration; }; @@ -90,6 +91,28 @@ export interface Media { focalX?: 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 * via the `definition` "payload-preferences". diff --git a/test/plugin-seo/shared.ts b/test/plugin-seo/shared.ts index fc3d1c546b..a7e31591a1 100644 --- a/test/plugin-seo/shared.ts +++ b/test/plugin-seo/shared.ts @@ -1,3 +1,5 @@ export const pagesSlug = 'pages' +export const pagesWithImportedFieldsSlug = 'pagesWithImportedFields' + export const mediaSlug = 'media'