@bl0cka.de/payload-alt-text-plugin (0.2.1)

Published 2026-06-10 15:11:20 +02:00 by marcel in npm-registry/payload-alt-text-plugin

Installation

@bl0cka.de:registry=
npm install @bl0cka.de/payload-alt-text-plugin@0.2.1
"@bl0cka.de/payload-alt-text-plugin": "0.2.1"

About this package

@bl0cka.de/payload-alt-text-plugin

A Payload CMS v3 plugin that adds AI-powered alt text (and keyword) generation to any upload-enabled collection or global — with a swappable AI resolver.

How it works

  1. User selects an image in the Payload admin
  2. The image is resized on the client (OffscreenCanvas, no extra server load)
  3. The small thumbnail is POSTed to a custom Payload endpoint
  4. The endpoint delegates to your chosen resolver (Claude, OpenAI, or custom)
  5. The alt and altKeywords fields are filled in automatically
  6. If the user has already started typing when the generation finishes, a conflict banner appears — offering to Overwrite, Append, Copy, or Keep

The user can always edit the generated text and click Regenerate at any time. Nothing is saved until the user clicks Payload's normal Save button.


Installation

pnpm add @bl0cka.de/payload-alt-text-plugin
pnpm add @anthropic-ai/sdk   # if using claudeResolver
pnpm add openai              # if using openAIResolver

This package ships pre-compiled ESM with bundled type declarations, so it works out of the box in any Next.js or Node.js setup — no transpilePackages entry is required.


File structure

src/
├── index.ts                              # Plugin entry point + public exports
├── types.ts                              # All shared TypeScript types
├── components/
│   └── AltTextGeneratorField.tsx         # 'use client' field component
├── endpoints/
│   ├── generateAltText.ts                # Payload custom endpoint (server only)
│   └── renameMediaFile.ts                # Optional: atomic file rename endpoint
├── lib/
│   └── resizeImage.ts                    # Client-side canvas resize utility
└── resolvers/
    ├── index.ts                          # Re-exports all resolvers
    ├── claudeResolver.ts                 # Anthropic Claude vision resolver
    └── openAIResolver.ts                 # OpenAI GPT-4o resolver

Minimal usage

// payload.config.ts
import { buildConfig } from 'payload'
import { altTextPlugin } from '@bl0cka.de/payload-alt-text-plugin'
import { claudeResolver } from '@bl0cka.de/payload-alt-text-plugin/resolvers'

export default buildConfig({
  plugins: [
    altTextPlugin({
      resolver: claudeResolver({ apiKey: process.env.ANTHROPIC_API_KEY! }),
      collections: ['media'],
    }),
  ],
})

All options

Top-level AltTextPluginOptions

Option Type Default Description
resolver AltTextResolver required AI resolver to use
collections Array<string | AltTextCollectionConfig> [] Upload collections to target
globals Array<string | AltTextGlobalConfig> [] Globals to target
apiEndpoint string '/api/generate-alt-text' URL the client POSTs to
renameApiEndpoint string '/api/rename-media-file' URL used to rename a saved upload's file
rateLimit { max, windowMs } | false { max: 30, windowMs: 60000 } Per-user limit on the generation endpoint (in-memory, per-instance). false to disable
enabled boolean true Set false to disable without removing

Additionally, every per-entity option below can also be set at the top level as a plugin-wide default for all targeted collections and globals. The per-entity value always wins (entity ?? pluginWide ?? built-in default):

altTextPlugin({
  resolver,
  enableKeywords: false,        // plugin-wide default
  thumbnailMaxWidth: 768,       // plugin-wide default
  collections: [
    'media',
    { slug: 'product-images', enableKeywords: true }, // entity override wins
  ],
})

Per-entity options (AltTextCollectionConfig / AltTextGlobalConfig)

