> ## Documentation Index
> Fetch the complete documentation index at: https://docs.frogbot.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# SEO

> Add meta titles, descriptions, images, and a search preview to FrogBot collections.

The SEO plugin adds a `meta` group to the collections you list. The group holds a meta title, description, and image, a search result preview, and a completeness overview. Each field can offer an "Auto-generate" button backed by a function you provide.

## Install

```bash theme={"theme":"tokyo-night"}
pnpm add @frogbotai/plugin-seo
```

## Configure

```ts theme={"theme":"tokyo-night"}
import { seoPlugin } from '@frogbotai/plugin-seo'

plugins: [
  seoPlugin({
    collections: ['posts'],
    uploadsCollection: 'media',
    generateTitle: ({ doc }) => `${doc.title ?? 'Untitled'} | Acme`,
    generateDescription: ({ doc }) => doc.excerpt ?? '',
    generateURL: ({ doc }) => `https://acme.com/posts/${doc.slug}`,
  }),
]
```

| Option | Type | Default | Description |
| - | - | - | - |
| `collections` | `CollectionSlug[]` | — | Collection slugs that get SEO fields and may call the generate functions. |
| `uploadsCollection` | `CollectionSlug` | — | Upload collection for the meta image. Without it, no image field is added. |
| `tabbedUI` | `boolean` | `false` | Uses a tabbed layout: the collection's existing fields move into a first tab, labeled with the collection's singular label or "Content", and the `meta` group goes in an "SEO" tab. If the first field is already a tabs field, the "SEO" tab is appended to it. An auth collection's `email` field stays above the tabs. |
| `interfaceName` | `string` | — | Names the generated type for the `meta` group. |
| `fields` | `({ defaultFields }: { defaultFields: Field[] }) => Field[]` | Default fields | Receives the default fields and returns the fields to use in the `meta` group. |
| `generateTitle` | `(args: GenerateArgs) => string \| Promise<string>` | — | Returns the meta title. |
| `generateDescription` | `(args: GenerateArgs) => string \| Promise<string>` | — | Returns the meta description. |
| `generateURL` | `(args: GenerateArgs) => string \| Promise<string>` | — | Returns the page URL shown in the search preview. |
| `generateImage` | `(args: GenerateArgs) => string \| number \| { id: string \| number } \| Promise<…>` | — | Returns the meta image, as an upload ID or `{ id }`. |

An "Auto-generate" button appears only on fields whose generate function you provide.

## Generate functions

Every generate function receives the same `GenerateArgs` object:

| Argument | Type | Description |
| - | - | - |
| `collectionSlug` | `CollectionSlug` | The listed collection the request came from. |
| `doc` | `Partial<TypedCollection<collectionSlug>>` | The document's current form values, typed from your generated types. Every property is optional, because the document may not be saved yet. |
| `req` | `FrogBotRequest` | The FrogBot request, with `req.frogbot` and `req.user`. |
| `id` | `string \| number` | The document ID. Unset for a document that hasn't been saved. |
| `locale` | `string` | The locale being edited, when localization is enabled. |
| `title` | `string` | The document's current title, from the collection's `useAsTitle` field. |
| `hasSavePermission` | `boolean` | Whether the user may save the document. |
| `hasPublishPermission` | `boolean` | Whether the user may publish the document. |
| `hasPublishedDoc` | `boolean` | Typed, but the admin panel doesn't send it, so it's `undefined`. |
| `versionCount` | `number` | Typed, but the admin panel doesn't send it, so it's `undefined`. |
| `preferencesKey` | `string` | Typed, but the admin panel doesn't send it, so it's `undefined`. |

Checking `collectionSlug` narrows `doc` to that collection's type:

```ts theme={"theme":"tokyo-night"}
seoPlugin({
  collections: ['posts', 'pages'],
  generateTitle: ({ collectionSlug, doc }) => {
    if (collectionSlug === 'posts') return `${doc.title ?? 'Untitled'} | Acme blog`

    return `${doc.title ?? 'Untitled'} | Acme`
  },
})
```

Generation requires a signed-in user with admin access. For a new document, the user must be allowed to create it; for a saved one, to read it. A collection that isn't listed in `collections` can't use the generate functions. When a request is rejected, the "Auto-generate" button clears the field instead of filling it, so check the user's access if a field empties.

## Place the fields yourself

To put the SEO fields in your own layout, such as a "Search" tab, build them with the factories from `@frogbotai/plugin-seo/fields` and still list the collection in `collections`. The plugin finds the factory fields and adds nothing to that collection, so it has exactly one `meta` group, and "Auto-generate" still works.

```ts theme={"theme":"tokyo-night"}
import {
  MetaDescriptionField,
  MetaImageField,
  MetaTitleField,
  OverviewField,
  PreviewField,
} from '@frogbotai/plugin-seo/fields'
import type { CollectionConfig } from 'frogbot'

