@jhb.software/payload-alt-text-plugin 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -19
- package/dist/endpoints/bulkGenerateAltTexts.js +13 -4
- package/dist/endpoints/bulkGenerateAltTexts.js.map +1 -1
- package/dist/endpoints/generateAltText.js +13 -4
- package/dist/endpoints/generateAltText.js.map +1 -1
- package/dist/exports/server.d.ts +6 -0
- package/dist/exports/server.js +6 -1
- package/dist/exports/server.js.map +1 -1
- package/dist/hooks/revalidateAltTextHealth.js +31 -17
- package/dist/hooks/revalidateAltTextHealth.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +24 -3
- package/dist/plugin.js.map +1 -1
- package/dist/resolvers/mistral.d.ts +40 -0
- package/dist/resolvers/mistral.js +280 -0
- package/dist/resolvers/mistral.js.map +1 -0
- package/dist/resolvers/openAI.d.ts +10 -0
- package/dist/resolvers/openAI.js +7 -7
- package/dist/resolvers/openAI.js.map +1 -1
- package/dist/resolvers/types.d.ts +24 -1
- package/dist/resolvers/types.js.map +1 -1
- package/dist/translations/index.js.map +1 -1
- package/dist/types/AltTextPluginConfig.d.ts +40 -8
- package/dist/types/AltTextPluginConfig.js.map +1 -1
- package/dist/utilities/altTextHealth.js.map +1 -1
- package/dist/utilities/mimeTypes.d.ts +54 -1
- package/dist/utilities/mimeTypes.js +41 -2
- package/dist/utilities/mimeTypes.js.map +1 -1
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ A [Payload CMS](https://payloadcms.com/) plugin that adds AI-powered alt text ge
|
|
|
6
6
|
|
|
7
7
|
- Generate alt text for images using AI in the Payload Admin UI
|
|
8
8
|
- Supports any AI provider using a resolver pattern (e.g., OpenAI, Anthropic, etc.)
|
|
9
|
-
- Comes with
|
|
9
|
+
- Comes with ready-to-use OpenAI and Mistral resolvers out of the box
|
|
10
10
|
- Automatic keyword extraction for improved admin search
|
|
11
11
|
- Bulk generation for processing multiple images at once
|
|
12
12
|
- Full localization support
|
|
@@ -77,27 +77,43 @@ This is also the recommended escape hatch if you hit Payload's Postgres SQL-buil
|
|
|
77
77
|
|
|
78
78
|
### Plugin Options
|
|
79
79
|
|
|
80
|
-
| Option | Type
|
|
81
|
-
| ---------------------------- |
|
|
82
|
-
| `collections` | `(CollectionSlug \| CollectionObj)[]`
|
|
83
|
-
| `resolver` | `AltTextResolver`
|
|
84
|
-
| `getImageThumbnail` | `Function`
|
|
85
|
-
| `enabled` | `boolean`
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
80
|
+
| Option | Type | Required | Description |
|
|
81
|
+
| ---------------------------- | ------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
82
|
+
| `collections` | `(CollectionSlug \| CollectionObj)[]` | Yes | Collections to enable alt text generation for (see [Per-collection options](#per-collection-options)) |
|
|
83
|
+
| `resolver` | `AltTextResolver` | Yes | Alt text resolver to use (e.g., `openAIResolver`) |
|
|
84
|
+
| `getImageThumbnail` | `Function` | Yes | Function to get the thumbnail URL from an image document |
|
|
85
|
+
| `enabled` | `boolean` | No | Whether to enable the plugin |
|
|
86
|
+
| `access` | `({ req }) => boolean \| Promise<boolean>` | No | Access control for the plugin's REST endpoints. Defaults to `({ req }) => !!req.user` (any authenticated user) — see [Authentication](#authentication) |
|
|
87
|
+
| `locale` | `string` | No | Locale for alt text generation (required when localization is disabled) |
|
|
88
|
+
| `maxBulkGenerateConcurrency` | `number` | No | Maximum concurrent API requests for bulk operations (default: 16) |
|
|
89
|
+
| `maxBulkGenerateIds` | `number` | No | Maximum number of image IDs accepted per bulk generate request; larger requests are rejected with `400`. Duplicate IDs are collapsed before the limit is applied (default: 100) |
|
|
90
|
+
| `fieldsOverride` | `Function` | No | Override the default fields inserted by the plugin |
|
|
91
|
+
| `healthCheck` | `boolean \| Function` | No | Alt text health tracking (REST endpoint, cache revalidation hooks, dashboard widget). `false` disables it; `true` enables it gated by `access`; a `({ req }) => boolean` function enables it and gates both the endpoint and the widget — use it to restrict the collection-wide report, e.g. to admins (default: `true`) |
|
|
92
|
+
| `imageThumbnailMimeType` | `string` | No | The MIME type `getImageThumbnail` delivers. Set it when your thumbnail URL transcodes the image, so the stored format no longer decides whether generation is possible (see [Transcoding thumbnails](#transcoding-thumbnails)) |
|
|
93
|
+
|
|
94
|
+
`getImageThumbnail` receives the document and `{ collection, req }`, so a single function can build different URLs per collection:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
getImageThumbnail: (doc, { collection }) =>
|
|
98
|
+
collection === 'media' ? cloudinaryThumbnail(doc) : String(doc.url)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
It may also be async, so the URL can be signed on demand:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
getImageThumbnail: async (doc, { req }) => await presignThumbnailUrl(String(doc.url), req)
|
|
105
|
+
```
|
|
91
106
|
|
|
92
107
|
### Per-collection options
|
|
93
108
|
|
|
94
109
|
Each entry in `collections` may be either a bare collection slug (shorthand, defaults to `['image/*']` for `mimeTypes`) or an object with the following fields:
|
|
95
110
|
|
|
96
|
-
| Option
|
|
97
|
-
|
|
|
98
|
-
| `slug`
|
|
99
|
-
| `mimeTypes`
|
|
100
|
-
| `validate`
|
|
111
|
+
| Option | Type | Required | Description |
|
|
112
|
+
| ------------------------ | ------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
113
|
+
| `slug` | `CollectionSlug` | Yes | The collection slug |
|
|
114
|
+
| `mimeTypes` | `string[]` | No | MIME types the plugin tracks, validates, and generates for. Supports wildcards like `image/*`. Defaults to `['image/*']`. |
|
|
115
|
+
| `validate` | `TextareaFieldValidation` | No | Custom validator that fully replaces the default required-alt check. Import `validateAltText` from the plugin to compose around the default behavior (see [Custom validator](#custom-validator)). |
|
|
116
|
+
| `imageThumbnailMimeType` | `string \| null` | No | Overrides the plugin-level option of the same name for this collection. `null` opts the collection out of a plugin-level default (see [Transcoding thumbnails](#transcoding-thumbnails)). |
|
|
101
117
|
|
|
102
118
|
```ts
|
|
103
119
|
payloadAltTextPlugin({
|
|
@@ -109,6 +125,39 @@ payloadAltTextPlugin({
|
|
|
109
125
|
})
|
|
110
126
|
```
|
|
111
127
|
|
|
128
|
+
#### Transcoding thumbnails
|
|
129
|
+
|
|
130
|
+
The resolver never sees the stored file — it only receives the URL returned by `getImageThumbnail`. When a resolver declares `supportedMimeTypes`, the plugin checks a document's stored `mimeType` against that list, which is a safe default but wrong as soon as your thumbnail URL transcodes: an AVIF or HEIC upload served through a Cloudinary `f_webp` transformation reaches the provider as WebP, yet gets rejected on its stored format.
|
|
131
|
+
|
|
132
|
+
Declare what your thumbnail URL actually delivers to remove the mismatch:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
payloadAltTextPlugin({
|
|
136
|
+
collections: ['media'],
|
|
137
|
+
resolver: openAIResolver({ apiKey: process.env.OPENAI_API_KEY }),
|
|
138
|
+
// Always transcodes to WebP, whatever the source format is
|
|
139
|
+
getImageThumbnail: (doc) => String(doc.url).replace('/upload/', '/upload/w_600,f_webp/'),
|
|
140
|
+
imageThumbnailMimeType: 'image/webp',
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
With the declaration in place, the source format no longer gates generation — the admin button stays enabled and the endpoints stop rejecting on `mimeType`. Which source formats get alt text at all is still governed by each collection's `mimeTypes`. The declaration is validated against the resolver's `supportedMimeTypes` once at config load, so transcoding into a format your resolver cannot handle fails at boot instead of once per image.
|
|
145
|
+
|
|
146
|
+
Only declare a format your transformation **always** produces. A `f_auto`-style transformation negotiates the format from the fetching client's `Accept` header and may serve the source format back, so leave it unset there and let the conservative source check apply. If you want AVIF sources to work, transcode explicitly.
|
|
147
|
+
|
|
148
|
+
Collections may override the plugin-level value, or opt out of it with `null` when they are served raw:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
payloadAltTextPlugin({
|
|
152
|
+
collections: [
|
|
153
|
+
'media', // inherits image/webp
|
|
154
|
+
{ slug: 'documents', imageThumbnailMimeType: null }, // checked on its stored mimeType
|
|
155
|
+
],
|
|
156
|
+
imageThumbnailMimeType: 'image/webp',
|
|
157
|
+
// ...
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
112
161
|
#### Custom validator
|
|
113
162
|
|
|
114
163
|
The default validator requires alt text on every tracked document. Some workflows — folder moves, partial API updates, or localized setups with `fallback: false` where some locales are intentionally empty — need to skip that check when the request body does not touch `alt`. Pass a `validate` function to override the default, and compose around the exported `validateAltText` to keep the standard behavior for full updates:
|
|
@@ -156,6 +205,18 @@ buildConfig({
|
|
|
156
205
|
|
|
157
206
|
Set `healthCheck: false` in the plugin config to disable the REST endpoint, cache revalidation hooks, and dashboard widget. If your project replaces the default dashboard via `admin.components.views.dashboard`, you need to integrate the widget into your custom dashboard yourself.
|
|
158
207
|
|
|
208
|
+
#### Skipping cache revalidation for individual writes
|
|
209
|
+
|
|
210
|
+
The plugin invalidates the cached health scan via `afterChange` and `afterDelete` hooks. For writes that don't need to invalidate the cache — typically seed data created from `payload.onInit`, batch imports, or migrations — pass `context: { disableRevalidate: true }` to skip the revalidation:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
await payload.create({
|
|
214
|
+
collection: 'media',
|
|
215
|
+
data: {/* ... */},
|
|
216
|
+
context: { disableRevalidate: true },
|
|
217
|
+
})
|
|
218
|
+
```
|
|
219
|
+
|
|
159
220
|
### Resolvers
|
|
160
221
|
|
|
161
222
|
This plugin is designed to work seamlessly with various AI providers by accepting a customizable resolver as a configuration option.
|
|
@@ -173,16 +234,47 @@ openAIResolver({
|
|
|
173
234
|
})
|
|
174
235
|
```
|
|
175
236
|
|
|
237
|
+
| Option | Type | Required | Description |
|
|
238
|
+
| -------------------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
239
|
+
| `apiKey` | `string` | Yes | API key for authentication |
|
|
240
|
+
| `model` | `string` | No | Model to use (default: `gpt-4.1-nano`) |
|
|
241
|
+
| `baseUrl` | `string` | No | Base URL for an OpenAI-compatible provider (e.g. Nebius, Azure) |
|
|
242
|
+
| `supportedMimeTypes` | `string[]` | No | Image formats the provider accepts (default: `['image/jpeg', 'image/png', 'image/gif', 'image/webp']`, per OpenAI's vision docs). Override it when using a `baseUrl` whose provider differs |
|
|
243
|
+
|
|
244
|
+
#### Mistral Resolver
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { mistralResolver } from '@jhb.software/payload-alt-text-plugin'
|
|
248
|
+
|
|
249
|
+
mistralResolver({
|
|
250
|
+
apiKey: process.env.MISTRAL_API_KEY,
|
|
251
|
+
model: 'mistral-medium-latest', // default; any vision-capable Mistral model works
|
|
252
|
+
})
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Unlike the OpenAI resolver, this one downloads the image and sends the bytes
|
|
256
|
+
rather than handing Mistral the thumbnail URL. Mistral's own fetcher needs the
|
|
257
|
+
file to be reachable from the public internet, which is never the case in local
|
|
258
|
+
development and not the case for private buckets; some hosts also refuse it
|
|
259
|
+
outright (`File could not be fetched from url`, error 3310). Sending the bytes
|
|
260
|
+
costs one extra download and removes that whole class of failure.
|
|
261
|
+
|
|
262
|
+
Because there is no image conversion step, `supportedMimeTypes` is limited to
|
|
263
|
+
what the Mistral API accepts directly: JPEG, PNG, GIF and WebP. Documents in
|
|
264
|
+
other formats — SVG or AVIF, for instance — keep their generate button disabled.
|
|
265
|
+
|
|
176
266
|
## Custom Resolver
|
|
177
267
|
|
|
178
268
|
You can create your own resolver by implementing the `AltTextResolver` interface.
|
|
179
269
|
|
|
270
|
+
Alongside `imageThumbnailUrl`, the resolver receives `imageThumbnailMimeType` — the format served at that URL, when the collection declares one via [`imageThumbnailMimeType`](#transcoding-thumbnails). Resolvers that hand the URL to the provider can ignore it. Resolvers that inline the bytes need it, because an explicit media type cannot be sniffed from a URL: Anthropic image blocks require `media_type` and Gemini's `inline_data` requires `mime_type`. It is `undefined` when nothing was declared.
|
|
271
|
+
|
|
180
272
|
```ts
|
|
181
273
|
import type { AltTextResolver } from '@jhb.software/payload-alt-text-plugin'
|
|
182
274
|
|
|
183
275
|
export const customResolver = (): AltTextResolver => ({
|
|
184
276
|
key: 'custom',
|
|
185
|
-
resolve: async ({ imageThumbnailUrl, filename, locale, req }) => {
|
|
277
|
+
resolve: async ({ imageThumbnailUrl, imageThumbnailMimeType, filename, locale, req }) => {
|
|
186
278
|
// Your custom alt text generation logic here
|
|
187
279
|
const altText = await generateAltText(imageThumbnailUrl, filename, locale, req)
|
|
188
280
|
|
|
@@ -205,7 +297,24 @@ export const customResolver = (): AltTextResolver => ({
|
|
|
205
297
|
|
|
206
298
|
## REST API Endpoints
|
|
207
299
|
|
|
208
|
-
The plugin registers the following REST API endpoints under `/api/alt-text/`.
|
|
300
|
+
The plugin registers the following REST API endpoints under `/api/alt-text/`.
|
|
301
|
+
|
|
302
|
+
### Authentication
|
|
303
|
+
|
|
304
|
+
The endpoints require an authenticated request and respond with `401` otherwise. By default any authenticated Payload user (admin session or API key) is allowed:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
;({ req }) => !!req.user
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
That default fits a setup where every Payload user is trusted staff. Generating alt text spends money at the configured provider and the bulk endpoint writes to many documents at once, so projects with public sign-up, customer-facing accounts, or any user tier that should not incur provider cost must narrow it via the `access` option:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
// Only allow editors to use the generate endpoints
|
|
314
|
+
access: ({ req }) => req.user?.role === 'editor'
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Beyond that gate, the generate endpoints enforce each collection's own access control on the documents they read and write, and the health endpoint reports only the collections the requesting user can read (and can be gated separately via the `healthCheck` function).
|
|
209
318
|
|
|
210
319
|
### `POST /api/alt-text/generate`
|
|
211
320
|
|
|
@@ -2,7 +2,7 @@ import pMap from 'p-map';
|
|
|
2
2
|
import { APIError, Forbidden } from 'payload';
|
|
3
3
|
import { ZodError } from 'zod';
|
|
4
4
|
import { localesFromConfig } from '../utilities/localesFromConfig.js';
|
|
5
|
-
import { matchesMimeType } from '../utilities/mimeTypes.js';
|
|
5
|
+
import { getUnsupportedSourceMimeTypeError, matchesMimeType } from '../utilities/mimeTypes.js';
|
|
6
6
|
import { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js';
|
|
7
7
|
/**
|
|
8
8
|
* Generates and updates alt text for multiple images in all locales.
|
|
@@ -151,12 +151,21 @@ async function generateAndUpdateAltText({ id, collection, locales, payload, plug
|
|
|
151
151
|
if (mimeType && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {
|
|
152
152
|
throw new Error(`Alt text is not tracked for files of type "${mimeType}" in the "${collection}" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`);
|
|
153
153
|
}
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
const unsupportedSourceError = getUnsupportedSourceMimeTypeError({
|
|
155
|
+
declaredThumbnailMimeType: collectionConfig.imageThumbnailMimeType,
|
|
156
|
+
mimeType,
|
|
157
|
+
supportedMimeTypes: pluginConfig.resolver.supportedMimeTypes
|
|
158
|
+
});
|
|
159
|
+
if (unsupportedSourceError) {
|
|
160
|
+
throw new Error(unsupportedSourceError);
|
|
156
161
|
}
|
|
157
|
-
const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc
|
|
162
|
+
const imageThumbnailUrl = await pluginConfig.getImageThumbnail(imageDoc, {
|
|
163
|
+
collection,
|
|
164
|
+
req
|
|
165
|
+
});
|
|
158
166
|
const result = await pluginConfig.resolver.resolveBulk({
|
|
159
167
|
filename: 'filename' in imageDoc && typeof imageDoc.filename === 'string' ? imageDoc.filename : undefined,
|
|
168
|
+
imageThumbnailMimeType: collectionConfig.imageThumbnailMimeType,
|
|
160
169
|
imageThumbnailUrl,
|
|
161
170
|
locales,
|
|
162
171
|
req
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/bulkGenerateAltTexts.ts"],"sourcesContent":["import type { BasePayload, CollectionSlug, PayloadHandler, PayloadRequest } from 'payload'\n\nimport pMap from 'p-map'\nimport { APIError, Forbidden } from 'payload'\nimport { ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { localesFromConfig } from '../utilities/localesFromConfig.js'\nimport { matchesMimeType } from '../utilities/mimeTypes.js'\nimport { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'\n\n/**\n * Generates and updates alt text for multiple images in all locales.\n */\nexport const bulkGenerateAltTextsEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const { collection, ids } = bulkGenerateAltTextsRequestSchema.parse(data)\n\n let updatedDocs = 0\n const erroredDocs: (number | string)[] = []\n\n // Get plugin config from payload config\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n | AltTextPluginConfig\n | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n // Treat the configured collections as an allowlist. Reject any other\n // collection before touching the Local API, so the endpoint can only ever\n // operate on the upload collections the plugin manages.\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)\n\n if (!collectionConfig) {\n return Response.json(\n { error: `Collection \"${collection}\" is not managed by the alt text plugin.` },\n { status: 403 },\n )\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const concurrency = pluginConfig.maxBulkGenerateConcurrency\n\n // De-duplicate so the same image is never generated (and billed) twice,\n // then bound the batch so a single request cannot fan out into an\n // unbounded number of paid resolver calls.\n const uniqueIds = [...new Set(ids)]\n\n if (uniqueIds.length > pluginConfig.maxBulkGenerateIds) {\n return Response.json(\n {\n error: `Too many ids: ${uniqueIds.length} exceeds the maximum of ${pluginConfig.maxBulkGenerateIds} per request.`,\n },\n { status: 400 },\n )\n }\n\n // determine target locales based on config\n const locales = localesFromConfig(req.payload.config)\n const targetLocales = locales ?? [pluginConfig.locale!]\n if (!targetLocales) {\n return Response.json(\n {\n error:\n 'Could not determine target locales for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n await pMap(\n uniqueIds,\n async (id) => {\n try {\n await generateAndUpdateAltText({\n id,\n collection,\n locales: targetLocales,\n payload: req.payload,\n pluginConfig,\n req,\n })\n updatedDocs++\n console.log(\n `${updatedDocs}/${uniqueIds.length} updated (${Math.round((updatedDocs / uniqueIds.length) * 100)}%)`,\n )\n } catch (error) {\n // A Forbidden means the user has no read/update access to the\n // collection at all — it applies to every id, so fail the whole\n // request with a real 403 instead of silently listing all ids as\n // errored. Row-level NotFound stays a per-doc error (partial success).\n if (error instanceof Forbidden) {\n throw error\n }\n console.error(`Error generating alt text for ${id}:`, error)\n erroredDocs.push(id)\n }\n },\n { concurrency },\n )\n\n if (erroredDocs.length > 0) {\n console.error(`Failed for: ${erroredDocs.join(', ')}`)\n }\n\n return Response.json({\n erroredDocs,\n totalDocs: uniqueIds.length,\n updatedDocs,\n })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(formatZodError(error), { status: 400 })\n }\n // Surface Payload access errors (Forbidden 403) with their real status so\n // an agent gets an accurate, non-retryable signal instead of a 500.\n if (error instanceof APIError) {\n return Response.json({ error: error.message }, { status: error.status })\n }\n console.error('Error in bulk generation:', error)\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n\nasync function generateAndUpdateAltText({\n id,\n collection,\n locales,\n payload,\n pluginConfig,\n req,\n}: {\n collection: CollectionSlug\n id: number | string\n locales: string[]\n payload: BasePayload\n pluginConfig: AltTextPluginConfig\n req: PayloadRequest\n}) {\n const imageDoc = await payload.findByID({\n id,\n collection,\n depth: 0,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n\n if (!imageDoc) {\n throw new Error('Image not found')\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string' ? imageDoc.mimeType : undefined\n\n // The handler validates `collection` against the configured collections before\n // reaching this helper, so a matching entry is guaranteed.\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)!\n\n if (mimeType && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {\n throw new Error(\n `Alt text is not tracked for files of type \"${mimeType}\" in the \"${collection}\" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`,\n )\n }\n\n if (\n mimeType &&\n pluginConfig.resolver.supportedMimeTypes &&\n !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)\n ) {\n throw new Error(\n `Alt text generation is not supported for files of type \"${mimeType}\". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`,\n )\n }\n\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n const result = await pluginConfig.resolver.resolveBulk({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailUrl,\n locales,\n req,\n })\n\n if (!result.success) {\n throw new Error(result.error || 'Failed to generate alt text')\n }\n\n for (const locale of locales) {\n const localeResult = result.results[locale]\n if (localeResult) {\n await payload.update({\n id,\n collection,\n data: {\n alt: localeResult.altText,\n keywords: localeResult.keywords,\n },\n locale,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n }\n }\n}\n"],"names":["pMap","APIError","Forbidden","ZodError","localesFromConfig","matchesMimeType","bulkGenerateAltTextsRequestSchema","formatZodError","bulkGenerateAltTextsEndpoint","access","req","Response","json","error","status","data","collection","ids","parse","updatedDocs","erroredDocs","pluginConfig","payload","config","custom","altTextPluginConfig","collectionConfig","collections","find","entry","slug","resolver","concurrency","maxBulkGenerateConcurrency","uniqueIds","Set","length","maxBulkGenerateIds","locales","targetLocales","locale","id","generateAndUpdateAltText","console","log","Math","round","push","join","totalDocs","message","Error","imageDoc","findByID","depth","overrideAccess","user","mimeType","undefined","mimeTypes","supportedMimeTypes","includes","imageThumbnailUrl","getImageThumbnail","result","resolveBulk","filename","success","localeResult","results","update","alt","altText","keywords"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/bulkGenerateAltTexts.ts"],"sourcesContent":["import type { BasePayload, CollectionSlug, PayloadHandler, PayloadRequest } from 'payload'\n\nimport pMap from 'p-map'\nimport { APIError, Forbidden } from 'payload'\nimport { ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { localesFromConfig } from '../utilities/localesFromConfig.js'\nimport { getUnsupportedSourceMimeTypeError, matchesMimeType } from '../utilities/mimeTypes.js'\nimport { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'\n\n/**\n * Generates and updates alt text for multiple images in all locales.\n */\nexport const bulkGenerateAltTextsEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const { collection, ids } = bulkGenerateAltTextsRequestSchema.parse(data)\n\n let updatedDocs = 0\n const erroredDocs: (number | string)[] = []\n\n // Get plugin config from payload config\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n AltTextPluginConfig | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n // Treat the configured collections as an allowlist. Reject any other\n // collection before touching the Local API, so the endpoint can only ever\n // operate on the upload collections the plugin manages.\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)\n\n if (!collectionConfig) {\n return Response.json(\n { error: `Collection \"${collection}\" is not managed by the alt text plugin.` },\n { status: 403 },\n )\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const concurrency = pluginConfig.maxBulkGenerateConcurrency\n\n // De-duplicate so the same image is never generated (and billed) twice,\n // then bound the batch so a single request cannot fan out into an\n // unbounded number of paid resolver calls.\n const uniqueIds = [...new Set(ids)]\n\n if (uniqueIds.length > pluginConfig.maxBulkGenerateIds) {\n return Response.json(\n {\n error: `Too many ids: ${uniqueIds.length} exceeds the maximum of ${pluginConfig.maxBulkGenerateIds} per request.`,\n },\n { status: 400 },\n )\n }\n\n // determine target locales based on config\n const locales = localesFromConfig(req.payload.config)\n const targetLocales = locales ?? [pluginConfig.locale!]\n if (!targetLocales) {\n return Response.json(\n {\n error:\n 'Could not determine target locales for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n await pMap(\n uniqueIds,\n async (id) => {\n try {\n await generateAndUpdateAltText({\n id,\n collection,\n locales: targetLocales,\n payload: req.payload,\n pluginConfig,\n req,\n })\n updatedDocs++\n console.log(\n `${updatedDocs}/${uniqueIds.length} updated (${Math.round((updatedDocs / uniqueIds.length) * 100)}%)`,\n )\n } catch (error) {\n // A Forbidden means the user has no read/update access to the\n // collection at all — it applies to every id, so fail the whole\n // request with a real 403 instead of silently listing all ids as\n // errored. Row-level NotFound stays a per-doc error (partial success).\n if (error instanceof Forbidden) {\n throw error\n }\n console.error(`Error generating alt text for ${id}:`, error)\n erroredDocs.push(id)\n }\n },\n { concurrency },\n )\n\n if (erroredDocs.length > 0) {\n console.error(`Failed for: ${erroredDocs.join(', ')}`)\n }\n\n return Response.json({\n erroredDocs,\n totalDocs: uniqueIds.length,\n updatedDocs,\n })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(formatZodError(error), { status: 400 })\n }\n // Surface Payload access errors (Forbidden 403) with their real status so\n // an agent gets an accurate, non-retryable signal instead of a 500.\n if (error instanceof APIError) {\n return Response.json({ error: error.message }, { status: error.status })\n }\n console.error('Error in bulk generation:', error)\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n\nasync function generateAndUpdateAltText({\n id,\n collection,\n locales,\n payload,\n pluginConfig,\n req,\n}: {\n collection: CollectionSlug\n id: number | string\n locales: string[]\n payload: BasePayload\n pluginConfig: AltTextPluginConfig\n req: PayloadRequest\n}) {\n const imageDoc = await payload.findByID({\n id,\n collection,\n depth: 0,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n\n if (!imageDoc) {\n throw new Error('Image not found')\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string' ? imageDoc.mimeType : undefined\n\n // The handler validates `collection` against the configured collections before\n // reaching this helper, so a matching entry is guaranteed.\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)!\n\n if (mimeType && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {\n throw new Error(\n `Alt text is not tracked for files of type \"${mimeType}\" in the \"${collection}\" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`,\n )\n }\n\n const unsupportedSourceError = getUnsupportedSourceMimeTypeError({\n declaredThumbnailMimeType: collectionConfig.imageThumbnailMimeType,\n mimeType,\n supportedMimeTypes: pluginConfig.resolver.supportedMimeTypes,\n })\n if (unsupportedSourceError) {\n throw new Error(unsupportedSourceError)\n }\n\n const imageThumbnailUrl = await pluginConfig.getImageThumbnail(imageDoc, { collection, req })\n\n const result = await pluginConfig.resolver.resolveBulk({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailMimeType: collectionConfig.imageThumbnailMimeType,\n imageThumbnailUrl,\n locales,\n req,\n })\n\n if (!result.success) {\n throw new Error(result.error || 'Failed to generate alt text')\n }\n\n for (const locale of locales) {\n const localeResult = result.results[locale]\n if (localeResult) {\n await payload.update({\n id,\n collection,\n data: {\n alt: localeResult.altText,\n keywords: localeResult.keywords,\n },\n locale,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n }\n }\n}\n"],"names":["pMap","APIError","Forbidden","ZodError","localesFromConfig","getUnsupportedSourceMimeTypeError","matchesMimeType","bulkGenerateAltTextsRequestSchema","formatZodError","bulkGenerateAltTextsEndpoint","access","req","Response","json","error","status","data","collection","ids","parse","updatedDocs","erroredDocs","pluginConfig","payload","config","custom","altTextPluginConfig","collectionConfig","collections","find","entry","slug","resolver","concurrency","maxBulkGenerateConcurrency","uniqueIds","Set","length","maxBulkGenerateIds","locales","targetLocales","locale","id","generateAndUpdateAltText","console","log","Math","round","push","join","totalDocs","message","Error","imageDoc","findByID","depth","overrideAccess","user","mimeType","undefined","mimeTypes","unsupportedSourceError","declaredThumbnailMimeType","imageThumbnailMimeType","supportedMimeTypes","imageThumbnailUrl","getImageThumbnail","result","resolveBulk","filename","success","localeResult","results","update","alt","altText","keywords"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { APIError } from 'payload';
|
|
2
2
|
import { ZodError } from 'zod';
|
|
3
|
-
import { matchesMimeType } from '../utilities/mimeTypes.js';
|
|
3
|
+
import { getUnsupportedSourceMimeTypeError, matchesMimeType } from '../utilities/mimeTypes.js';
|
|
4
4
|
import { formatZodError, generateAltTextRequestSchema } from './schemas.js';
|
|
5
5
|
/**
|
|
6
6
|
* Generates alt text for a single image using the configured resolver.
|
|
@@ -80,9 +80,14 @@ import { formatZodError, generateAltTextRequestSchema } from './schemas.js';
|
|
|
80
80
|
status: 400
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
const unsupportedSourceError = getUnsupportedSourceMimeTypeError({
|
|
84
|
+
declaredThumbnailMimeType: collectionConfig.imageThumbnailMimeType,
|
|
85
|
+
mimeType,
|
|
86
|
+
supportedMimeTypes: pluginConfig.resolver.supportedMimeTypes
|
|
87
|
+
});
|
|
88
|
+
if (unsupportedSourceError) {
|
|
84
89
|
return Response.json({
|
|
85
|
-
error:
|
|
90
|
+
error: unsupportedSourceError
|
|
86
91
|
}, {
|
|
87
92
|
status: 400
|
|
88
93
|
});
|
|
@@ -106,9 +111,13 @@ import { formatZodError, generateAltTextRequestSchema } from './schemas.js';
|
|
|
106
111
|
status: 500
|
|
107
112
|
});
|
|
108
113
|
}
|
|
109
|
-
const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc
|
|
114
|
+
const imageThumbnailUrl = await pluginConfig.getImageThumbnail(imageDoc, {
|
|
115
|
+
collection,
|
|
116
|
+
req
|
|
117
|
+
});
|
|
110
118
|
const result = await pluginConfig.resolver.resolve({
|
|
111
119
|
filename: 'filename' in imageDoc && typeof imageDoc.filename === 'string' ? imageDoc.filename : undefined,
|
|
120
|
+
imageThumbnailMimeType: collectionConfig.imageThumbnailMimeType,
|
|
112
121
|
imageThumbnailUrl,
|
|
113
122
|
locale: targetLocale,
|
|
114
123
|
req
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/generateAltText.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport { APIError } from 'payload'\nimport { ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { matchesMimeType } from '../utilities/mimeTypes.js'\nimport { formatZodError, generateAltTextRequestSchema } from './schemas.js'\n\n/**\n * Generates alt text for a single image using the configured resolver.\n *\n * By default, returns the result without updating the document (preview mode).\n * Pass `update: true` in the request body to also persist the generated alt text\n * and keywords to the document — useful for programmatic/agent workflows.\n *\n * The response always includes the `id` and `collection` for easy correlation.\n */\nexport const generateAltTextEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const { id, collection, locale, update } = generateAltTextRequestSchema.parse(data)\n\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n | AltTextPluginConfig\n | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n // Treat the configured collections as an allowlist. Reject any other\n // collection before touching the Local API, so the endpoint can only ever\n // operate on the upload collections the plugin manages.\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)\n\n if (!collectionConfig) {\n return Response.json(\n { error: `Collection \"${collection}\" is not managed by the alt text plugin.` },\n { status: 403 },\n )\n }\n\n const imageDoc = await req.payload.findByID({\n id,\n collection,\n depth: 0,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n\n if (!imageDoc) {\n return Response.json({ error: 'Image not found' }, { status: 404 })\n }\n\n if (!pluginConfig.getImageThumbnail) {\n return Response.json(\n { error: 'getImageThumbnail function not configured' },\n { status: 500 },\n )\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string'\n ? imageDoc.mimeType\n : undefined\n\n if (mimeType && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {\n return Response.json(\n {\n error: `Alt text is not tracked for files of type \"${mimeType}\" in the \"${collection}\" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n\n if (\n mimeType &&\n pluginConfig.resolver.supportedMimeTypes &&\n !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)\n ) {\n return Response.json(\n {\n error: `Alt text generation is not supported for files of type \"${mimeType}\". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n\n // When localization is enabled, the requested locale must be one of the\n // configured locales. Reject anything else before it can be written to an\n // unconfigured locale or interpolated into the resolver's prompt.\n if (\n locale != null &&\n pluginConfig.locales.length > 0 &&\n !pluginConfig.locales.includes(locale)\n ) {\n return Response.json(\n {\n error: `Locale \"${locale}\" is not configured. Configured locales: ${pluginConfig.locales.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n\n // determine target locale\n const targetLocale = locale ?? pluginConfig.locale\n if (!targetLocale) {\n return Response.json(\n {\n error:\n 'Could not determine target locale for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n const result = await pluginConfig.resolver.resolve({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailUrl,\n locale: targetLocale,\n req,\n })\n\n if (!result.success) {\n return Response.json(\n { error: result.error || 'Failed to generate alt text' },\n { status: 500 },\n )\n }\n\n if (update) {\n await req.payload.update({\n id,\n collection,\n data: {\n alt: result.result.altText,\n keywords: result.result.keywords,\n },\n locale: targetLocale,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n }\n\n return Response.json({ id, collection, ...result.result })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(formatZodError(error), { status: 400 })\n }\n // Surface Payload access errors (Forbidden 403 / NotFound 404) with their\n // real status so an agent gets an accurate, non-retryable signal instead\n // of a misleading 500.\n if (error instanceof APIError) {\n return Response.json({ error: error.message }, { status: error.status })\n }\n console.error('Error generating alt text:', error)\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n"],"names":["APIError","ZodError","matchesMimeType","formatZodError","generateAltTextRequestSchema","generateAltTextEndpoint","access","req","Response","json","error","status","data","id","collection","locale","update","parse","pluginConfig","payload","config","custom","altTextPluginConfig","collectionConfig","collections","find","entry","slug","imageDoc","findByID","depth","overrideAccess","user","getImageThumbnail","resolver","mimeType","undefined","mimeTypes","join","supportedMimeTypes","includes","locales","length","targetLocale","imageThumbnailUrl","result","resolve","filename","success","alt","altText","keywords","message","console","Error"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/generateAltText.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport { APIError } from 'payload'\nimport { ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { getUnsupportedSourceMimeTypeError, matchesMimeType } from '../utilities/mimeTypes.js'\nimport { formatZodError, generateAltTextRequestSchema } from './schemas.js'\n\n/**\n * Generates alt text for a single image using the configured resolver.\n *\n * By default, returns the result without updating the document (preview mode).\n * Pass `update: true` in the request body to also persist the generated alt text\n * and keywords to the document — useful for programmatic/agent workflows.\n *\n * The response always includes the `id` and `collection` for easy correlation.\n */\nexport const generateAltTextEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const { id, collection, locale, update } = generateAltTextRequestSchema.parse(data)\n\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n AltTextPluginConfig | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n // Treat the configured collections as an allowlist. Reject any other\n // collection before touching the Local API, so the endpoint can only ever\n // operate on the upload collections the plugin manages.\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)\n\n if (!collectionConfig) {\n return Response.json(\n { error: `Collection \"${collection}\" is not managed by the alt text plugin.` },\n { status: 403 },\n )\n }\n\n const imageDoc = await req.payload.findByID({\n id,\n collection,\n depth: 0,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n\n if (!imageDoc) {\n return Response.json({ error: 'Image not found' }, { status: 404 })\n }\n\n if (!pluginConfig.getImageThumbnail) {\n return Response.json(\n { error: 'getImageThumbnail function not configured' },\n { status: 500 },\n )\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string'\n ? imageDoc.mimeType\n : undefined\n\n if (mimeType && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {\n return Response.json(\n {\n error: `Alt text is not tracked for files of type \"${mimeType}\" in the \"${collection}\" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n\n const unsupportedSourceError = getUnsupportedSourceMimeTypeError({\n declaredThumbnailMimeType: collectionConfig.imageThumbnailMimeType,\n mimeType,\n supportedMimeTypes: pluginConfig.resolver.supportedMimeTypes,\n })\n if (unsupportedSourceError) {\n return Response.json({ error: unsupportedSourceError }, { status: 400 })\n }\n\n // When localization is enabled, the requested locale must be one of the\n // configured locales. Reject anything else before it can be written to an\n // unconfigured locale or interpolated into the resolver's prompt.\n if (\n locale != null &&\n pluginConfig.locales.length > 0 &&\n !pluginConfig.locales.includes(locale)\n ) {\n return Response.json(\n {\n error: `Locale \"${locale}\" is not configured. Configured locales: ${pluginConfig.locales.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n\n // determine target locale\n const targetLocale = locale ?? pluginConfig.locale\n if (!targetLocale) {\n return Response.json(\n {\n error:\n 'Could not determine target locale for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n const imageThumbnailUrl = await pluginConfig.getImageThumbnail(imageDoc, { collection, req })\n\n const result = await pluginConfig.resolver.resolve({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailMimeType: collectionConfig.imageThumbnailMimeType,\n imageThumbnailUrl,\n locale: targetLocale,\n req,\n })\n\n if (!result.success) {\n return Response.json(\n { error: result.error || 'Failed to generate alt text' },\n { status: 500 },\n )\n }\n\n if (update) {\n await req.payload.update({\n id,\n collection,\n data: {\n alt: result.result.altText,\n keywords: result.result.keywords,\n },\n locale: targetLocale,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n }\n\n return Response.json({ id, collection, ...result.result })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(formatZodError(error), { status: 400 })\n }\n // Surface Payload access errors (Forbidden 403 / NotFound 404) with their\n // real status so an agent gets an accurate, non-retryable signal instead\n // of a misleading 500.\n if (error instanceof APIError) {\n return Response.json({ error: error.message }, { status: error.status })\n }\n console.error('Error generating alt text:', error)\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n"],"names":["APIError","ZodError","getUnsupportedSourceMimeTypeError","matchesMimeType","formatZodError","generateAltTextRequestSchema","generateAltTextEndpoint","access","req","Response","json","error","status","data","id","collection","locale","update","parse","pluginConfig","payload","config","custom","altTextPluginConfig","collectionConfig","collections","find","entry","slug","imageDoc","findByID","depth","overrideAccess","user","getImageThumbnail","resolver","mimeType","undefined","mimeTypes","join","unsupportedSourceError","declaredThumbnailMimeType","imageThumbnailMimeType","supportedMimeTypes","locales","length","includes","targetLocale","imageThumbnailUrl","result","resolve","filename","success","alt","altText","keywords","message","console","Error"],"mappings":"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"}
|
package/dist/exports/server.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
export { AltTextHealthWidget } from '../components/AltTextHealthWidget.js';
|
|
2
|
+
/**
|
|
3
|
+
* @deprecated Import `getAltTextHealth` from the package root
|
|
4
|
+
* (`@jhb.software/payload-alt-text-plugin`) instead. This entrypoint re-exports
|
|
5
|
+
* the admin widget, which pulls in `@payloadcms/ui`/`.scss`, so importing the
|
|
6
|
+
* util from here breaks `payload generate:*` and any non-bundler consumer.
|
|
7
|
+
*/
|
|
2
8
|
export { getAltTextHealth } from '../utilities/altTextHealth.js';
|
|
3
9
|
export type { AltTextHealthError, AltTextHealthErrorCode, AltTextHealthScan, AltTextHealthScanCollection, } from '../utilities/altTextHealth.js';
|
package/dist/exports/server.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export { AltTextHealthWidget } from '../components/AltTextHealthWidget.js';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* @deprecated Import `getAltTextHealth` from the package root
|
|
4
|
+
* (`@jhb.software/payload-alt-text-plugin`) instead. This entrypoint re-exports
|
|
5
|
+
* the admin widget, which pulls in `@payloadcms/ui`/`.scss`, so importing the
|
|
6
|
+
* util from here breaks `payload generate:*` and any non-bundler consumer.
|
|
7
|
+
*/ export { getAltTextHealth } from '../utilities/altTextHealth.js';
|
|
3
8
|
|
|
4
9
|
//# sourceMappingURL=server.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/exports/server.ts"],"sourcesContent":["export { AltTextHealthWidget } from '../components/AltTextHealthWidget.js'\nexport { getAltTextHealth } from '../utilities/altTextHealth.js'\nexport type {\n AltTextHealthError,\n AltTextHealthErrorCode,\n AltTextHealthScan,\n AltTextHealthScanCollection,\n} from '../utilities/altTextHealth.js'\n"],"names":["AltTextHealthWidget","getAltTextHealth"],"mappings":"AAAA,SAASA,mBAAmB,QAAQ,uCAAsC;
|
|
1
|
+
{"version":3,"sources":["../../src/exports/server.ts"],"sourcesContent":["export { AltTextHealthWidget } from '../components/AltTextHealthWidget.js'\n\n/**\n * @deprecated Import `getAltTextHealth` from the package root\n * (`@jhb.software/payload-alt-text-plugin`) instead. This entrypoint re-exports\n * the admin widget, which pulls in `@payloadcms/ui`/`.scss`, so importing the\n * util from here breaks `payload generate:*` and any non-bundler consumer.\n */\nexport { getAltTextHealth } from '../utilities/altTextHealth.js'\nexport type {\n AltTextHealthError,\n AltTextHealthErrorCode,\n AltTextHealthScan,\n AltTextHealthScanCollection,\n} from '../utilities/altTextHealth.js'\n"],"names":["AltTextHealthWidget","getAltTextHealth"],"mappings":"AAAA,SAASA,mBAAmB,QAAQ,uCAAsC;AAE1E;;;;;CAKC,GACD,SAASC,gBAAgB,QAAQ,gCAA+B"}
|
|
@@ -1,24 +1,38 @@
|
|
|
1
1
|
import { revalidateTag } from 'next/cache.js';
|
|
2
|
+
import { after } from 'next/server.js';
|
|
2
3
|
import { ALT_TEXT_HEALTH_PLUGIN_SLUG, getAltTextHealthCollectionTag } from '../utilities/altTextHealth.js';
|
|
3
4
|
function safeRevalidateTag(req, tag) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
5
|
+
const runRevalidate = ()=>{
|
|
6
|
+
try {
|
|
7
|
+
// Support both Next 15 and Next 16. Next 15 types `revalidateTag(tag)` as 1-arg; Next 16
|
|
8
|
+
// added a required second `profile` arg and logs a deprecation warning for 1-arg calls.
|
|
9
|
+
// Passing 'max' satisfies Next 16 and is ignored at runtime by Next 15. The cast lets the
|
|
10
|
+
// build succeed regardless of which Next types are resolved from the consuming project.
|
|
11
|
+
;
|
|
12
|
+
revalidateTag(tag, 'max');
|
|
13
|
+
} catch (error) {
|
|
14
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15
|
+
if (message.includes('static generation store missing')) {
|
|
16
|
+
req.payload.logger.warn({
|
|
17
|
+
msg: 'Skipping alt text health cache revalidation outside a Next.js request context.',
|
|
18
|
+
plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,
|
|
19
|
+
tag
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
throw error;
|
|
20
24
|
}
|
|
21
|
-
|
|
25
|
+
};
|
|
26
|
+
try {
|
|
27
|
+
// Defer via `after()` so the call escapes the current render scope.
|
|
28
|
+
// Next.js disallows synchronous `revalidateTag` from inside a server-component
|
|
29
|
+
// render — relevant when users seed via `payload.create` from `onInit`,
|
|
30
|
+
// which runs while the admin route is rendering.
|
|
31
|
+
after(runRevalidate);
|
|
32
|
+
} catch {
|
|
33
|
+
// No request scope (CLI / migrations / scripts). Run inline; the inner
|
|
34
|
+
// `try/catch` will warn-and-skip if Next.js itself has no context either.
|
|
35
|
+
runRevalidate();
|
|
22
36
|
}
|
|
23
37
|
}
|
|
24
38
|
export const createRevalidateAltTextHealthAfterChangeHook = (collectionSlug)=>({ doc, req })=>{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/revalidateAltTextHealth.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, PayloadRequest } from 'payload'\n\nimport { revalidateTag } from 'next/cache.js'\n\nimport {\n ALT_TEXT_HEALTH_PLUGIN_SLUG,\n getAltTextHealthCollectionTag,\n} from '../utilities/altTextHealth.js'\n\nfunction safeRevalidateTag(req: PayloadRequest, tag: string): void {\n try {\n
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/revalidateAltTextHealth.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, PayloadRequest } from 'payload'\n\nimport { revalidateTag } from 'next/cache.js'\nimport { after } from 'next/server.js'\n\nimport {\n ALT_TEXT_HEALTH_PLUGIN_SLUG,\n getAltTextHealthCollectionTag,\n} from '../utilities/altTextHealth.js'\n\nfunction safeRevalidateTag(req: PayloadRequest, tag: string): void {\n const runRevalidate = (): void => {\n try {\n // Support both Next 15 and Next 16. Next 15 types `revalidateTag(tag)` as 1-arg; Next 16\n // added a required second `profile` arg and logs a deprecation warning for 1-arg calls.\n // Passing 'max' satisfies Next 16 and is ignored at runtime by Next 15. The cast lets the\n // build succeed regardless of which Next types are resolved from the consuming project.\n ;(revalidateTag as (tag: string, profile?: string) => void)(tag, 'max')\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n\n if (message.includes('static generation store missing')) {\n req.payload.logger.warn({\n msg: 'Skipping alt text health cache revalidation outside a Next.js request context.',\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n tag,\n })\n return\n }\n\n throw error\n }\n }\n\n try {\n // Defer via `after()` so the call escapes the current render scope.\n // Next.js disallows synchronous `revalidateTag` from inside a server-component\n // render — relevant when users seed via `payload.create` from `onInit`,\n // which runs while the admin route is rendering.\n after(runRevalidate)\n } catch {\n // No request scope (CLI / migrations / scripts). Run inline; the inner\n // `try/catch` will warn-and-skip if Next.js itself has no context either.\n runRevalidate()\n }\n}\n\nexport const createRevalidateAltTextHealthAfterChangeHook =\n (collectionSlug: string): CollectionAfterChangeHook =>\n ({ doc, req }) => {\n if (!req.context?.disableRevalidate) {\n safeRevalidateTag(req, getAltTextHealthCollectionTag(collectionSlug))\n }\n\n return doc\n }\n\nexport const createRevalidateAltTextHealthAfterDeleteHook =\n (collectionSlug: string): CollectionAfterDeleteHook =>\n ({ doc, req }) => {\n if (!req.context?.disableRevalidate) {\n safeRevalidateTag(req, getAltTextHealthCollectionTag(collectionSlug))\n }\n\n return doc\n }\n"],"names":["revalidateTag","after","ALT_TEXT_HEALTH_PLUGIN_SLUG","getAltTextHealthCollectionTag","safeRevalidateTag","req","tag","runRevalidate","error","message","Error","String","includes","payload","logger","warn","msg","plugin","createRevalidateAltTextHealthAfterChangeHook","collectionSlug","doc","context","disableRevalidate","createRevalidateAltTextHealthAfterDeleteHook"],"mappings":"AAEA,SAASA,aAAa,QAAQ,gBAAe;AAC7C,SAASC,KAAK,QAAQ,iBAAgB;AAEtC,SACEC,2BAA2B,EAC3BC,6BAA6B,QACxB,gCAA+B;AAEtC,SAASC,kBAAkBC,GAAmB,EAAEC,GAAW;IACzD,MAAMC,gBAAgB;QACpB,IAAI;YACF,yFAAyF;YACzF,wFAAwF;YACxF,0FAA0F;YAC1F,wFAAwF;;YACtFP,cAA0DM,KAAK;QACnE,EAAE,OAAOE,OAAO;YACd,MAAMC,UAAUD,iBAAiBE,QAAQF,MAAMC,OAAO,GAAGE,OAAOH;YAEhE,IAAIC,QAAQG,QAAQ,CAAC,oCAAoC;gBACvDP,IAAIQ,OAAO,CAACC,MAAM,CAACC,IAAI,CAAC;oBACtBC,KAAK;oBACLC,QAAQf;oBACRI;gBACF;gBACA;YACF;YAEA,MAAME;QACR;IACF;IAEA,IAAI;QACF,oEAAoE;QACpE,+EAA+E;QAC/E,wEAAwE;QACxE,iDAAiD;QACjDP,MAAMM;IACR,EAAE,OAAM;QACN,uEAAuE;QACvE,0EAA0E;QAC1EA;IACF;AACF;AAEA,OAAO,MAAMW,+CACX,CAACC,iBACD,CAAC,EAAEC,GAAG,EAAEf,GAAG,EAAE;QACX,IAAI,CAACA,IAAIgB,OAAO,EAAEC,mBAAmB;YACnClB,kBAAkBC,KAAKF,8BAA8BgB;QACvD;QAEA,OAAOC;IACT,EAAC;AAEH,OAAO,MAAMG,+CACX,CAACJ,iBACD,CAAC,EAAEC,GAAG,EAAEf,GAAG,EAAE;QACX,IAAI,CAACA,IAAIgB,OAAO,EAAEC,mBAAmB;YACnClB,kBAAkBC,KAAKF,8BAA8BgB;QACvD;QAEA,OAAOC;IACT,EAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export { payloadAltTextPlugin } from './plugin.js';
|
|
2
|
+
export { mistralResolver } from './resolvers/mistral.js';
|
|
3
|
+
export type { MistralResolverConfig } from './resolvers/mistral.js';
|
|
2
4
|
export { openAIResolver } from './resolvers/openAI.js';
|
|
3
5
|
export * from './resolvers/types.js';
|
|
4
|
-
export type { AltTextCollectionConfig, IncomingAltTextPluginConfig as AltTextPluginConfig, } from './types/AltTextPluginConfig.js';
|
|
6
|
+
export type { AltTextCollectionConfig, GetImageThumbnail, IncomingAltTextPluginConfig as AltTextPluginConfig, } from './types/AltTextPluginConfig.js';
|
|
7
|
+
export { getAltTextHealth } from './utilities/altTextHealth.js';
|
|
8
|
+
export type { AltTextHealthError, AltTextHealthErrorCode, AltTextHealthScan, AltTextHealthScanCollection, } from './utilities/altTextHealth.js';
|
|
5
9
|
export { matchesMimeType, validateAltText } from './utilities/mimeTypes.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { payloadAltTextPlugin } from './plugin.js';
|
|
2
|
+
export { mistralResolver } from './resolvers/mistral.js';
|
|
2
3
|
export { openAIResolver } from './resolvers/openAI.js';
|
|
3
4
|
export * from './resolvers/types.js';
|
|
5
|
+
export { getAltTextHealth } from './utilities/altTextHealth.js';
|
|
4
6
|
export { matchesMimeType, validateAltText } from './utilities/mimeTypes.js';
|
|
5
7
|
|
|
6
8
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { payloadAltTextPlugin } from './plugin.js'\nexport { openAIResolver } from './resolvers/openAI.js'\nexport * from './resolvers/types.js'\nexport type {\n AltTextCollectionConfig,\n IncomingAltTextPluginConfig as AltTextPluginConfig,\n} from './types/AltTextPluginConfig.js'\nexport { matchesMimeType, validateAltText } from './utilities/mimeTypes.js'\n"],"names":["payloadAltTextPlugin","openAIResolver","matchesMimeType","validateAltText"],"mappings":"AAAA,SAASA,oBAAoB,QAAQ,cAAa;AAClD,SAASC,cAAc,QAAQ,wBAAuB;AACtD,cAAc,uBAAsB;
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { payloadAltTextPlugin } from './plugin.js'\nexport { mistralResolver } from './resolvers/mistral.js'\nexport type { MistralResolverConfig } from './resolvers/mistral.js'\nexport { openAIResolver } from './resolvers/openAI.js'\nexport * from './resolvers/types.js'\nexport type {\n AltTextCollectionConfig,\n GetImageThumbnail,\n IncomingAltTextPluginConfig as AltTextPluginConfig,\n} from './types/AltTextPluginConfig.js'\nexport { getAltTextHealth } from './utilities/altTextHealth.js'\nexport type {\n AltTextHealthError,\n AltTextHealthErrorCode,\n AltTextHealthScan,\n AltTextHealthScanCollection,\n} from './utilities/altTextHealth.js'\nexport { matchesMimeType, validateAltText } from './utilities/mimeTypes.js'\n"],"names":["payloadAltTextPlugin","mistralResolver","openAIResolver","getAltTextHealth","matchesMimeType","validateAltText"],"mappings":"AAAA,SAASA,oBAAoB,QAAQ,cAAa;AAClD,SAASC,eAAe,QAAQ,yBAAwB;AAExD,SAASC,cAAc,QAAQ,wBAAuB;AACtD,cAAc,uBAAsB;AAMpC,SAASC,gBAAgB,QAAQ,+BAA8B;AAO/D,SAASC,eAAe,EAAEC,eAAe,QAAQ,2BAA0B"}
|