@nitrogenbuilder/connector-payload 0.1.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.
Files changed (40) hide show
  1. package/README.md +101 -0
  2. package/dist/collection-registry.d.ts +7 -0
  3. package/dist/collection-registry.js +12 -0
  4. package/dist/collections/NitrogenTemplates.d.ts +2 -0
  5. package/dist/collections/NitrogenTemplates.js +78 -0
  6. package/dist/components/NitrogenEditButton.d.ts +4 -0
  7. package/dist/components/NitrogenEditButton.js +43 -0
  8. package/dist/editor/NitrogenEditorLayout.d.ts +15 -0
  9. package/dist/editor/NitrogenEditorLayout.js +15 -0
  10. package/dist/editor/NitrogenEditorPage.d.ts +20 -0
  11. package/dist/editor/NitrogenEditorPage.js +87 -0
  12. package/dist/editor/index.d.ts +2 -0
  13. package/dist/editor/index.js +2 -0
  14. package/dist/endpoints/all.d.ts +2 -0
  15. package/dist/endpoints/all.js +48 -0
  16. package/dist/endpoints/collection-endpoints.d.ts +8 -0
  17. package/dist/endpoints/collection-endpoints.js +168 -0
  18. package/dist/endpoints/helpers.d.ts +64 -0
  19. package/dist/endpoints/helpers.js +104 -0
  20. package/dist/endpoints/media.d.ts +2 -0
  21. package/dist/endpoints/media.js +93 -0
  22. package/dist/endpoints/menu.d.ts +2 -0
  23. package/dist/endpoints/menu.js +19 -0
  24. package/dist/endpoints/nitrogen-settings.d.ts +2 -0
  25. package/dist/endpoints/nitrogen-settings.js +22 -0
  26. package/dist/endpoints/templates.d.ts +2 -0
  27. package/dist/endpoints/templates.js +114 -0
  28. package/dist/frontend/NitrogenPageClient.d.ts +16 -0
  29. package/dist/frontend/NitrogenPageClient.js +55 -0
  30. package/dist/frontend/NitrogenWrapper.d.ts +45 -0
  31. package/dist/frontend/NitrogenWrapper.js +38 -0
  32. package/dist/frontend/index.d.ts +7 -0
  33. package/dist/frontend/index.js +8 -0
  34. package/dist/globals/NitrogenSettings.d.ts +2 -0
  35. package/dist/globals/NitrogenSettings.js +41 -0
  36. package/dist/index.d.ts +21 -0
  37. package/dist/index.js +140 -0
  38. package/dist/types.d.ts +125 -0
  39. package/dist/types.js +7 -0
  40. package/package.json +49 -0
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Nitrogen Connector Payload
2
+
3
+ A Payload CMS plugin for visual page building with Nitrogen.
4
+
5
+ ## Setup
6
+
7
+ This project uses the `nitrogen-connector-payload` plugin for visual page building with Nitrogen. Here's how to install it in a fresh Payload CMS project:
8
+
9
+ ### 1. Install Dependencies
10
+
11
+ ```bash
12
+ pnpm add nitrogen-connector-payload //This is not done yet. It's dev only for now.
13
+ pnpm add @nitrogenbuilder/client-react @nitrogenbuilder/client-core @nitrogenbuilder/types
14
+ ```
15
+
16
+ ### 2. Add Plugin to Payload Config
17
+
18
+ In your `src/plugins/index.ts` (or wherever you configure plugins):
19
+
20
+ ```ts
21
+ import { nitrogenConnectorPlugin } from "nitrogen-connector-payload"; //This is not done yet. For Dev: "link:../nitrogen-connector-payload"
22
+
23
+ export const plugins: Plugin[] = [
24
+ nitrogenConnectorPlugin({
25
+ collections: ["pages", "posts"], // Collections to enable Nitrogen editing on
26
+ }),
27
+ // ... other plugins
28
+ ];
29
+ ```
30
+
31
+ ### 3. Wrap Your Page Routes
32
+
33
+ In your page route files (e.g., `src/app/(frontend)/[slug]/page.tsx`), wrap your content with `NitrogenWrapper`:
34
+
35
+ ```tsx
36
+ import { NitrogenWrapper } from 'nitrogen-connector-payload/frontend'
37
+
38
+ export default async function Page({ params, searchParams }) {
39
+ const page = await queryPage(...)
40
+ const search = await searchParams
41
+
42
+ return (
43
+ <NitrogenWrapper page={page} searchParams={search} collection="pages">
44
+ {/* Your regular page content */}
45
+ <RenderHero {...page.hero} />
46
+ <RenderBlocks blocks={page.layout} />
47
+ </NitrogenWrapper>
48
+ )
49
+ }
50
+ ```
51
+
52
+ That's it! The `NitrogenWrapper` component automatically:
53
+
54
+ - Detects when the Nitrogen editor is requesting the page (via `?nitrogen-builder` query param)
55
+ - Checks if the page has Nitrogen data
56
+ - Renders the Nitrogen visual builder when needed, otherwise renders your regular content
57
+
58
+ ### Adding Custom Components
59
+
60
+ To register custom Nitrogen components, create a client component:
61
+
62
+ ```tsx
63
+ // src/components/NitrogenComponents.tsx
64
+ "use client";
65
+
66
+ import { nitrogen } from "nitrogen-connector-payload/frontend";
67
+ import type {
68
+ ComponentSettings,
69
+ ComponentSettingsToProps,
70
+ } from "nitrogen-connector-payload/frontend";
71
+ import MyComponent from "./MyComponent";
72
+
73
+ const myComponentSettings = {
74
+ categories: {
75
+ content: {
76
+ label: "Content",
77
+ groups: {
78
+ content: {
79
+ label: "Content",
80
+ props: {
81
+ title: { type: "string", default: "Hello" },
82
+ },
83
+ },
84
+ },
85
+ },
86
+ },
87
+ } as const satisfies ComponentSettings;
88
+
89
+ // Register on module load
90
+ nitrogen.registerModule("my-component", MyComponent, myComponentSettings);
91
+
92
+ export {};
93
+ ```
94
+
95
+ Then import it in your page route:
96
+
97
+ ```tsx
98
+ import "@/components/NitrogenComponents";
99
+ ```
100
+
101
+ ---
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Runtime registry of collections configured for Nitrogen editing.
3
+ *
4
+ * The plugin registers collections at init time via `registerCollection()`.
5
+ */
6
+ export declare function registerCollection(alias: string, collectionSlug: string): void;
7
+ export declare function resolveCollection(postType: string): string;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Runtime registry of collections configured for Nitrogen editing.
3
+ *
4
+ * The plugin registers collections at init time via `registerCollection()`.
5
+ */
6
+ const registeredCollections = new Map();
7
+ export function registerCollection(alias, collectionSlug) {
8
+ registeredCollections.set(alias, collectionSlug);
9
+ }
10
+ export function resolveCollection(postType) {
11
+ return registeredCollections.get(postType) || postType;
12
+ }
@@ -0,0 +1,2 @@
1
+ import type { CollectionConfig } from 'payload';
2
+ export declare const NitrogenTemplates: CollectionConfig;
@@ -0,0 +1,78 @@
1
+ export const NitrogenTemplates = {
2
+ slug: 'nitrogen-templates',
3
+ admin: {
4
+ useAsTitle: 'title',
5
+ defaultColumns: ['title', 'associatedCollection', 'updatedAt'],
6
+ components: {
7
+ edit: {
8
+ beforeDocumentControls: [
9
+ {
10
+ path: 'nitrogen-connector-payload/components/NitrogenEditButton#NitrogenEditButton',
11
+ clientProps: {
12
+ collection: 'nitrogen-templates',
13
+ },
14
+ },
15
+ ],
16
+ },
17
+ },
18
+ },
19
+ access: {
20
+ read: () => true,
21
+ create: ({ req }) => !!req.user,
22
+ update: ({ req }) => !!req.user,
23
+ delete: ({ req }) => !!req.user,
24
+ },
25
+ fields: [
26
+ {
27
+ name: 'title',
28
+ type: 'text',
29
+ required: true,
30
+ },
31
+ {
32
+ name: 'slug',
33
+ type: 'text',
34
+ required: true,
35
+ unique: true,
36
+ },
37
+ {
38
+ name: 'status',
39
+ type: 'select',
40
+ options: [
41
+ { label: 'Draft', value: 'draft' },
42
+ { label: 'Published', value: 'published' },
43
+ ],
44
+ defaultValue: 'draft',
45
+ admin: {
46
+ position: 'sidebar',
47
+ },
48
+ },
49
+ {
50
+ name: 'templateType',
51
+ type: 'text',
52
+ admin: {
53
+ description: 'Template type identifier for filtering',
54
+ },
55
+ },
56
+ {
57
+ name: 'associatedCollection',
58
+ type: 'text',
59
+ admin: {
60
+ description: 'Which collection this template applies to (e.g., "posts", "header", "footer")',
61
+ },
62
+ },
63
+ {
64
+ name: 'nitrogenData',
65
+ type: 'json',
66
+ admin: {
67
+ description: 'Nitrogen page builder JSON module tree',
68
+ },
69
+ },
70
+ {
71
+ name: 'templateSettings',
72
+ type: 'json',
73
+ admin: {
74
+ description: 'Template-level Nitrogen settings',
75
+ },
76
+ },
77
+ ],
78
+ };
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ export declare const NitrogenEditButton: React.FC<{
3
+ collection: string;
4
+ }>;
@@ -0,0 +1,43 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useState, useEffect } from "react";
4
+ export const NitrogenEditButton = ({ collection }) => {
5
+ const [id, setId] = useState(undefined);
6
+ const [token, setToken] = useState(undefined);
7
+ useEffect(() => {
8
+ const segments = window.location.pathname.split("/").filter(Boolean);
9
+ // segments: ["admin", "collections", "{collection-slug}", "{id}"]
10
+ if (segments.length >= 4) {
11
+ setId(segments[3]);
12
+ }
13
+ // Fetch the connector token from NitrogenSettings global
14
+ fetch("/api/globals/nitrogen-settings")
15
+ .then((res) => res.json())
16
+ .then((data) => {
17
+ if (data.connectorToken) {
18
+ setToken(data.connectorToken);
19
+ }
20
+ })
21
+ .catch((err) => {
22
+ console.error("Failed to fetch nitrogen settings:", err);
23
+ });
24
+ }, []);
25
+ if (!id || !token)
26
+ return null;
27
+ // Determine which query param to use based on collection type
28
+ const param = collection === "nitrogen-templates" ? "templateId" : "pageId";
29
+ // Always pass the collection slug so the editor knows exactly which collection to query.
30
+ const href = `/nitrogen-editor?token=${encodeURIComponent(token)}&collection=${encodeURIComponent(collection)}&${param}=${id}`;
31
+ return (_jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", style: {
32
+ display: "inline-flex",
33
+ alignItems: "center",
34
+ gap: "8px",
35
+ padding: "8px 16px",
36
+ background: "#6366f1",
37
+ color: "#fff",
38
+ borderRadius: "4px",
39
+ textDecoration: "none",
40
+ fontSize: "14px",
41
+ fontWeight: 500,
42
+ }, children: "Edit with Nitrogen" }));
43
+ };
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ /**
3
+ * Root layout for the Nitrogen editor route group.
4
+ * Provides <html>, <head>, and <body> so the editor page
5
+ * doesn't conflict with Next.js's layout requirements.
6
+ *
7
+ * Mount this as the route-group root layout, e.g.
8
+ * `app/(nitrogen-editor)/layout.tsx`:
9
+ *
10
+ * import { NitrogenEditorLayout } from 'nitrogen-connector-payload/editor'
11
+ * export default NitrogenEditorLayout
12
+ */
13
+ export default function NitrogenEditorLayout({ children, }: {
14
+ children: React.ReactNode;
15
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Root layout for the Nitrogen editor route group.
4
+ * Provides <html>, <head>, and <body> so the editor page
5
+ * doesn't conflict with Next.js's layout requirements.
6
+ *
7
+ * Mount this as the route-group root layout, e.g.
8
+ * `app/(nitrogen-editor)/layout.tsx`:
9
+ *
10
+ * import { NitrogenEditorLayout } from 'nitrogen-connector-payload/editor'
11
+ * export default NitrogenEditorLayout
12
+ */
13
+ export default function NitrogenEditorLayout({ children, }) {
14
+ return (_jsxs("html", { lang: "en", suppressHydrationWarning: true, children: [_jsxs("head", { children: [_jsx("meta", { charSet: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("title", { children: "Nitrogen Editor" }), _jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "" }), _jsx("link", { href: "https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap", rel: "stylesheet" })] }), _jsx("body", { suppressHydrationWarning: true, children: children })] }));
15
+ }
@@ -0,0 +1,20 @@
1
+ interface NitrogenEditorSearchParams {
2
+ pageId?: string;
3
+ templateId?: string;
4
+ collection?: string;
5
+ development?: string;
6
+ }
7
+ /**
8
+ * Standalone editor page that loads the Nitrogen builder.
9
+ *
10
+ * Mount this in your Next.js app at e.g. `app/(app)/nitrogen-editor/page.tsx`:
11
+ *
12
+ * import { NitrogenEditorPage } from 'nitrogen-connector-payload/editor'
13
+ * export default NitrogenEditorPage
14
+ *
15
+ * Accessible at `/nitrogen-editor?pageId=xxx` or `/nitrogen-editor?templateId=xxx`.
16
+ */
17
+ export default function NitrogenEditorPage({ searchParams, }: {
18
+ searchParams: Promise<NitrogenEditorSearchParams>;
19
+ }): Promise<import("react/jsx-runtime").JSX.Element>;
20
+ export {};
@@ -0,0 +1,87 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { getPayload } from 'payload';
3
+ import { headers } from 'next/headers';
4
+ import { redirect } from 'next/navigation';
5
+ /**
6
+ * Standalone editor page that loads the Nitrogen builder.
7
+ *
8
+ * Mount this in your Next.js app at e.g. `app/(app)/nitrogen-editor/page.tsx`:
9
+ *
10
+ * import { NitrogenEditorPage } from 'nitrogen-connector-payload/editor'
11
+ * export default NitrogenEditorPage
12
+ *
13
+ * Accessible at `/nitrogen-editor?pageId=xxx` or `/nitrogen-editor?templateId=xxx`.
14
+ */
15
+ export default async function NitrogenEditorPage({ searchParams, }) {
16
+ // Dynamic import so consumers provide their own @payload-config
17
+ const payloadConfig = (await import('@payload-config')).default;
18
+ const payload = await getPayload({ config: payloadConfig });
19
+ const params = await searchParams;
20
+ // Authenticate — redirect to login if not authenticated
21
+ const headersList = await headers();
22
+ const { user } = await payload.auth({ headers: headersList });
23
+ if (!user) {
24
+ return redirect('/admin/login');
25
+ }
26
+ // Load global settings
27
+ const settings = (await payload.findGlobal({
28
+ slug: 'nitrogen-settings',
29
+ }));
30
+ const isDevelopment = params.development === 'true';
31
+ const siteUrl = isDevelopment ? settings.developmentUrl : settings.frontendUrl;
32
+ const nitrogenConfig = settings.nitrogenConfig || {};
33
+ // Determine the postType for the editor's API calls
34
+ let postType;
35
+ if (params.collection) {
36
+ postType = params.collection;
37
+ }
38
+ else if (params.templateId) {
39
+ postType = 'nitrogen-templates';
40
+ }
41
+ else {
42
+ return redirect('/admin');
43
+ }
44
+ // Build the config object the builder reads from window.nitrogenConfig
45
+ const builderConfig = {
46
+ licenseKey: settings.licenseKey || '',
47
+ provider: {
48
+ type: 'payload',
49
+ apiUrl: '/api/nitrogen/v1',
50
+ collection: postType,
51
+ },
52
+ siteUrl: siteUrl || '',
53
+ urlMaps: nitrogenConfig.urlMaps || [],
54
+ wysiwygColors: nitrogenConfig.wysiwygColors || {},
55
+ cssInjection: nitrogenConfig.cssInjection || '',
56
+ };
57
+ const editId = params.templateId || params.pageId || '';
58
+ const isEditorDev = process.env.NITROGEN_EDITOR_DEV === 'true';
59
+ const viteOrigin = 'http://localhost:5173';
60
+ const cdnBase = 'https://cdn.jsdelivr.net/npm/@nitrogenbuilder/editor@0.4';
61
+ return (_jsxs(_Fragment, { children: [isEditorDev ? (_jsxs(_Fragment, { children: [_jsx("link", { rel: "stylesheet", crossOrigin: "", href: `${viteOrigin}/fa620pro/css/all.css` }), _jsx("link", { rel: "stylesheet", crossOrigin: "", href: `${viteOrigin}/fa620pro/css/sharp-solid.css` }), _jsx("link", { rel: "stylesheet", crossOrigin: "", href: `${viteOrigin}/src/index.scss` })] })) : (_jsxs(_Fragment, { children: [_jsx("link", { rel: "stylesheet", crossOrigin: "", href: `${cdnBase}/fa620pro/css/all.css` }), _jsx("link", { rel: "stylesheet", crossOrigin: "", href: `${cdnBase}/fa620pro/css/sharp-solid.css` }), _jsx("link", { rel: "stylesheet", crossOrigin: "", href: `${cdnBase}/index.css` })] })), _jsx("script", { dangerouslySetInnerHTML: {
62
+ __html: [
63
+ `window.nitrogenConfig = ${JSON.stringify(builderConfig)};`,
64
+ `window.nitrogenEditId = "${editId}";`,
65
+ // Inject authorId into the URL so the editor can read it from window.location.search
66
+ `(function(){var u=new URL(window.location.href);if(!u.searchParams.has("authorId")){u.searchParams.set("authorId",${JSON.stringify(String(user.id))});window.history.replaceState(null,"",u.toString())}})();`,
67
+ ].join(''),
68
+ } }), _jsxs("div", { id: "root", children: [_jsx("div", { style: {
69
+ position: 'fixed',
70
+ inset: 0,
71
+ zIndex: 9999999,
72
+ display: 'flex',
73
+ alignItems: 'center',
74
+ justifyContent: 'center',
75
+ backgroundColor: '#0F1214',
76
+ }, children: _jsx("img", { src: isEditorDev
77
+ ? `${viteOrigin}/src/components/logo-white.svg`
78
+ : `${cdnBase}/logo-white.svg`, alt: "Loading\u2026", style: {
79
+ width: '24rem',
80
+ maxWidth: '100%',
81
+ animation: 'nitrogen-loader-pulse 3s ease-in-out infinite',
82
+ } }) }), _jsx("style", { dangerouslySetInnerHTML: {
83
+ __html: `@keyframes nitrogen-loader-pulse{0%{transform:scale(1);opacity:0}50%{opacity:1}100%{transform:scale(1.05);opacity:0}}`,
84
+ } })] }), isEditorDev ? (_jsxs(_Fragment, { children: [_jsx("script", { type: "module", dangerouslySetInnerHTML: {
85
+ __html: `import RefreshRuntime from "${viteOrigin}/@react-refresh";RefreshRuntime.injectIntoGlobalHook(window);window.$RefreshReg$ = () => {};window.$RefreshSig$ = () => (type) => type;window.__vite_plugin_react_preamble_installed__ = true;`,
86
+ } }), _jsx("script", { type: "module", src: `${viteOrigin}/@vite/client` }), _jsx("script", { type: "module", crossOrigin: "", src: `${viteOrigin}/src/main.tsx` })] })) : (_jsx("script", { type: "module", crossOrigin: "", src: `${cdnBase}/index.js` }))] }));
87
+ }
@@ -0,0 +1,2 @@
1
+ export { default as NitrogenEditorPage } from './NitrogenEditorPage';
2
+ export { default as NitrogenEditorLayout } from './NitrogenEditorLayout';
@@ -0,0 +1,2 @@
1
+ export { default as NitrogenEditorPage } from './NitrogenEditorPage';
2
+ export { default as NitrogenEditorLayout } from './NitrogenEditorLayout';
@@ -0,0 +1,2 @@
1
+ import type { Endpoint } from 'payload';
2
+ export declare const allEndpoints: Endpoint[];
@@ -0,0 +1,48 @@
1
+ import { getNitrogenSettings } from './helpers';
2
+ import { resolveCollection } from '../collection-registry';
3
+ export const allEndpoints = [
4
+ // GET /api/nitrogen/v1/all — List all items with filtering
5
+ {
6
+ path: '/nitrogen/v1/all',
7
+ method: 'get',
8
+ handler: async (req) => {
9
+ const { payload } = req;
10
+ const settings = await getNitrogenSettings(payload);
11
+ const url = new URL(req.url || '', 'http://localhost');
12
+ const postType = url.searchParams.get('post_type') || '';
13
+ if (!postType) {
14
+ return Response.json({ error: 'post_type query parameter is required' }, { status: 400 });
15
+ }
16
+ const statusParam = url.searchParams.get('post_status') || 'published';
17
+ const paged = parseInt(url.searchParams.get('paged') || '1', 10);
18
+ const perPage = parseInt(url.searchParams.get('per_page') || '20', 10);
19
+ const statuses = statusParam.split(',').map((s) => s.trim());
20
+ const collection = resolveCollection(postType);
21
+ const where = {};
22
+ if (!statuses.includes('any')) {
23
+ where.status = { in: statuses };
24
+ }
25
+ try {
26
+ const result = await payload.find({
27
+ collection: collection,
28
+ where,
29
+ page: paged,
30
+ limit: perPage,
31
+ depth: 0,
32
+ });
33
+ const items = result.docs.map((doc) => ({
34
+ id: doc.id,
35
+ title: String(doc.title || ''),
36
+ slug: String(doc.slug || ''),
37
+ permalink: `${settings.frontendUrl || ''}/${String(doc.slug || '')}`,
38
+ relative_permalink: `/${String(doc.slug || '')}`,
39
+ type: collection,
40
+ }));
41
+ return Response.json(items);
42
+ }
43
+ catch {
44
+ return Response.json({ error: `Collection "${collection}" not found` }, { status: 400 });
45
+ }
46
+ },
47
+ },
48
+ ];
@@ -0,0 +1,8 @@
1
+ import type { Endpoint } from 'payload';
2
+ /**
3
+ * Creates a complete set of CRUD endpoints for a given Payload collection.
4
+ *
5
+ * @param collectionSlug - The Payload collection slug to query (e.g., 'pages', 'posts')
6
+ * @param endpointPrefix - The URL path prefix (e.g., 'pages', 'posts')
7
+ */
8
+ export declare function createCollectionEndpoints(collectionSlug: string, endpointPrefix: string): Endpoint[];
@@ -0,0 +1,168 @@
1
+ import { getNitrogenSettings, buildDynamicData, buildPageResponse, buildListItemResponse, requireAuth, } from './helpers';
2
+ /**
3
+ * Creates a complete set of CRUD endpoints for a given Payload collection.
4
+ *
5
+ * @param collectionSlug - The Payload collection slug to query (e.g., 'pages', 'posts')
6
+ * @param endpointPrefix - The URL path prefix (e.g., 'pages', 'posts')
7
+ */
8
+ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
9
+ const collection = collectionSlug;
10
+ return [
11
+ // GET /api/nitrogen/v1/{prefix} — List items
12
+ {
13
+ path: `/nitrogen/v1/${endpointPrefix}`,
14
+ method: 'get',
15
+ handler: async (req) => {
16
+ const { payload } = req;
17
+ const settings = await getNitrogenSettings(payload);
18
+ const url = new URL(req.url || '', 'http://localhost');
19
+ const statusParam = url.searchParams.get('post_status') || 'published';
20
+ const statuses = statusParam.split(',').map((s) => s.trim());
21
+ const where = {};
22
+ if (!statuses.includes('any')) {
23
+ where.status = { in: statuses };
24
+ }
25
+ const result = await payload.find({
26
+ collection,
27
+ where,
28
+ limit: 0,
29
+ depth: 1,
30
+ });
31
+ const items = result.docs.map((doc) => buildListItemResponse(doc, settings));
32
+ return Response.json(items);
33
+ },
34
+ },
35
+ // POST /api/nitrogen/v1/{prefix} — Create a new item
36
+ {
37
+ path: `/nitrogen/v1/${endpointPrefix}`,
38
+ method: 'post',
39
+ handler: async (req) => {
40
+ const authError = requireAuth(req);
41
+ if (authError)
42
+ return authError;
43
+ const { payload } = req;
44
+ const body = (await req.json?.());
45
+ const data = body?.data || body;
46
+ const doc = await payload.create({
47
+ collection,
48
+ data: {
49
+ title: data?.title || 'Untitled',
50
+ slug: data?.slug ||
51
+ (data?.title || 'untitled')
52
+ .toLowerCase()
53
+ .replace(/[^a-z0-9]+/g, '-')
54
+ .replace(/(^-|-$)/g, ''),
55
+ author: data?.author || (req.user && 'id' in req.user ? req.user.id : undefined),
56
+ status: 'draft',
57
+ },
58
+ });
59
+ return Response.json({ data: { id: doc.id } });
60
+ },
61
+ },
62
+ // GET /api/nitrogen/v1/{prefix}/:id — Get a single item
63
+ {
64
+ path: `/nitrogen/v1/${endpointPrefix}/:id`,
65
+ method: 'get',
66
+ handler: async (req) => {
67
+ const { payload, routeParams } = req;
68
+ const id = routeParams?.id;
69
+ try {
70
+ const doc = (await payload.findByID({
71
+ collection,
72
+ id,
73
+ depth: 1,
74
+ }));
75
+ const settings = await getNitrogenSettings(payload);
76
+ const dynamicData = buildDynamicData(doc, settings);
77
+ return Response.json(buildPageResponse(doc, settings, dynamicData));
78
+ }
79
+ catch {
80
+ return Response.json({ error: 'Not found' }, { status: 404 });
81
+ }
82
+ },
83
+ },
84
+ // PATCH /api/nitrogen/v1/{prefix}/:id — Update an item
85
+ {
86
+ path: `/nitrogen/v1/${endpointPrefix}/:id`,
87
+ method: 'patch',
88
+ handler: async (req) => {
89
+ const authError = requireAuth(req);
90
+ if (authError)
91
+ return authError;
92
+ const { payload, routeParams } = req;
93
+ const id = routeParams?.id;
94
+ const body = (await req.json?.());
95
+ // Only use body.data as wrapper if it's an object (not a string which is page content)
96
+ const data = (body?.data && typeof body.data === 'object') ? body.data : body;
97
+ if (!data?.title) {
98
+ return Response.json({ error: 'Title is required' }, { status: 400 });
99
+ }
100
+ const updateData = { title: data.title };
101
+ if (data.author) {
102
+ updateData.author = data.author;
103
+ }
104
+ if ('data' in data && data.data !== undefined) {
105
+ updateData.nitrogenData =
106
+ typeof data.data === 'string' ? JSON.parse(data.data) : data.data;
107
+ }
108
+ if (data.settings !== undefined) {
109
+ updateData.pageSettings = data.settings;
110
+ }
111
+ await payload.update({
112
+ collection,
113
+ id,
114
+ data: updateData,
115
+ });
116
+ return Response.json({ success: true });
117
+ },
118
+ },
119
+ // POST /api/nitrogen/v1/{prefix}/slug — Get item by slug (POST variant)
120
+ {
121
+ path: `/nitrogen/v1/${endpointPrefix}/slug`,
122
+ method: 'post',
123
+ handler: async (req) => {
124
+ const { payload } = req;
125
+ const body = (await req.json?.());
126
+ const slug = body?.slug || body?.data?.slug;
127
+ if (!slug) {
128
+ return Response.json({ error: 'Slug is required' }, { status: 400 });
129
+ }
130
+ const result = await payload.find({
131
+ collection,
132
+ where: { slug: { equals: slug } },
133
+ limit: 1,
134
+ depth: 1,
135
+ });
136
+ if (!result.docs.length) {
137
+ return Response.json({ error: 'Not found' }, { status: 404 });
138
+ }
139
+ const doc = result.docs[0];
140
+ const settings = await getNitrogenSettings(payload);
141
+ const dynamicData = buildDynamicData(doc, settings);
142
+ return Response.json(buildPageResponse(doc, settings, dynamicData));
143
+ },
144
+ },
145
+ // GET /api/nitrogen/v1/{prefix}/slug/:slug — Get item by slug
146
+ {
147
+ path: `/nitrogen/v1/${endpointPrefix}/slug/:slug`,
148
+ method: 'get',
149
+ handler: async (req) => {
150
+ const { payload, routeParams } = req;
151
+ const slug = routeParams?.slug;
152
+ const result = await payload.find({
153
+ collection,
154
+ where: { slug: { equals: slug } },
155
+ limit: 1,
156
+ depth: 1,
157
+ });
158
+ if (!result.docs.length) {
159
+ return Response.json({ error: 'Not found' }, { status: 404 });
160
+ }
161
+ const doc = result.docs[0];
162
+ const settings = await getNitrogenSettings(payload);
163
+ const dynamicData = buildDynamicData(doc, settings);
164
+ return Response.json(buildPageResponse(doc, settings, dynamicData));
165
+ },
166
+ },
167
+ ];
168
+ }