export const Pages: CollectionConfig = {
  slug: 'pages',
  fields: [
    {
      type: 'tabs',
      tabs: [
        { label: 'Content', fields: [{ name: 'title', type: 'text' }] },
        {
          name: 'meta',
          label: 'Search',
          fields: [
            OverviewField({}),
            MetaTitleField({ hasGenerateFn: true }),
            MetaDescriptionField({ hasGenerateFn: true }),
            MetaImageField({ relationTo: 'media' }),
            PreviewField({ hasGenerateFn: true }),
          ],
        },
      ],
    },
  ],
}
```

```ts theme={"theme":"tokyo-night"}
plugins: [seoPlugin({ collections: ['posts', 'pages'], generateTitle })]
```

Here `posts` gets the automatic `meta` group and `pages` keeps its own layout. Pass `hasGenerateFn: true` to a field whose generate function you provide, to show its "Auto-generate" button; on `PreviewField` it uses `generateURL`.

Each factory takes one argument object. `overrides` is spread over the returned field last, so it can replace any property, including `name` and `admin`.

| Factory | Returns | Argument | Type | Default | Description |
| - | - | - | - | - | - |
| `MetaTitleField` | `text` field named `title` | `hasGenerateFn` | `boolean` | `false` | Shows the "Auto-generate" button, backed by `generateTitle`. |
| | | `overrides` | `Partial<TextField>` | — | Properties merged over the field. |
| `MetaDescriptionField` | `textarea` field named `description` | `hasGenerateFn` | `boolean` | `false` | Shows the "Auto-generate" button, backed by `generateDescription`. |
| | | `overrides` | `Partial<TextareaField>` | — | Properties merged over the field. |
| `MetaImageField` | `upload` field named `image` | `relationTo` | `string` | Required | Upload collection the image comes from. |
| | | `hasGenerateFn` | `boolean` | `false` | Shows the "Auto-generate" button, backed by `generateImage`. |
| | | `overrides` | `Partial<UploadField>` | — | Properties merged over the field. |
| `OverviewField` | `ui` field named `overview` | `titlePath` | `string` | `'meta.title'` | Path of the title field the overview checks. |
| | | `descriptionPath` | `string` | `'meta.description'` | Path of the description field the overview checks. |
| | | `imagePath` | `string` | `'meta.image'` | Path of the image field the overview checks. |
| | | `titleOverrides` | `{ minLength?: number; maxLength?: number }` | `{ minLength: 50, maxLength: 60 }` | Title length the overview counts as complete. |
| | | `descriptionOverrides` | `{ minLength?: number; maxLength?: number }` | `{ minLength: 100, maxLength: 150 }` | Description length the overview counts as complete. |
| | | `overrides` | `Partial<UIField>` | — | Properties merged over the field. |
| `PreviewField` | `ui` field named `preview` | `hasGenerateFn` | `boolean` | `false` | Fetches the preview URL from `generateURL`. |
| | | `titlePath` | `string` | `'meta.title'` | Path of the title field shown in the preview. |
| | | `descriptionPath` | `string` | `'meta.description'` | Path of the description field shown in the preview. |
| | | `overrides` | `Partial<UIField>` | — | Properties merged over the field. |

The factories return FrogBot fields, so they need no casts, and `overrides` accepts FrogBot hooks, access, and validation.

How the plugin detects placed fields:

* It looks through the collection's fields, including tabs, groups, rows, and collapsibles. Fields inside blocks and arrays don't count.
* One factory field is enough. The plugin doesn't add the ones you left out.
* Fields you write by hand, and factory fields whose `overrides` replace the admin component, aren't detected. That collection still gets the automatic group.

Name the tab or group `meta`, as above, so the fields are stored at `meta.title`, `meta.description`, and `meta.image`, where the overview and preview look for them. Under another name, pass `titlePath`, `descriptionPath`, and `imagePath` to `OverviewField`, and `titlePath` and `descriptionPath` to `PreviewField`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.