Option Type Default Description
slug string required Collection/global slug
altFieldName string 'alt' Name of the alt text field
keywordsFieldName string 'altKeywords' Name of the keywords field
enableKeywords boolean true Include the keywords hasMany field
required boolean true Make alt text required on save
altFieldOverrides Partial<TextField> {} Merge extra config onto the alt field
keywordsFieldOverrides Partial<TextField> {} Merge extra config onto the keywords field
defaultLocale string 'en' Fallback locale for generation
thumbnailMaxWidth number 512 Client-side resize max width (px)
thumbnailQuality number 0.8 Client-side JPEG quality (0–1)
autoGenerate boolean true Auto-generate on file select
autoGenerateOnOpen boolean true Auto-generate when opening a saved doc with empty alt
overwriteOnAutoGenerate boolean false Silently overwrite on auto-generate
enableFilenameRename boolean true AI-powered filename suggestion banner
autoAcceptSuggestedFilename boolean false Apply filename suggestions automatically instead of showing the accept/dismiss banner
generateOnUpload boolean false Generate alt text server-side on upload (covers native bulk upload; see below)
replaceExistingFields boolean true Replace a pre-existing top-level alt/keywords field instead of erroring on a duplicate (top-level only; overrides the plugin-wide value)
uploadFieldName (globals only) string 'image' Form field whose File to use

Existing alt fields: Upload collections often already declare an alt field. By default the plugin replaces any colliding top-level field with its own (which wires up the generator UI), so you don't hit Payload's duplicate field-name error. Set replaceExistingFields: false to keep your own field instead (then use a distinct altFieldName to avoid the collision). Only top-level fields are scanned — not fields nested in tabs/rows/groups.

Payload plugin API: On Payload ≥ 3.83 the plugin registers via the experimental definePlugin API, exposing a slug (payload-alt-text-plugin) for cross-plugin discovery. On older versions it falls back to a plain plugin factory automatically — no minimum-version requirement.


Swapping the AI provider

// Use Claude (fast and cost-effective)
import { claudeResolver } from '@bl0cka.de/payload-alt-text-plugin/resolvers'

resolver: claudeResolver({
  apiKey: process.env.ANTHROPIC_API_KEY!,
  model: 'claude-sonnet-4-5', // optional, default: 'claude-haiku-4-5'
})

// Use OpenAI
import { openAIResolver } from '@bl0cka.de/payload-alt-text-plugin/resolvers'

resolver: openAIResolver({
  apiKey: process.env.OPENAI_API_KEY!,
  model: 'gpt-4o', // optional, default: 'gpt-4o-mini'
})

// Bring your own
const myResolver: AltTextResolver = {
  key: 'my-provider',
  resolve: async ({ fileBuffer, filename, locale }) => {
    // ... call your API
    return { altText: '...', keywords: ['...'] }
  },
}

Advanced example

altTextPlugin({
  resolver: claudeResolver({ apiKey: process.env.ANTHROPIC_API_KEY! }),
  enabled: process.env.NODE_ENV !== 'test',

  collections: [
    // Shorthand — all defaults
    'media',

    // Full config
    {
      slug: 'product-images',
      altFieldName: 'imageAlt',
      keywordsFieldName: 'imageKeywords',
      enableKeywords: true,
      required: true,
      defaultLocale: 'de',
      thumbnailMaxWidth: 768,
      thumbnailQuality: 0.85,
      autoGenerate: true,
      overwriteOnAutoGenerate: false,
      enableFilenameRename: true,
      altFieldOverrides: {
        label: 'Image description',
        localized: true,
        admin: {
          description: 'Required for accessibility. Auto-generated from the image.',
        },
      },
    },

    // No keywords, manual generation only
    {
      slug: 'avatars',
      enableKeywords: false,
      autoGenerate: false,
      required: false,
    },
  ],

  globals: [
    {
      slug: 'seo',
      uploadFieldName: 'openGraphImage',
      altFieldName: 'ogImageAlt',
      enableKeywords: false,
    },
  ],
})

Conflict resolution banner

