@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 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;
@@ -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"}
@@ -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,14 @@
1
+ export declare const en: {
2
+ articles: {
3
+ plural: string;
4
+ singular: string;
5
+ };
6
+ fields: {
7
+ content: string;
8
+ coverImage: string;
9
+ publishedAt: string;
10
+ seo: string;
11
+ seoTitle: string;
12
+ title: string;
13
+ };
14
+ };
@@ -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,2 @@
1
+ import type { Translation } from './index.js';
2
+ export declare const fr: Translation;
@@ -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,4 @@
1
+ import { en } from './en.js';
2
+ export type Translation = typeof en;
3
+ /** Builds a Payload label record ({ en, fr }) from a translation key selector. */
4
+ export declare const label: (pick: (t: Translation) => string) => Record<string, string>;
@@ -0,0 +1,8 @@
1
+ import { en } from './en.js';
2
+ import { fr } from './fr.js';
3
+ /** Builds a Payload label record ({ en, fr }) from a translation key selector. */ export const label = (pick)=>({
4
+ en: pick(en),
5
+ fr: pick(fr)
6
+ });
7
+
8
+ //# sourceMappingURL=index.js.map
@@ -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
+ }