@focus-reactive/payload-plugin-seo 1.0.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 +336 -0
- package/package.json +106 -0
package/README.md
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# @focus-reactive/payload-plugin-seo
|
|
2
|
+
|
|
3
|
+
Live SEO analysis for [Payload CMS](https://payloadcms.com/) v3 + Next.js, powered by [Yoast](https://github.com/Yoast/wordpress-seo). Adds a real-time SEO drawer to the document editor — keyphrase optimization, on-page checks, readability, inclusive language, content vitals, and a Google SERP preview — without adding a single field to your database.
|
|
4
|
+
|
|
5
|
+
The plugin injects a button into the editor toolbar of each configured collection. Clicking it opens a drawer that reads the current (unsaved) form values, extracts the title, meta description, slug, body content, and images, and runs the Yoast analysis engine **entirely in the browser**. Nothing is persisted — there are zero new collections, globals, or fields.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## AI Integration Prompt
|
|
10
|
+
|
|
11
|
+
> Copy and paste this prompt into your AI assistant (Cursor, Claude, etc.) to integrate the plugin into an existing Payload + Next.js project.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
I want to add live SEO analysis to my Payload CMS v3 + Next.js project using @focus-reactive/payload-plugin-seo.
|
|
15
|
+
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
The plugin adds NO database fields, collections, or globals. It injects a button into the
|
|
19
|
+
document editor toolbar (admin.components.edit.beforeDocumentControls) of each configured
|
|
20
|
+
collection. The button opens a drawer that:
|
|
21
|
+
- Reads the live (unsaved) form values for the document
|
|
22
|
+
- Extracts title, meta description, slug, body content and images using dot-path config
|
|
23
|
+
- Resolves upload/relationship media into <img> tags via the Payload REST API
|
|
24
|
+
- Runs the Yoast engine (yoastseo + @yoast/search-metadata-previews) in the browser
|
|
25
|
+
- Shows tabs: Keyphrase, On-page SEO, Readability, Inclusive, Content vitals, SERP preview
|
|
26
|
+
|
|
27
|
+
It works with or without a focus keyphrase; keyphrase-specific checks unlock once you enter one.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
pnpm add @focus-reactive/payload-plugin-seo
|
|
32
|
+
|
|
33
|
+
## Step 1 — Register the plugin in payload.config.ts
|
|
34
|
+
|
|
35
|
+
import { seoPlugin } from '@focus-reactive/payload-plugin-seo'
|
|
36
|
+
|
|
37
|
+
// Inside buildConfig({ plugins: [...] })
|
|
38
|
+
seoPlugin({
|
|
39
|
+
collections: [
|
|
40
|
+
{
|
|
41
|
+
slug: 'pages',
|
|
42
|
+
fields: {
|
|
43
|
+
seoTitle: 'seoTitle', // dot-path; falls back to useAsTitle / 'title'
|
|
44
|
+
metaDescription: 'metaDescription',
|
|
45
|
+
slug: 'slug', // default: 'slug'
|
|
46
|
+
content: 'sections', // dot-path to the main content field (blocks/richText/textarea)
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
site: { name: 'My Site', baseUrl: 'https://example.com', faviconUrl: '/favicon.ico' },
|
|
51
|
+
supportedLocales: ['en'], // language packs to load; default ['en']
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
## Step 2 — Import the admin styles
|
|
55
|
+
|
|
56
|
+
In your Payload admin CSS (e.g. app/(payload)/custom.scss):
|
|
57
|
+
|
|
58
|
+
@import "@focus-reactive/payload-plugin-seo/admin.css";
|
|
59
|
+
|
|
60
|
+
## Step 3 — Allow Next.js to transpile the Yoast UI packages
|
|
61
|
+
|
|
62
|
+
In next.config.mjs:
|
|
63
|
+
|
|
64
|
+
const nextConfig = {
|
|
65
|
+
transpilePackages: ['@yoast/search-metadata-previews', '@yoast/components'],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
## Important notes
|
|
69
|
+
|
|
70
|
+
- The plugin reads UNSAVED form values, so analysis updates live as you type (debounced ~1s).
|
|
71
|
+
- `fields.content` should point at your primary body field. The built-in extractor walks
|
|
72
|
+
blocks, arrays, groups, tabs, lexical richText, and uploads, converting them to HTML.
|
|
73
|
+
- If the built-in extractor can't reach your content shape, supply `extractContentPath`:
|
|
74
|
+
an importMap module path to a `(formValues) => string | Promise<string>` returning HTML.
|
|
75
|
+
- Non-English analysis requires the locale code in `supportedLocales`; the matching Yoast
|
|
76
|
+
language pack is dynamically imported on demand.
|
|
77
|
+
- No GA4, no API keys, no server calls except resolving media URLs from your own Payload API.
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## How It Works
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
Document editor (configured collection)
|
|
86
|
+
│
|
|
87
|
+
▼
|
|
88
|
+
[ SeoButton ] ← injected into admin.components.edit.beforeDocumentControls
|
|
89
|
+
│ click
|
|
90
|
+
▼
|
|
91
|
+
SEO Drawer (client-only)
|
|
92
|
+
│
|
|
93
|
+
├─ read live form values (title, description, slug, content, keyphrase)
|
|
94
|
+
├─ collect upload/relationship refs → resolve via /api/{collection}?depth=0&locale=…
|
|
95
|
+
├─ hydrate values → walk tree → build HTML (lexical → HTML, images → <img>)
|
|
96
|
+
▼
|
|
97
|
+
Yoast engine (in browser): Paper + EnglishResearcher + SeoAssessor
|
|
98
|
+
│
|
|
99
|
+
▼
|
|
100
|
+
Tabs: Keyphrase · On-page SEO · Readability · Inclusive · Content vitals · SERP preview
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The analysis runs on a ~1 second debounce as form values change. **No data is written** — the drawer is a pure read-only overlay on top of the editor's current state.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Installation
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pnpm add @focus-reactive/payload-plugin-seo
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Peer dependencies:** `payload ^3.0.0`, `@payloadcms/next ^3.0.0`, `@payloadcms/ui ^3.0.0`, `@payloadcms/richtext-lexical ^3.0.0`, `lucide-react ^0.469.0`. `next ^14 || ^15` and `react`/`react-dom ^18 || ^19` are optional peers.
|
|
114
|
+
|
|
115
|
+
The Yoast engine (`yoastseo`, `@yoast/search-metadata-previews`) ships as a direct dependency — you don't install it yourself, but you do need to transpile the two UI packages (see Quick Start step 3).
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Quick Start
|
|
120
|
+
|
|
121
|
+
### Step 1 — Register the plugin
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// payload.config.ts
|
|
125
|
+
import { buildConfig } from "payload";
|
|
126
|
+
import { seoPlugin } from "@focus-reactive/payload-plugin-seo";
|
|
127
|
+
|
|
128
|
+
export default buildConfig({
|
|
129
|
+
plugins: [
|
|
130
|
+
seoPlugin({
|
|
131
|
+
collections: [
|
|
132
|
+
{
|
|
133
|
+
slug: "pages",
|
|
134
|
+
fields: {
|
|
135
|
+
seoTitle: "seoTitle",
|
|
136
|
+
metaDescription: "metaDescription",
|
|
137
|
+
slug: "slug",
|
|
138
|
+
content: "sections",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
site: {
|
|
143
|
+
name: "My Site",
|
|
144
|
+
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL ?? "http://localhost:3000",
|
|
145
|
+
},
|
|
146
|
+
supportedLocales: ["en", "de", "fr", "es"],
|
|
147
|
+
}),
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
This injects the **SEO** button into the document toolbar of every configured collection. A colored dot on the button reflects the current overall status (good / warn / bad).
|
|
153
|
+
|
|
154
|
+
### Step 2 — Import the admin styles
|
|
155
|
+
|
|
156
|
+
The drawer's components import their compiled CSS internally, but the package also ships it at `./admin.css` so you can include it explicitly in your admin stylesheet:
|
|
157
|
+
|
|
158
|
+
```scss
|
|
159
|
+
/* app/(payload)/custom.scss */
|
|
160
|
+
@import "@focus-reactive/payload-plugin-seo/admin.css";
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Step 3 — Transpile the Yoast UI packages
|
|
164
|
+
|
|
165
|
+
`@yoast/search-metadata-previews` and `@yoast/components` ship CSS inside `node_modules`, which Next.js (and Turbopack) won't process unless they're listed in `transpilePackages`:
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
// next.config.mjs
|
|
169
|
+
/** @type {import('next').NextConfig} */
|
|
170
|
+
const nextConfig = {
|
|
171
|
+
transpilePackages: ["@yoast/search-metadata-previews", "@yoast/components"],
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export default nextConfig;
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Configuration Reference
|
|
180
|
+
|
|
181
|
+
### Plugin Options
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
interface SeoPluginConfig {
|
|
185
|
+
/** Skip injection entirely. Default: false */
|
|
186
|
+
disabled?: boolean;
|
|
187
|
+
/** Collections to attach the SEO drawer to. At least one is required. */
|
|
188
|
+
collections: SeoCollectionConfig[];
|
|
189
|
+
/** Site identity used in the SERP preview and permalink. */
|
|
190
|
+
site?: SeoSiteConfig;
|
|
191
|
+
/** Locale codes whose Yoast language packs may be loaded. Default: ['en'] */
|
|
192
|
+
supportedLocales?: string[];
|
|
193
|
+
/** Override the English UI strings (merged with defaults). */
|
|
194
|
+
translations?: Translations;
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### SeoCollectionConfig
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
interface SeoCollectionConfig {
|
|
202
|
+
/** Collection slug to attach the drawer to. */
|
|
203
|
+
slug: string;
|
|
204
|
+
/** Dot-paths telling the plugin which fields hold the SEO inputs. */
|
|
205
|
+
fields?: SeoFieldPaths;
|
|
206
|
+
/**
|
|
207
|
+
* importMap module-path to a custom client extractor
|
|
208
|
+
* `(formData) => string | Promise<string>` returning HTML.
|
|
209
|
+
* Example: "@/seo/my-extractor#default".
|
|
210
|
+
* Default: built-in smart extractor.
|
|
211
|
+
*/
|
|
212
|
+
extractContentPath?: string;
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### SeoFieldPaths
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
interface SeoFieldPaths {
|
|
220
|
+
/** Dot-path to the SEO title. Falls back to the collection's useAsTitle / `title`. */
|
|
221
|
+
seoTitle?: string;
|
|
222
|
+
/** Dot-path to the meta description. Absent → meta-description checks are disabled
|
|
223
|
+
* and the SERP snippet shows no description. */
|
|
224
|
+
metaDescription?: string;
|
|
225
|
+
/** Dot-path to the slug. Default: 'slug' */
|
|
226
|
+
slug?: string;
|
|
227
|
+
/** Dot-path to the primary content field (blocks / richText / textarea). */
|
|
228
|
+
content?: string;
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Dot-paths support nesting, e.g. `"meta.description"` or `"content.body"`.
|
|
233
|
+
|
|
234
|
+
### SeoSiteConfig
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
interface SeoSiteConfig {
|
|
238
|
+
/** Site name shown in the SERP preview. */
|
|
239
|
+
name?: string;
|
|
240
|
+
/** Base URL used to build the permalink in the SERP preview. */
|
|
241
|
+
baseUrl?: string;
|
|
242
|
+
/** Favicon shown in the SERP preview. */
|
|
243
|
+
faviconUrl?: string;
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Custom content extractor
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
type ExtractorFn = (data: Record<string, unknown>) => string | Promise<string>;
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Provide one when the built-in extractor can't reconstruct your content shape. It receives the raw (unhydrated) form values and must return an HTML string. Reference it from config by importMap path:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
// payload.config.ts
|
|
257
|
+
seoPlugin({
|
|
258
|
+
collections: [
|
|
259
|
+
{ slug: "pages", extractContentPath: "@/seo/my-extractor#default" },
|
|
260
|
+
],
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
// src/seo/my-extractor.ts
|
|
266
|
+
export default function extractContent(data: Record<string, unknown>): string {
|
|
267
|
+
return `<h1>${data.title}</h1><p>${data.body}</p>`;
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Content Extraction
|
|
274
|
+
|
|
275
|
+
When `extractContentPath` is **not** set, the plugin's built-in extractor:
|
|
276
|
+
|
|
277
|
+
1. **Reads** the value at `fields.content` from the live form values.
|
|
278
|
+
2. **Collects upload / relationship references** by walking the form schema (arrays, blocks, groups, tabs, rows, collapsibles, and lexical richText, including inline media nodes).
|
|
279
|
+
3. **Resolves media** by calling your Payload REST API per collection:
|
|
280
|
+
`GET /api/{collection}?depth=0&locale={locale}&where[id][in][]=…` — fetching each doc's `url`, `mimeType`, and `alt`. Results are cached in-memory and invalidated when the drawer re-opens or content changes.
|
|
281
|
+
4. **Hydrates** the value tree (upload IDs → full docs) and walks it to build HTML:
|
|
282
|
+
- Lexical richText → HTML via `@payloadcms/richtext-lexical/html`
|
|
283
|
+
- `{ url, mimeType: "image/*", alt? }` → `<img src="…" alt="…" />`
|
|
284
|
+
- `{ url, label | text | title }` → `<a href="…">…</a>`
|
|
285
|
+
- Strings → `<p>…</p>`
|
|
286
|
+
- Structural keys (`id`, `blockType`, `blockName`, `_template`, `order`) are skipped
|
|
287
|
+
|
|
288
|
+
This means image checks (alt text, keyphrase in alt, image count) work against the real, resolved media — not raw relationship IDs.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## The Analysis Drawer
|
|
293
|
+
|
|
294
|
+
The drawer presents six tabs, all derived from a single in-browser Yoast analysis pass (a `Paper` analyzed by `SeoAssessor` with the language-appropriate `Researcher`):
|
|
295
|
+
|
|
296
|
+
| Tab | What it checks |
|
|
297
|
+
| --------------------- | ------------------------------------------------------------------------------ |
|
|
298
|
+
| **Keyphrase** | Focus keyphrase usage — in title, slug, meta description, first paragraph, density, image alt, synonyms. Enter a keyphrase to unlock these checks. |
|
|
299
|
+
| **On-page SEO** | Title width, meta description presence/length, internal & outbound links, heading structure. |
|
|
300
|
+
| **Readability** | Sentence/paragraph length, transition words, passive voice, consecutive sentences. |
|
|
301
|
+
| **Inclusive** | Flags potentially exclusionary or non-inclusive language. |
|
|
302
|
+
| **Content vitals** | Word count, sentence/paragraph counts, image & video counts, reading time, prominent words. |
|
|
303
|
+
| **Search result preview** | Live Google SERP preview (desktop + mobile) with keyphrase highlighting, built on `@yoast/search-metadata-previews`. |
|
|
304
|
+
|
|
305
|
+
**Without a keyphrase:** the drawer still runs and the On-page, Readability, Inclusive, Content vitals, and SERP tabs all populate. Only the keyphrase-specific assessments wait until you type a focus keyphrase and analysis runs.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Localization
|
|
310
|
+
|
|
311
|
+
`supportedLocales` lists which locale codes the drawer may load Yoast language packs for. English is built in; other languages are dynamically imported on demand the first time the document is edited in that locale:
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
seoPlugin({
|
|
315
|
+
collections: [{ slug: "pages", fields: { content: "sections" } }],
|
|
316
|
+
supportedLocales: ["en", "de", "fr", "es"],
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The active locale is taken from the admin and normalized to Yoast's `xx_XX` form (e.g. `en` → `en_EN`). Media is resolved per-locale so localized URLs and alt text are analyzed correctly. A locale not listed in `supportedLocales` falls back to English processing.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Exports Reference
|
|
325
|
+
|
|
326
|
+
| Import path | Exports |
|
|
327
|
+
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
|
|
328
|
+
| `@focus-reactive/payload-plugin-seo` | `seoPlugin`, and types `SeoPluginConfig`, `SeoCollectionConfig`, `SeoFieldPaths`, `SeoSiteConfig`, `ExtractorFn` |
|
|
329
|
+
| `@focus-reactive/payload-plugin-seo/admin.css` | Compiled admin styles for the drawer & button |
|
|
330
|
+
| `@focus-reactive/payload-plugin-seo/components/SeoButton` | `SeoButton` — the toolbar button component (wired automatically by the plugin via the importMap; you normally never import this directly) |
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## License
|
|
335
|
+
|
|
336
|
+
MIT © FocusReactive
|
package/package.json
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@focus-reactive/payload-plugin-seo",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"author": "FocusReactive <ship@focusreactive.com>",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"payload",
|
|
8
|
+
"payloadcms",
|
|
9
|
+
"payload-plugin",
|
|
10
|
+
"seo",
|
|
11
|
+
"yoast",
|
|
12
|
+
"cms",
|
|
13
|
+
"plugin",
|
|
14
|
+
"react",
|
|
15
|
+
"nextjs",
|
|
16
|
+
"typescript"
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/focusreactive/payload-plugins",
|
|
21
|
+
"directory": "packages/payload-plugin-seo"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"sideEffects": [
|
|
30
|
+
"**/*.css"
|
|
31
|
+
],
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"import": "./dist/index.js",
|
|
38
|
+
"types": "./dist/index.d.ts"
|
|
39
|
+
},
|
|
40
|
+
"./admin.css": "./dist/admin.css",
|
|
41
|
+
"./components/SeoButton": {
|
|
42
|
+
"import": "./dist/components/SeoButton/index.js",
|
|
43
|
+
"types": "./dist/components/SeoButton/index.d.ts"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup && tsc --emitDeclarationOnly --declarationMap",
|
|
48
|
+
"dev": "tsup --watch",
|
|
49
|
+
"lint": "eslint src/ tests/",
|
|
50
|
+
"lint:fix": "eslint src/ tests/ --fix",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:watch": "vitest"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@payloadcms/next": "^3.0.0",
|
|
56
|
+
"@payloadcms/richtext-lexical": "^3.0.0",
|
|
57
|
+
"@payloadcms/ui": "^3.0.0",
|
|
58
|
+
"lucide-react": "^0.469.0",
|
|
59
|
+
"next": "^14.0.0 || ^15.0.0",
|
|
60
|
+
"payload": "^3.0.0",
|
|
61
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
62
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"next": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"react": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"react-dom": {
|
|
72
|
+
"optional": true
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"@yoast/search-metadata-previews": "^3.0.0",
|
|
77
|
+
"class-variance-authority": "0.7.1",
|
|
78
|
+
"clsx": "^2.1.1",
|
|
79
|
+
"tailwind-merge": "^3.6.0",
|
|
80
|
+
"yoastseo": "^3.6.0"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@payloadcms/next": "3.84.1",
|
|
84
|
+
"@payloadcms/richtext-lexical": "3.84.1",
|
|
85
|
+
"@payloadcms/ui": "3.84.1",
|
|
86
|
+
"@tailwindcss/postcss": "^4.0.0",
|
|
87
|
+
"@types/node": "^25.7.0",
|
|
88
|
+
"@types/react": "19.2.9",
|
|
89
|
+
"@types/react-dom": "19.2.3",
|
|
90
|
+
"eslint": "^9.18.0",
|
|
91
|
+
"eslint-config-prettier": "9.0.0",
|
|
92
|
+
"lucide-react": "^0.469.0",
|
|
93
|
+
"next": "15.4.10",
|
|
94
|
+
"payload": "3.84.1",
|
|
95
|
+
"postcss": "^8.5.1",
|
|
96
|
+
"postcss-cli": "^11.0.0",
|
|
97
|
+
"prettier": "3.0.0",
|
|
98
|
+
"react": "^19.0.0",
|
|
99
|
+
"react-dom": "^19.0.0",
|
|
100
|
+
"tailwindcss": "^4.0.0",
|
|
101
|
+
"tsup": "8.0.0",
|
|
102
|
+
"typescript": "5.7.3",
|
|
103
|
+
"typescript-eslint": "^8.20.0",
|
|
104
|
+
"vitest": "^2.1.8"
|
|
105
|
+
}
|
|
106
|
+
}
|