When autoGenerate: true and the user starts typing in the alt text field before generation finishes, the component shows a banner with four choices:

  • Overwrite — replace typed text with the generated text
  • Append — join the typed text and generated text with a sentence separator
  • Copy to clipboard — copy generated text to clipboard, keep typed text as-is
  • Keep mine — dismiss the banner, discard the generated text

Set overwriteOnAutoGenerate: true to skip the banner and always overwrite silently.


Filename rename

When enableFilenameRename is active (the default), the AI also evaluates whether the current filename is descriptive and suggests a better one if not. A banner appears offering to rename or keep the original. Set enableFilenameRename: false to turn the suggestion off.

For already-saved documents a dedicated Suggest filename button appears next to Generate: it asks the AI to evaluate the current filename only, without touching the alt text or keywords fields. If the name is judged descriptive, a confirmation note is shown instead of a banner.

Set autoAcceptSuggestedFilename: true (per-entity or plugin-wide) to apply suggestions automatically instead of showing the banner: pre-save uploads (including the bulk-upload drawer) get their staged file renamed in place, and saved documents are renamed through the rename endpoint right away. The server-side generateOnUpload hook never renames files — auto-accept applies to the admin UI flows.

How the rename is applied depends on whether the document has been saved yet:

  • New (unsaved) upload — the suggestion is written to the form's filename field and stored under that name when you click Payload's Save button.
  • Already-saved upload — clicking Rename calls the plugin's /api/rename-media-file endpoint, which re-stores the file under the new name through Payload's own upload pipeline. This regenerates the image size variants, rewrites filename/url/sizes.* in the database, and removes the old files. Because it goes through payload.update, it works on all database adapters (MongoDB, Postgres, SQLite) and any storage adapter — no manual wiring required.

The rename reads the original file from disk (local storage). The size variants are regenerated (re-encoded) on rename; the original is preserved byte-for-byte.

The endpoint is registered automatically. Its path can be customised with the renameApiEndpoint plugin option (default /api/rename-media-file). The lower-level factory is also exported if you need to mount it yourself:

import { createRenameEndpoint } from '@bl0cka.de/payload-alt-text-plugin/endpoints/renameMediaFile'

Bulk upload / server-side generation (optional)

The generator UI is client-driven, so it doesn't run during Payload's native bulk upload (many docs created at once, no field UI in the loop) — those images would land with an empty alt, and a required: true alt would even reject the bulk create.

Set generateOnUpload: true (per-entity or plugin-wide) to also generate alt text server-side in a beforeChange hook, reading the uploaded file directly. Because it runs before validation and only fills alt when empty, it covers bulk upload and satisfies a required alt field. Failures are logged and never block the upload. Note: it adds one AI call per qualifying upload, so it's opt-in.

altTextPlugin({
  resolver: claudeResolver({ apiKey: process.env.ANTHROPIC_API_KEY! }),
  collections: [{ slug: 'media', generateOnUpload: true }],
})

Writing a custom resolver

import type { AltTextResolver } from '@bl0cka.de/payload-alt-text-plugin'

export const myResolver: AltTextResolver = {
  key: 'my-backend',
  resolve: async ({ fileBuffer, filename, locale }) => {
    const base64 = fileBuffer.toString('base64')
    // ... call your API with base64, filename, locale
    return {
      altText: 'A descriptive alt text string',
      keywords: ['keyword1', 'keyword2'],
    }
  },
}

Dependencies

Dependencies

ID Version
ts-essentials ^10.0.0

Development dependencies

ID Version
release-it ^20.2.0
tsdown ^0.15.12
typescript ^5.9.3
vitest ^4.1.8

Peer dependencies

ID Version
@payloadcms/ui >=3
payload >=3
react >=18

Optional dependencies

ID Version
@anthropic-ai/sdk >=0.20
openai >=4
Details
npm
2026-06-10 15:11:20 +02:00
13
MIT
58 KiB
Assets (1)
Versions (6) View all
0.2.2 2026-07-07
0.2.1 2026-06-10
0.2.0 2026-06-10
0.1.4 2026-06-04
0.1.3 2026-06-02