@vitrailweb/payload-plugin-articles 1.0.1
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 +70 -0
- package/dist/collections/Articles.d.ts +18 -0
- package/dist/collections/Articles.js +114 -0
- package/dist/collections/Articles.js.map +1 -0
- package/dist/defaults.d.ts +10 -0
- package/dist/defaults.js +36 -0
- package/dist/defaults.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/translations/en.d.ts +14 -0
- package/dist/translations/en.js +16 -0
- package/dist/translations/en.js.map +1 -0
- package/dist/translations/fr.d.ts +2 -0
- package/dist/translations/fr.js +16 -0
- package/dist/translations/fr.js.map +1 -0
- package/dist/translations/index.d.ts +4 -0
- package/dist/translations/index.js +8 -0
- package/dist/translations/index.js.map +1 -0
- package/package.json +89 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# payload-vw-articles
|
|
2
|
+
|
|
3
|
+
A [Payload CMS](https://payloadcms.com) plugin that adds an `articles` collection with drafts (autosave), live preview, and SEO fields from `@payloadcms/plugin-seo`.
|
|
4
|
+
|
|
5
|
+
## Fields
|
|
6
|
+
|
|
7
|
+
| Field | Type | Notes |
|
|
8
|
+
| ------------- | ---------- | ----------------------------------------- |
|
|
9
|
+
| `title` | `text` | required, used as admin title |
|
|
10
|
+
| `slug` | `text` | auto-generated from title, unique |
|
|
11
|
+
| `coverImage` | `upload` | relates to `media` |
|
|
12
|
+
| `content` | `richText` | |
|
|
13
|
+
| `publishedAt` | `date` | auto-set on first publish |
|
|
14
|
+
| `meta` | `group` | SEO title/description/image/preview |
|
|
15
|
+
|
|
16
|
+
> Requires a `media` upload collection in the host config.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { buildConfig } from 'payload'
|
|
22
|
+
import { VWPayloadPluginArticles } from 'payload-vw-articles'
|
|
23
|
+
|
|
24
|
+
export default buildConfig({
|
|
25
|
+
plugins: [VWPayloadPluginArticles()],
|
|
26
|
+
// ...
|
|
27
|
+
})
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Options
|
|
31
|
+
|
|
32
|
+
All optional — defaults shown as comments:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
VWPayloadPluginArticles({
|
|
36
|
+
// Access per operation. Defaults: read = published or authenticated,
|
|
37
|
+
// create/update/delete = authenticated.
|
|
38
|
+
access: { read, create, update, delete },
|
|
39
|
+
|
|
40
|
+
// Front-end URL of an article, used for (live) preview and SEO.
|
|
41
|
+
// Default: `${NEXT_PUBLIC_SERVER_URL}/articles/${slug}`
|
|
42
|
+
articleUrl: (slug) => string,
|
|
43
|
+
|
|
44
|
+
// SEO meta group + generate endpoints. `true` (default) uses built-in
|
|
45
|
+
// generate functions; pass an object to override any of them; `false` disables.
|
|
46
|
+
seo: { generateTitle, generateDescription, generateImage, generateURL },
|
|
47
|
+
|
|
48
|
+
// Keeps the collection schema but disables runtime behavior (default: false).
|
|
49
|
+
disabled: false,
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Development
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pnpm install
|
|
57
|
+
pnpm dev # start the dev Payload app
|
|
58
|
+
pnpm test:int # integration tests (vitest)
|
|
59
|
+
pnpm test:e2e # e2e tests (playwright)
|
|
60
|
+
pnpm build # build to dist/
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Test locally
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# commit everything
|
|
67
|
+
npm version patch # or minor/major
|
|
68
|
+
pnpm clean && pnpm build
|
|
69
|
+
pnpm pack
|
|
70
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Access, CollectionConfig } from 'payload';
|
|
2
|
+
export type ArticlesAccess = {
|
|
3
|
+
create?: Access;
|
|
4
|
+
delete?: Access;
|
|
5
|
+
read?: Access;
|
|
6
|
+
update?: Access;
|
|
7
|
+
};
|
|
8
|
+
export type ArticlesSeoGenerators = {
|
|
9
|
+
hasGenerateDescription: boolean;
|
|
10
|
+
hasGenerateImage: boolean;
|
|
11
|
+
hasGenerateTitle: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type ArticlesOptions = {
|
|
14
|
+
access: Required<ArticlesAccess>;
|
|
15
|
+
articleUrl: (slug?: string | null) => string;
|
|
16
|
+
seo: false | ArticlesSeoGenerators;
|
|
17
|
+
};
|
|
18
|
+
export declare const Articles: ({ access, articleUrl, seo }: ArticlesOptions) => CollectionConfig;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { slugField } from 'payload';
|
|
2
|
+
import { MetaDescriptionField, MetaImageField, MetaTitleField, OverviewField, PreviewField } from '@payloadcms/plugin-seo/fields';
|
|
3
|
+
import { label } from '../translations/index.js';
|
|
4
|
+
const seoField = (generators)=>({
|
|
5
|
+
name: 'meta',
|
|
6
|
+
label: label((t)=>t.fields.seo),
|
|
7
|
+
type: 'group',
|
|
8
|
+
admin: {
|
|
9
|
+
position: 'sidebar'
|
|
10
|
+
},
|
|
11
|
+
fields: [
|
|
12
|
+
OverviewField({
|
|
13
|
+
titlePath: 'meta.title',
|
|
14
|
+
descriptionPath: 'meta.description',
|
|
15
|
+
imagePath: 'meta.image'
|
|
16
|
+
}),
|
|
17
|
+
MetaTitleField({
|
|
18
|
+
hasGenerateFn: generators.hasGenerateTitle,
|
|
19
|
+
overrides: {
|
|
20
|
+
label: label((t)=>t.fields.seoTitle)
|
|
21
|
+
}
|
|
22
|
+
}),
|
|
23
|
+
MetaImageField({
|
|
24
|
+
relationTo: 'media',
|
|
25
|
+
hasGenerateFn: generators.hasGenerateImage
|
|
26
|
+
}),
|
|
27
|
+
MetaDescriptionField({
|
|
28
|
+
hasGenerateFn: generators.hasGenerateDescription
|
|
29
|
+
}),
|
|
30
|
+
PreviewField({
|
|
31
|
+
hasGenerateFn: true,
|
|
32
|
+
titlePath: 'meta.title',
|
|
33
|
+
descriptionPath: 'meta.description'
|
|
34
|
+
})
|
|
35
|
+
]
|
|
36
|
+
});
|
|
37
|
+
export const Articles = ({ access, articleUrl, seo })=>({
|
|
38
|
+
slug: 'articles',
|
|
39
|
+
labels: {
|
|
40
|
+
singular: label((t)=>t.articles.singular),
|
|
41
|
+
plural: label((t)=>t.articles.plural)
|
|
42
|
+
},
|
|
43
|
+
admin: {
|
|
44
|
+
useAsTitle: 'title',
|
|
45
|
+
defaultColumns: [
|
|
46
|
+
'title',
|
|
47
|
+
'slug',
|
|
48
|
+
'_status',
|
|
49
|
+
'publishedAt',
|
|
50
|
+
'updatedAt'
|
|
51
|
+
],
|
|
52
|
+
livePreview: {
|
|
53
|
+
url: ({ data })=>articleUrl(data?.slug)
|
|
54
|
+
},
|
|
55
|
+
preview: (data)=>articleUrl(data?.slug)
|
|
56
|
+
},
|
|
57
|
+
access: {
|
|
58
|
+
read: access.read,
|
|
59
|
+
create: access.create,
|
|
60
|
+
update: access.update,
|
|
61
|
+
delete: access.delete
|
|
62
|
+
},
|
|
63
|
+
versions: {
|
|
64
|
+
drafts: {
|
|
65
|
+
autosave: true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
fields: [
|
|
69
|
+
{
|
|
70
|
+
name: 'title',
|
|
71
|
+
type: 'text',
|
|
72
|
+
label: label((t)=>t.fields.title),
|
|
73
|
+
required: true
|
|
74
|
+
},
|
|
75
|
+
slugField(),
|
|
76
|
+
{
|
|
77
|
+
name: 'coverImage',
|
|
78
|
+
type: 'upload',
|
|
79
|
+
label: label((t)=>t.fields.coverImage),
|
|
80
|
+
relationTo: 'media'
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'content',
|
|
84
|
+
type: 'richText',
|
|
85
|
+
label: label((t)=>t.fields.content)
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'publishedAt',
|
|
89
|
+
type: 'date',
|
|
90
|
+
label: label((t)=>t.fields.publishedAt),
|
|
91
|
+
admin: {
|
|
92
|
+
position: 'sidebar',
|
|
93
|
+
date: {
|
|
94
|
+
pickerAppearance: 'dayAndTime'
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
hooks: {
|
|
98
|
+
beforeChange: [
|
|
99
|
+
({ siblingData, value })=>{
|
|
100
|
+
if (siblingData._status === 'published' && !value) {
|
|
101
|
+
return new Date();
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
...seo ? [
|
|
109
|
+
seoField(seo)
|
|
110
|
+
] : []
|
|
111
|
+
]
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=Articles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/collections/Articles.ts"],"sourcesContent":["import type { Access, CollectionConfig, Field } from 'payload'\nimport { slugField } from 'payload'\nimport {\n MetaDescriptionField,\n MetaImageField,\n MetaTitleField,\n OverviewField,\n PreviewField,\n} from '@payloadcms/plugin-seo/fields'\nimport { label } from '../translations/index.js'\n\nexport type ArticlesAccess = {\n create?: Access\n delete?: Access\n read?: Access\n update?: Access\n}\n\nexport type ArticlesSeoGenerators = {\n hasGenerateDescription: boolean\n hasGenerateImage: boolean\n hasGenerateTitle: boolean\n}\n\nexport type ArticlesOptions = {\n access: Required<ArticlesAccess>\n articleUrl: (slug?: string | null) => string\n seo: false | ArticlesSeoGenerators\n}\n\nconst seoField = (generators: ArticlesSeoGenerators): Field => ({\n name: 'meta',\n label: label((t) => t.fields.seo),\n type: 'group',\n admin: {\n position: 'sidebar',\n },\n fields: [\n OverviewField({\n titlePath: 'meta.title',\n descriptionPath: 'meta.description',\n imagePath: 'meta.image',\n }),\n MetaTitleField({\n hasGenerateFn: generators.hasGenerateTitle,\n overrides: {\n label: label((t) => t.fields.seoTitle),\n },\n }),\n MetaImageField({\n relationTo: 'media',\n hasGenerateFn: generators.hasGenerateImage,\n }),\n MetaDescriptionField({\n hasGenerateFn: generators.hasGenerateDescription,\n }),\n PreviewField({\n hasGenerateFn: true,\n titlePath: 'meta.title',\n descriptionPath: 'meta.description',\n }),\n ],\n})\n\nexport const Articles = ({ access, articleUrl, seo }: ArticlesOptions): CollectionConfig => ({\n slug: 'articles',\n labels: {\n singular: label((t) => t.articles.singular),\n plural: label((t) => t.articles.plural),\n },\n admin: {\n useAsTitle: 'title',\n defaultColumns: ['title', 'slug', '_status', 'publishedAt', 'updatedAt'],\n livePreview: {\n url: ({ data }) => articleUrl(data?.slug as string | undefined),\n },\n preview: (data) => articleUrl(data?.slug as string | undefined),\n },\n access: {\n read: access.read,\n create: access.create,\n update: access.update,\n delete: access.delete,\n },\n versions: {\n drafts: {\n autosave: true,\n },\n },\n fields: [\n {\n name: 'title',\n type: 'text',\n label: label((t) => t.fields.title),\n required: true,\n },\n slugField(),\n {\n name: 'coverImage',\n type: 'upload',\n label: label((t) => t.fields.coverImage),\n relationTo: 'media',\n },\n {\n name: 'content',\n type: 'richText',\n label: label((t) => t.fields.content),\n },\n {\n name: 'publishedAt',\n type: 'date',\n label: label((t) => t.fields.publishedAt),\n admin: {\n position: 'sidebar',\n date: {\n pickerAppearance: 'dayAndTime',\n },\n },\n hooks: {\n beforeChange: [\n ({ siblingData, value }) => {\n if (siblingData._status === 'published' && !value) {\n return new Date()\n }\n return value\n },\n ],\n },\n },\n ...(seo ? [seoField(seo)] : []),\n ],\n})\n"],"names":["slugField","MetaDescriptionField","MetaImageField","MetaTitleField","OverviewField","PreviewField","label","seoField","generators","name","t","fields","seo","type","admin","position","titlePath","descriptionPath","imagePath","hasGenerateFn","hasGenerateTitle","overrides","seoTitle","relationTo","hasGenerateImage","hasGenerateDescription","Articles","access","articleUrl","slug","labels","singular","articles","plural","useAsTitle","defaultColumns","livePreview","url","data","preview","read","create","update","delete","versions","drafts","autosave","title","required","coverImage","content","publishedAt","date","pickerAppearance","hooks","beforeChange","siblingData","value","_status","Date"],"mappings":"AACA,SAASA,SAAS,QAAQ,UAAS;AACnC,SACEC,oBAAoB,EACpBC,cAAc,EACdC,cAAc,EACdC,aAAa,EACbC,YAAY,QACP,gCAA+B;AACtC,SAASC,KAAK,QAAQ,2BAA0B;AAqBhD,MAAMC,WAAW,CAACC,aAA8C,CAAA;QAC9DC,MAAM;QACNH,OAAOA,MAAM,CAACI,IAAMA,EAAEC,MAAM,CAACC,GAAG;QAChCC,MAAM;QACNC,OAAO;YACLC,UAAU;QACZ;QACAJ,QAAQ;YACNP,cAAc;gBACZY,WAAW;gBACXC,iBAAiB;gBACjBC,WAAW;YACb;YACAf,eAAe;gBACbgB,eAAeX,WAAWY,gBAAgB;gBAC1CC,WAAW;oBACTf,OAAOA,MAAM,CAACI,IAAMA,EAAEC,MAAM,CAACW,QAAQ;gBACvC;YACF;YACApB,eAAe;gBACbqB,YAAY;gBACZJ,eAAeX,WAAWgB,gBAAgB;YAC5C;YACAvB,qBAAqB;gBACnBkB,eAAeX,WAAWiB,sBAAsB;YAClD;YACApB,aAAa;gBACXc,eAAe;gBACfH,WAAW;gBACXC,iBAAiB;YACnB;SACD;IACH,CAAA;AAEA,OAAO,MAAMS,WAAW,CAAC,EAAEC,MAAM,EAAEC,UAAU,EAAEhB,GAAG,EAAmB,GAAwB,CAAA;QAC3FiB,MAAM;QACNC,QAAQ;YACNC,UAAUzB,MAAM,CAACI,IAAMA,EAAEsB,QAAQ,CAACD,QAAQ;YAC1CE,QAAQ3B,MAAM,CAACI,IAAMA,EAAEsB,QAAQ,CAACC,MAAM;QACxC;QACAnB,OAAO;YACLoB,YAAY;YACZC,gBAAgB;gBAAC;gBAAS;gBAAQ;gBAAW;gBAAe;aAAY;YACxEC,aAAa;gBACXC,KAAK,CAAC,EAAEC,IAAI,EAAE,GAAKV,WAAWU,MAAMT;YACtC;YACAU,SAAS,CAACD,OAASV,WAAWU,MAAMT;QACtC;QACAF,QAAQ;YACNa,MAAMb,OAAOa,IAAI;YACjBC,QAAQd,OAAOc,MAAM;YACrBC,QAAQf,OAAOe,MAAM;YACrBC,QAAQhB,OAAOgB,MAAM;QACvB;QACAC,UAAU;YACRC,QAAQ;gBACNC,UAAU;YACZ;QACF;QACAnC,QAAQ;YACN;gBACEF,MAAM;gBACNI,MAAM;gBACNP,OAAOA,MAAM,CAACI,IAAMA,EAAEC,MAAM,CAACoC,KAAK;gBAClCC,UAAU;YACZ;YACAhD;YACA;gBACES,MAAM;gBACNI,MAAM;gBACNP,OAAOA,MAAM,CAACI,IAAMA,EAAEC,MAAM,CAACsC,UAAU;gBACvC1B,YAAY;YACd;YACA;gBACEd,MAAM;gBACNI,MAAM;gBACNP,OAAOA,MAAM,CAACI,IAAMA,EAAEC,MAAM,CAACuC,OAAO;YACtC;YACA;gBACEzC,MAAM;gBACNI,MAAM;gBACNP,OAAOA,MAAM,CAACI,IAAMA,EAAEC,MAAM,CAACwC,WAAW;gBACxCrC,OAAO;oBACLC,UAAU;oBACVqC,MAAM;wBACJC,kBAAkB;oBACpB;gBACF;gBACAC,OAAO;oBACLC,cAAc;wBACZ,CAAC,EAAEC,WAAW,EAAEC,KAAK,EAAE;4BACrB,IAAID,YAAYE,OAAO,KAAK,eAAe,CAACD,OAAO;gCACjD,OAAO,IAAIE;4BACb;4BACA,OAAOF;wBACT;qBACD;gBACH;YACF;eACI7C,MAAM;gBAACL,SAASK;aAAK,GAAG,EAAE;SAC/B;IACH,CAAA,EAAE"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Access } from 'payload';
|
|
2
|
+
import type { GenerateDescription, GenerateImage, GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types';
|
|
3
|
+
export declare const SEO_DESCRIPTION_MAX_LENGTH = 160;
|
|
4
|
+
export declare const authenticated: Access;
|
|
5
|
+
export declare const authenticatedOrPublished: Access;
|
|
6
|
+
export declare const defaultArticleUrl: (slug?: string | null) => string;
|
|
7
|
+
export declare const defaultGenerateDescription: GenerateDescription;
|
|
8
|
+
export declare const defaultGenerateImage: GenerateImage;
|
|
9
|
+
export declare const defaultGenerateTitle: GenerateTitle;
|
|
10
|
+
export declare const defaultGenerateURL: (articleUrl: (slug?: string | null) => string) => GenerateURL;
|
package/dist/defaults.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const SEO_DESCRIPTION_MAX_LENGTH = 160;
|
|
2
|
+
export const authenticated = ({ req: { user } })=>Boolean(user);
|
|
3
|
+
export const authenticatedOrPublished = ({ req: { user } })=>{
|
|
4
|
+
if (user) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
return {
|
|
8
|
+
_status: {
|
|
9
|
+
equals: 'published'
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export const defaultArticleUrl = (slug)=>`${process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000'}/articles/${slug ?? ''}`;
|
|
14
|
+
/** Collects the plain text of a lexical richText value, for the default meta description. */ const richTextToPlainText = (content)=>{
|
|
15
|
+
const texts = [];
|
|
16
|
+
const walk = (node)=>{
|
|
17
|
+
if (!node || typeof node !== 'object') {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const { children, text } = node;
|
|
21
|
+
if (typeof text === 'string') {
|
|
22
|
+
texts.push(text);
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(children)) {
|
|
25
|
+
children.forEach(walk);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
walk(content?.root);
|
|
29
|
+
return texts.join(' ').replace(/\s+/g, ' ').trim();
|
|
30
|
+
};
|
|
31
|
+
export const defaultGenerateDescription = ({ doc })=>richTextToPlainText(doc?.content).slice(0, SEO_DESCRIPTION_MAX_LENGTH);
|
|
32
|
+
export const defaultGenerateImage = ({ doc })=>(typeof doc?.coverImage === 'object' ? doc?.coverImage?.id : doc?.coverImage) ?? '';
|
|
33
|
+
export const defaultGenerateTitle = ({ doc })=>doc?.title ?? '';
|
|
34
|
+
export const defaultGenerateURL = (articleUrl)=>({ doc })=>articleUrl(doc?.slug);
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=defaults.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type { Access } from 'payload'\nimport type {\n GenerateDescription,\n GenerateImage,\n GenerateTitle,\n GenerateURL,\n} from '@payloadcms/plugin-seo/types'\n\nexport const SEO_DESCRIPTION_MAX_LENGTH = 160\n\nexport const authenticated: Access = ({ req: { user } }) => Boolean(user)\n\nexport const authenticatedOrPublished: Access = ({ req: { user } }) => {\n if (user) {\n return true\n }\n\n return {\n _status: {\n equals: 'published',\n },\n }\n}\n\nexport const defaultArticleUrl = (slug?: string | null) =>\n `${process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000'}/articles/${slug ?? ''}`\n\n/** Collects the plain text of a lexical richText value, for the default meta description. */\nconst richTextToPlainText = (content: unknown): string => {\n const texts: string[] = []\n\n const walk = (node: unknown) => {\n if (!node || typeof node !== 'object') {\n return\n }\n const { children, text } = node as { children?: unknown[]; text?: unknown }\n if (typeof text === 'string') {\n texts.push(text)\n }\n if (Array.isArray(children)) {\n children.forEach(walk)\n }\n }\n\n walk((content as { root?: unknown })?.root)\n\n return texts.join(' ').replace(/\\s+/g, ' ').trim()\n}\n\nexport const defaultGenerateDescription: GenerateDescription = ({ doc }) =>\n richTextToPlainText(doc?.content).slice(0, SEO_DESCRIPTION_MAX_LENGTH)\n\nexport const defaultGenerateImage: GenerateImage = ({ doc }) =>\n (typeof doc?.coverImage === 'object' ? doc?.coverImage?.id : doc?.coverImage) ?? ''\n\nexport const defaultGenerateTitle: GenerateTitle = ({ doc }) => doc?.title ?? ''\n\nexport const defaultGenerateURL =\n (articleUrl: (slug?: string | null) => string): GenerateURL =>\n ({ doc }) =>\n articleUrl(doc?.slug)\n"],"names":["SEO_DESCRIPTION_MAX_LENGTH","authenticated","req","user","Boolean","authenticatedOrPublished","_status","equals","defaultArticleUrl","slug","process","env","NEXT_PUBLIC_SERVER_URL","richTextToPlainText","content","texts","walk","node","children","text","push","Array","isArray","forEach","root","join","replace","trim","defaultGenerateDescription","doc","slice","defaultGenerateImage","coverImage","id","defaultGenerateTitle","title","defaultGenerateURL","articleUrl"],"mappings":"AAQA,OAAO,MAAMA,6BAA6B,IAAG;AAE7C,OAAO,MAAMC,gBAAwB,CAAC,EAAEC,KAAK,EAAEC,IAAI,EAAE,EAAE,GAAKC,QAAQD,MAAK;AAEzE,OAAO,MAAME,2BAAmC,CAAC,EAAEH,KAAK,EAAEC,IAAI,EAAE,EAAE;IAChE,IAAIA,MAAM;QACR,OAAO;IACT;IAEA,OAAO;QACLG,SAAS;YACPC,QAAQ;QACV;IACF;AACF,EAAC;AAED,OAAO,MAAMC,oBAAoB,CAACC,OAChC,GAAGC,QAAQC,GAAG,CAACC,sBAAsB,IAAI,wBAAwB,UAAU,EAAEH,QAAQ,IAAI,CAAA;AAE3F,2FAA2F,GAC3F,MAAMI,sBAAsB,CAACC;IAC3B,MAAMC,QAAkB,EAAE;IAE1B,MAAMC,OAAO,CAACC;QACZ,IAAI,CAACA,QAAQ,OAAOA,SAAS,UAAU;YACrC;QACF;QACA,MAAM,EAAEC,QAAQ,EAAEC,IAAI,EAAE,GAAGF;QAC3B,IAAI,OAAOE,SAAS,UAAU;YAC5BJ,MAAMK,IAAI,CAACD;QACb;QACA,IAAIE,MAAMC,OAAO,CAACJ,WAAW;YAC3BA,SAASK,OAAO,CAACP;QACnB;IACF;IAEAA,KAAMF,SAAgCU;IAEtC,OAAOT,MAAMU,IAAI,CAAC,KAAKC,OAAO,CAAC,QAAQ,KAAKC,IAAI;AAClD;AAEA,OAAO,MAAMC,6BAAkD,CAAC,EAAEC,GAAG,EAAE,GACrEhB,oBAAoBgB,KAAKf,SAASgB,KAAK,CAAC,GAAG9B,4BAA2B;AAExE,OAAO,MAAM+B,uBAAsC,CAAC,EAAEF,GAAG,EAAE,GACzD,AAAC,CAAA,OAAOA,KAAKG,eAAe,WAAWH,KAAKG,YAAYC,KAAKJ,KAAKG,UAAS,KAAM,GAAE;AAErF,OAAO,MAAME,uBAAsC,CAAC,EAAEL,GAAG,EAAE,GAAKA,KAAKM,SAAS,GAAE;AAEhF,OAAO,MAAMC,qBACX,CAACC,aACD,CAAC,EAAER,GAAG,EAAE,GACNQ,WAAWR,KAAKpB,MAAK"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Config } from 'payload';
|
|
2
|
+
import type { GenerateDescription, GenerateImage, GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types';
|
|
3
|
+
import type { ArticlesAccess } from './collections/Articles.js';
|
|
4
|
+
export type VWPayloadPluginArticlesConfig = {
|
|
5
|
+
/**
|
|
6
|
+
* Access control for the articles collection, per operation.
|
|
7
|
+
* Defaults: `read` allows authenticated users or published documents,
|
|
8
|
+
* `create`/`update`/`delete` require an authenticated user.
|
|
9
|
+
*/
|
|
10
|
+
access?: ArticlesAccess;
|
|
11
|
+
/**
|
|
12
|
+
* Builds the front-end URL of an article, used for admin preview and live preview.
|
|
13
|
+
* Defaults to `${NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000'}/articles/${slug}`.
|
|
14
|
+
*/
|
|
15
|
+
articleUrl?: (slug?: string | null) => string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Adds an SEO `meta` group (title, description, image, preview) to the
|
|
19
|
+
* articles collection, built from `@payloadcms/plugin-seo` fields.
|
|
20
|
+
* `true` (the default) enables it with built-in generate functions
|
|
21
|
+
* (title from the article title, description from the content, image from
|
|
22
|
+
* the cover image, URL from `articleUrl`). Pass an object to override any
|
|
23
|
+
* of the generate functions, or `false` to disable SEO entirely.
|
|
24
|
+
* @default true
|
|
25
|
+
*/
|
|
26
|
+
seo?: boolean | {
|
|
27
|
+
generateDescription?: GenerateDescription;
|
|
28
|
+
generateImage?: GenerateImage;
|
|
29
|
+
generateTitle?: GenerateTitle;
|
|
30
|
+
generateURL?: GenerateURL;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
export declare const VWPayloadPluginArticles: (pluginOptions?: VWPayloadPluginArticlesConfig) => (config: Config) => Config;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { seoPlugin } from '@payloadcms/plugin-seo';
|
|
2
|
+
import { Articles } from './collections/Articles.js';
|
|
3
|
+
import { authenticated, authenticatedOrPublished, defaultArticleUrl, defaultGenerateDescription, defaultGenerateImage, defaultGenerateTitle, defaultGenerateURL } from './defaults.js';
|
|
4
|
+
export const VWPayloadPluginArticles = (pluginOptions = {})=>(config)=>{
|
|
5
|
+
if (!config.collections) {
|
|
6
|
+
config.collections = [];
|
|
7
|
+
}
|
|
8
|
+
const articleUrl = pluginOptions.articleUrl ?? defaultArticleUrl;
|
|
9
|
+
const access = {
|
|
10
|
+
create: pluginOptions.access?.create ?? authenticated,
|
|
11
|
+
delete: pluginOptions.access?.delete ?? authenticated,
|
|
12
|
+
read: pluginOptions.access?.read ?? authenticatedOrPublished,
|
|
13
|
+
update: pluginOptions.access?.update ?? authenticated
|
|
14
|
+
};
|
|
15
|
+
const seoEnabled = pluginOptions.seo !== false;
|
|
16
|
+
const seoOverrides = typeof pluginOptions.seo === 'object' ? pluginOptions.seo : {};
|
|
17
|
+
const generateDescription = seoOverrides.generateDescription ?? defaultGenerateDescription;
|
|
18
|
+
const generateImage = seoOverrides.generateImage ?? defaultGenerateImage;
|
|
19
|
+
const generateTitle = seoOverrides.generateTitle ?? defaultGenerateTitle;
|
|
20
|
+
const generateURL = seoOverrides.generateURL ?? defaultGenerateURL(articleUrl);
|
|
21
|
+
config.collections.push(Articles({
|
|
22
|
+
access,
|
|
23
|
+
articleUrl,
|
|
24
|
+
seo: seoEnabled ? {
|
|
25
|
+
hasGenerateDescription: true,
|
|
26
|
+
hasGenerateImage: true,
|
|
27
|
+
hasGenerateTitle: true
|
|
28
|
+
} : false
|
|
29
|
+
}));
|
|
30
|
+
/**
|
|
31
|
+
* If the plugin is disabled, we still want to keep added collections/fields so the database schema is consistent which is important for migrations.
|
|
32
|
+
*/ if (pluginOptions.disabled) {
|
|
33
|
+
return config;
|
|
34
|
+
}
|
|
35
|
+
if (seoEnabled) {
|
|
36
|
+
// Registers the /plugin-seo/generate-* endpoints the field buttons call.
|
|
37
|
+
// No collections are passed: the meta fields are added by Articles itself.
|
|
38
|
+
config = seoPlugin({
|
|
39
|
+
collections: [],
|
|
40
|
+
generateDescription,
|
|
41
|
+
generateImage,
|
|
42
|
+
generateTitle,
|
|
43
|
+
generateURL
|
|
44
|
+
})(config);
|
|
45
|
+
}
|
|
46
|
+
return config;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config } from 'payload'\nimport type {\n GenerateDescription,\n GenerateImage,\n GenerateTitle,\n GenerateURL,\n} from '@payloadcms/plugin-seo/types'\nimport { seoPlugin } from '@payloadcms/plugin-seo'\nimport type { ArticlesAccess } from './collections/Articles.js'\nimport { Articles } from './collections/Articles.js'\nimport {\n authenticated,\n authenticatedOrPublished,\n defaultArticleUrl,\n defaultGenerateDescription,\n defaultGenerateImage,\n defaultGenerateTitle,\n defaultGenerateURL,\n} from './defaults.js'\n\nexport type VWPayloadPluginArticlesConfig = {\n /**\n * Access control for the articles collection, per operation.\n * Defaults: `read` allows authenticated users or published documents,\n * `create`/`update`/`delete` require an authenticated user.\n */\n access?: ArticlesAccess\n /**\n * Builds the front-end URL of an article, used for admin preview and live preview.\n * Defaults to `${NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000'}/articles/${slug}`.\n */\n articleUrl?: (slug?: string | null) => string\n disabled?: boolean\n /**\n * Adds an SEO `meta` group (title, description, image, preview) to the\n * articles collection, built from `@payloadcms/plugin-seo` fields.\n * `true` (the default) enables it with built-in generate functions\n * (title from the article title, description from the content, image from\n * the cover image, URL from `articleUrl`). Pass an object to override any\n * of the generate functions, or `false` to disable SEO entirely.\n * @default true\n */\n seo?:\n | boolean\n | {\n generateDescription?: GenerateDescription\n generateImage?: GenerateImage\n generateTitle?: GenerateTitle\n generateURL?: GenerateURL\n }\n}\n\nexport const VWPayloadPluginArticles =\n (pluginOptions: VWPayloadPluginArticlesConfig = {}) =>\n (config: Config): Config => {\n if (!config.collections) {\n config.collections = []\n }\n\n const articleUrl = pluginOptions.articleUrl ?? defaultArticleUrl\n\n const access = {\n create: pluginOptions.access?.create ?? authenticated,\n delete: pluginOptions.access?.delete ?? authenticated,\n read: pluginOptions.access?.read ?? authenticatedOrPublished,\n update: pluginOptions.access?.update ?? authenticated,\n }\n\n const seoEnabled = pluginOptions.seo !== false\n const seoOverrides = typeof pluginOptions.seo === 'object' ? pluginOptions.seo : {}\n\n const generateDescription: GenerateDescription =\n seoOverrides.generateDescription ?? defaultGenerateDescription\n const generateImage: GenerateImage = seoOverrides.generateImage ?? defaultGenerateImage\n const generateTitle: GenerateTitle = seoOverrides.generateTitle ?? defaultGenerateTitle\n const generateURL: GenerateURL = seoOverrides.generateURL ?? defaultGenerateURL(articleUrl)\n\n config.collections.push(\n Articles({\n access,\n articleUrl,\n seo: seoEnabled\n ? {\n hasGenerateDescription: true,\n hasGenerateImage: true,\n hasGenerateTitle: true,\n }\n : false,\n }),\n )\n\n /**\n * If the plugin is disabled, we still want to keep added collections/fields so the database schema is consistent which is important for migrations.\n */\n if (pluginOptions.disabled) {\n return config\n }\n\n if (seoEnabled) {\n // Registers the /plugin-seo/generate-* endpoints the field buttons call.\n // No collections are passed: the meta fields are added by Articles itself.\n config = seoPlugin({\n collections: [],\n generateDescription,\n generateImage,\n generateTitle,\n generateURL,\n })(config)\n }\n\n return config\n }\n"],"names":["seoPlugin","Articles","authenticated","authenticatedOrPublished","defaultArticleUrl","defaultGenerateDescription","defaultGenerateImage","defaultGenerateTitle","defaultGenerateURL","VWPayloadPluginArticles","pluginOptions","config","collections","articleUrl","access","create","delete","read","update","seoEnabled","seo","seoOverrides","generateDescription","generateImage","generateTitle","generateURL","push","hasGenerateDescription","hasGenerateImage","hasGenerateTitle","disabled"],"mappings":"AAOA,SAASA,SAAS,QAAQ,yBAAwB;AAElD,SAASC,QAAQ,QAAQ,4BAA2B;AACpD,SACEC,aAAa,EACbC,wBAAwB,EACxBC,iBAAiB,EACjBC,0BAA0B,EAC1BC,oBAAoB,EACpBC,oBAAoB,EACpBC,kBAAkB,QACb,gBAAe;AAkCtB,OAAO,MAAMC,0BACX,CAACC,gBAA+C,CAAC,CAAC,GAClD,CAACC;QACC,IAAI,CAACA,OAAOC,WAAW,EAAE;YACvBD,OAAOC,WAAW,GAAG,EAAE;QACzB;QAEA,MAAMC,aAAaH,cAAcG,UAAU,IAAIT;QAE/C,MAAMU,SAAS;YACbC,QAAQL,cAAcI,MAAM,EAAEC,UAAUb;YACxCc,QAAQN,cAAcI,MAAM,EAAEE,UAAUd;YACxCe,MAAMP,cAAcI,MAAM,EAAEG,QAAQd;YACpCe,QAAQR,cAAcI,MAAM,EAAEI,UAAUhB;QAC1C;QAEA,MAAMiB,aAAaT,cAAcU,GAAG,KAAK;QACzC,MAAMC,eAAe,OAAOX,cAAcU,GAAG,KAAK,WAAWV,cAAcU,GAAG,GAAG,CAAC;QAElF,MAAME,sBACJD,aAAaC,mBAAmB,IAAIjB;QACtC,MAAMkB,gBAA+BF,aAAaE,aAAa,IAAIjB;QACnE,MAAMkB,gBAA+BH,aAAaG,aAAa,IAAIjB;QACnE,MAAMkB,cAA2BJ,aAAaI,WAAW,IAAIjB,mBAAmBK;QAEhFF,OAAOC,WAAW,CAACc,IAAI,CACrBzB,SAAS;YACPa;YACAD;YACAO,KAAKD,aACD;gBACEQ,wBAAwB;gBACxBC,kBAAkB;gBAClBC,kBAAkB;YACpB,IACA;QACN;QAGF;;KAEC,GACD,IAAInB,cAAcoB,QAAQ,EAAE;YAC1B,OAAOnB;QACT;QAEA,IAAIQ,YAAY;YACd,yEAAyE;YACzE,2EAA2E;YAC3ER,SAASX,UAAU;gBACjBY,aAAa,EAAE;gBACfU;gBACAC;gBACAC;gBACAC;YACF,GAAGd;QACL;QAEA,OAAOA;IACT,EAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const en = {
|
|
2
|
+
articles: {
|
|
3
|
+
plural: 'Articles',
|
|
4
|
+
singular: 'Article'
|
|
5
|
+
},
|
|
6
|
+
fields: {
|
|
7
|
+
content: 'Content',
|
|
8
|
+
coverImage: 'Cover Image',
|
|
9
|
+
publishedAt: 'Published At',
|
|
10
|
+
seo: 'SEO',
|
|
11
|
+
seoTitle: 'Title',
|
|
12
|
+
title: 'Title'
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=en.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/translations/en.ts"],"sourcesContent":["export const en = {\n articles: {\n plural: 'Articles',\n singular: 'Article',\n },\n fields: {\n content: 'Content',\n coverImage: 'Cover Image',\n publishedAt: 'Published At',\n seo: 'SEO',\n seoTitle: 'Title',\n title: 'Title',\n },\n}\n"],"names":["en","articles","plural","singular","fields","content","coverImage","publishedAt","seo","seoTitle","title"],"mappings":"AAAA,OAAO,MAAMA,KAAK;IAChBC,UAAU;QACRC,QAAQ;QACRC,UAAU;IACZ;IACAC,QAAQ;QACNC,SAAS;QACTC,YAAY;QACZC,aAAa;QACbC,KAAK;QACLC,UAAU;QACVC,OAAO;IACT;AACF,EAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const fr = {
|
|
2
|
+
articles: {
|
|
3
|
+
plural: 'Articles',
|
|
4
|
+
singular: 'Article'
|
|
5
|
+
},
|
|
6
|
+
fields: {
|
|
7
|
+
content: 'Contenu',
|
|
8
|
+
coverImage: 'Image de couverture',
|
|
9
|
+
publishedAt: 'Publié le',
|
|
10
|
+
seo: 'SEO',
|
|
11
|
+
seoTitle: 'Titre',
|
|
12
|
+
title: 'Titre'
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=fr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/translations/fr.ts"],"sourcesContent":["import type { Translation } from './index.js'\n\nexport const fr: Translation = {\n articles: {\n plural: 'Articles',\n singular: 'Article',\n },\n fields: {\n content: 'Contenu',\n coverImage: 'Image de couverture',\n publishedAt: 'Publié le',\n seo: 'SEO',\n seoTitle: 'Titre',\n title: 'Titre',\n },\n}\n"],"names":["fr","articles","plural","singular","fields","content","coverImage","publishedAt","seo","seoTitle","title"],"mappings":"AAEA,OAAO,MAAMA,KAAkB;IAC7BC,UAAU;QACRC,QAAQ;QACRC,UAAU;IACZ;IACAC,QAAQ;QACNC,SAAS;QACTC,YAAY;QACZC,aAAa;QACbC,KAAK;QACLC,UAAU;QACVC,OAAO;IACT;AACF,EAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/translations/index.ts"],"sourcesContent":["import { en } from './en.js'\nimport { fr } from './fr.js'\n\nexport type Translation = typeof en\n\n/** Builds a Payload label record ({ en, fr }) from a translation key selector. */\nexport const label = (pick: (t: Translation) => string): Record<string, string> => ({\n en: pick(en),\n fr: pick(fr),\n})\n"],"names":["en","fr","label","pick"],"mappings":"AAAA,SAASA,EAAE,QAAQ,UAAS;AAC5B,SAASC,EAAE,QAAQ,UAAS;AAI5B,gFAAgF,GAChF,OAAO,MAAMC,QAAQ,CAACC,OAA8D,CAAA;QAClFH,IAAIG,KAAKH;QACTC,IAAIE,KAAKF;IACX,CAAA,EAAE"}
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vitrailweb/payload-plugin-articles",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Payload plugin that adds an Articles collection",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Vitrail Web"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@eslint/eslintrc": "^3.2.0",
|
|
24
|
+
"@payloadcms/db-mongodb": "3.84.1",
|
|
25
|
+
"@payloadcms/db-postgres": "3.84.1",
|
|
26
|
+
"@payloadcms/db-sqlite": "3.84.1",
|
|
27
|
+
"@payloadcms/eslint-config": "3.28.0",
|
|
28
|
+
"@payloadcms/next": "3.84.1",
|
|
29
|
+
"@payloadcms/richtext-lexical": "3.84.1",
|
|
30
|
+
"@payloadcms/ui": "3.84.1",
|
|
31
|
+
"@payloadcms/plugin-seo": "3.84.1",
|
|
32
|
+
"@playwright/test": "1.58.2",
|
|
33
|
+
"@swc-node/register": "1.10.9",
|
|
34
|
+
"@swc/cli": "0.6.0",
|
|
35
|
+
"@types/node": "22.19.9",
|
|
36
|
+
"@types/react": "19.2.14",
|
|
37
|
+
"@types/react-dom": "19.2.3",
|
|
38
|
+
"copyfiles": "2.4.1",
|
|
39
|
+
"cross-env": "^7.0.3",
|
|
40
|
+
"eslint": "^9.23.0",
|
|
41
|
+
"eslint-config-next": "16.2.6",
|
|
42
|
+
"graphql": "^16.8.1",
|
|
43
|
+
"mongodb-memory-server": "10.1.4",
|
|
44
|
+
"next": "16.2.6",
|
|
45
|
+
"open": "^10.1.0",
|
|
46
|
+
"payload": "3.84.1",
|
|
47
|
+
"prettier": "^3.4.2",
|
|
48
|
+
"qs-esm": "8.0.1",
|
|
49
|
+
"react": "19.2.6",
|
|
50
|
+
"react-dom": "19.2.6",
|
|
51
|
+
"rimraf": "3.0.2",
|
|
52
|
+
"sharp": "0.34.2",
|
|
53
|
+
"sort-package-json": "^2.10.0",
|
|
54
|
+
"typescript": "5.7.3",
|
|
55
|
+
"vite-tsconfig-paths": "6.0.5",
|
|
56
|
+
"vitest": "4.0.18"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"payload": "^3.84.1",
|
|
60
|
+
"@payloadcms/plugin-seo": "^3.84.1"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": "^18.20.2 || >=20.9.0",
|
|
64
|
+
"pnpm": "^9 || ^10 || ^11"
|
|
65
|
+
},
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public",
|
|
68
|
+
"registry": "https://registry.npmjs.org/"
|
|
69
|
+
},
|
|
70
|
+
"dependencies": {},
|
|
71
|
+
"scripts": {
|
|
72
|
+
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|
|
73
|
+
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
|
74
|
+
"build:types": "tsc --outDir dist --rootDir ./src",
|
|
75
|
+
"clean": "rimraf {dist,*.tsbuildinfo}",
|
|
76
|
+
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
77
|
+
"dev": "next dev dev --turbo",
|
|
78
|
+
"dev:generate-importmap": "pnpm dev:payload generate:importmap",
|
|
79
|
+
"dev:generate-types": "pnpm dev:payload generate:types",
|
|
80
|
+
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
81
|
+
"generate:importmap": "pnpm dev:generate-importmap",
|
|
82
|
+
"generate:types": "pnpm dev:generate-types",
|
|
83
|
+
"lint": "eslint",
|
|
84
|
+
"lint:fix": "eslint ./src --fix",
|
|
85
|
+
"test": "pnpm test:int && pnpm test:e2e",
|
|
86
|
+
"test:e2e": "playwright test",
|
|
87
|
+
"test:int": "vitest"
|
|
88
|
+
}
|
|
89
|
+
}
|