@eyeballs/cms 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.
- package/README.md +74 -0
- package/dist/client.d.ts +33 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +96 -0
- package/dist/client.js.map +1 -0
- package/dist/helpers.d.ts +9 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +36 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/next.d.ts +9 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +54 -0
- package/dist/next.js.map +1 -0
- package/dist/types.d.ts +138 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/content-api.md +530 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @eyeballs/cms
|
|
2
|
+
|
|
3
|
+
Shared Eyeballs CMS API types, client helpers, Next.js revalidation helpers, and API docs for
|
|
4
|
+
starter sites and custom website projects.
|
|
5
|
+
|
|
6
|
+
## Install In A Website Project
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @eyeballs/cms
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Vercel installs the package during build like any other npm dependency. Keep site-specific secrets
|
|
13
|
+
in the Vercel project environment:
|
|
14
|
+
|
|
15
|
+
```txt
|
|
16
|
+
EYEBALLS_CMS_URL=https://app.example.com
|
|
17
|
+
EYEBALLS_CMS_TOKEN=eyb_...
|
|
18
|
+
REVALIDATE_SECRET=eyb_reval_...
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Basic Usage
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createEyeballsCms, type CmsVideo } from '@eyeballs/cms';
|
|
25
|
+
|
|
26
|
+
const cms = createEyeballsCms({
|
|
27
|
+
baseUrl: process.env.EYEBALLS_CMS_URL!,
|
|
28
|
+
token: process.env.EYEBALLS_CMS_TOKEN!,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const videos: CmsVideo[] = await cms.videos.list();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Next.js Revalidation Route
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// app/api/revalidate/route.ts
|
|
38
|
+
import { createEyeballsRevalidateHandler } from '@eyeballs/cms/next';
|
|
39
|
+
|
|
40
|
+
export const POST = createEyeballsRevalidateHandler({
|
|
41
|
+
secret: process.env.REVALIDATE_SECRET,
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Agent Instructions For Starter Sites
|
|
46
|
+
|
|
47
|
+
Add this to the starter site's `AGENTS.md`:
|
|
48
|
+
|
|
49
|
+
```md
|
|
50
|
+
Before using the Eyeballs CMS API, read:
|
|
51
|
+
node_modules/@eyeballs/cms/docs/content-api.md
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Publishing
|
|
55
|
+
|
|
56
|
+
Build locally:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm run cms:build
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Preview the npm package contents:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm run cms:pack:dry
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Publish after bumping `packages/cms/package.json` version:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm run cms:publish
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This package contains no account tokens or site-specific config.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { CmsContentEntry, CmsRosterProfile, CmsVideo } from './types.js';
|
|
2
|
+
export type EyeballsCmsClientOptions = {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
token: string;
|
|
5
|
+
fetch?: typeof fetch;
|
|
6
|
+
defaultRequestInit?: RequestInit;
|
|
7
|
+
};
|
|
8
|
+
export declare class EyeballsCmsError extends Error {
|
|
9
|
+
status: number;
|
|
10
|
+
payload: unknown;
|
|
11
|
+
constructor(message: string, status: number, payload: unknown);
|
|
12
|
+
}
|
|
13
|
+
export declare function createEyeballsCms(options: EyeballsCmsClientOptions): {
|
|
14
|
+
request: <T>(path: string, init?: RequestInit) => Promise<T>;
|
|
15
|
+
videos: {
|
|
16
|
+
list<Fields extends Record<string, unknown> = Record<string, unknown>>(): Promise<CmsVideo<Fields>[]>;
|
|
17
|
+
get<Fields extends Record<string, unknown> = Record<string, unknown>>(slug: string): Promise<CmsVideo<Fields>>;
|
|
18
|
+
};
|
|
19
|
+
roster: {
|
|
20
|
+
list<Fields extends Record<string, unknown> = Record<string, unknown>>(): Promise<CmsRosterProfile<Fields>[]>;
|
|
21
|
+
get<Fields extends Record<string, unknown> = Record<string, unknown>>(slug: string): Promise<CmsRosterProfile<Fields>>;
|
|
22
|
+
};
|
|
23
|
+
content: {
|
|
24
|
+
list<Fields extends Record<string, unknown> = Record<string, unknown>>(options?: {
|
|
25
|
+
type?: string;
|
|
26
|
+
slug?: string;
|
|
27
|
+
}): Promise<CmsContentEntry<Fields>[]>;
|
|
28
|
+
getBySlug<Fields extends Record<string, unknown> = Record<string, unknown>>(slug: string): Promise<CmsContentEntry<Fields>>;
|
|
29
|
+
getByTypeAndSlug<Fields extends Record<string, unknown> = Record<string, unknown>>(type: string, slug: string): Promise<CmsContentEntry<Fields>>;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export type EyeballsCmsClient = ReturnType<typeof createEyeballsCms>;
|
|
33
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE9E,MAAM,MAAM,wBAAwB,GAAG;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,kBAAkB,CAAC,EAAE,WAAW,CAAC;CACjC,CAAC;AAEF,qBAAa,gBAAiB,SAAQ,KAAK;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;gBAEL,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;CAM7D;AAsBD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB;cAO3C,CAAC,QAAQ,MAAM,SAAQ,WAAW,KAAQ,OAAO,CAAC,CAAC,CAAC;;aA2B9D,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;YAIvC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kCACzC,MAAM;;;aASF,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;YAMvC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kCACzC,MAAM;;;aASF,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,sCAAsC;YACtF,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,IAAI,CAAC,EAAE,MAAM,CAAC;SACd;kBASe,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kCAC/C,MAAM;yBAQZ,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kCAC/B,MAAM,QAAQ,MAAM;;EAQ9B;AAED,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export class EyeballsCmsError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
payload;
|
|
4
|
+
constructor(message, status, payload) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'EyeballsCmsError';
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.payload = payload;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function normalizeBaseUrl(baseUrl) {
|
|
12
|
+
const trimmed = baseUrl.trim().replace(/\/+$/, '');
|
|
13
|
+
if (!trimmed)
|
|
14
|
+
throw new Error('Eyeballs CMS baseUrl is required');
|
|
15
|
+
return trimmed;
|
|
16
|
+
}
|
|
17
|
+
function joinPath(baseUrl, path) {
|
|
18
|
+
return `${baseUrl}/${path.replace(/^\/+/, '')}`;
|
|
19
|
+
}
|
|
20
|
+
function withQuery(path, params) {
|
|
21
|
+
const query = new URLSearchParams();
|
|
22
|
+
for (const [key, value] of Object.entries(params)) {
|
|
23
|
+
const trimmed = value?.trim();
|
|
24
|
+
if (trimmed)
|
|
25
|
+
query.set(key, trimmed);
|
|
26
|
+
}
|
|
27
|
+
const queryString = query.toString();
|
|
28
|
+
return queryString ? `${path}?${queryString}` : path;
|
|
29
|
+
}
|
|
30
|
+
export function createEyeballsCms(options) {
|
|
31
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
32
|
+
const token = options.token.trim();
|
|
33
|
+
if (!token)
|
|
34
|
+
throw new Error('Eyeballs CMS token is required');
|
|
35
|
+
const requestFetch = options.fetch ?? fetch;
|
|
36
|
+
async function request(path, init = {}) {
|
|
37
|
+
const headers = new Headers(options.defaultRequestInit?.headers);
|
|
38
|
+
new Headers(init.headers).forEach((value, key) => headers.set(key, value));
|
|
39
|
+
headers.set('authorization', `Bearer ${token}`);
|
|
40
|
+
const res = await requestFetch(joinPath(baseUrl, path), {
|
|
41
|
+
...options.defaultRequestInit,
|
|
42
|
+
...init,
|
|
43
|
+
headers,
|
|
44
|
+
});
|
|
45
|
+
const text = await res.text();
|
|
46
|
+
const payload = text ? JSON.parse(text) : null;
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
const message = payload && typeof payload === 'object' && 'error' in payload
|
|
49
|
+
? String(payload.error)
|
|
50
|
+
: `Eyeballs CMS request failed: ${res.status}`;
|
|
51
|
+
throw new EyeballsCmsError(message, res.status, payload);
|
|
52
|
+
}
|
|
53
|
+
return payload;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
request,
|
|
57
|
+
videos: {
|
|
58
|
+
async list() {
|
|
59
|
+
const payload = await request('/api/cms/videos');
|
|
60
|
+
return payload.videos;
|
|
61
|
+
},
|
|
62
|
+
async get(slug) {
|
|
63
|
+
const payload = await request(`/api/cms/videos/${encodeURIComponent(slug)}`);
|
|
64
|
+
return payload.video;
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
roster: {
|
|
68
|
+
async list() {
|
|
69
|
+
const payload = await request('/api/cms/roster');
|
|
70
|
+
return payload.roster;
|
|
71
|
+
},
|
|
72
|
+
async get(slug) {
|
|
73
|
+
const payload = await request(`/api/cms/roster/${encodeURIComponent(slug)}`);
|
|
74
|
+
return payload.roster;
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
content: {
|
|
78
|
+
async list(options) {
|
|
79
|
+
const payload = await request(withQuery('/api/cms/content', {
|
|
80
|
+
type: options?.type,
|
|
81
|
+
slug: options?.slug,
|
|
82
|
+
}));
|
|
83
|
+
return payload.content;
|
|
84
|
+
},
|
|
85
|
+
async getBySlug(slug) {
|
|
86
|
+
const payload = await request(`/api/cms/content/${encodeURIComponent(slug)}`);
|
|
87
|
+
return payload.content;
|
|
88
|
+
},
|
|
89
|
+
async getByTypeAndSlug(type, slug) {
|
|
90
|
+
const payload = await request(withQuery('/api/cms/content', { type, slug }));
|
|
91
|
+
return payload.content;
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CmsContentEntry, CmsRosterProfile, CmsVideo } from './types.js';
|
|
2
|
+
export declare function trimSlashes(value: string): string;
|
|
3
|
+
export declare function getContentEntryPath(entry: CmsContentEntry): string | null;
|
|
4
|
+
export declare function getContentTypePath(typeSlug: string): string | null;
|
|
5
|
+
export declare function getRosterProfilePath(profile: Pick<CmsRosterProfile, 'slug' | 'type'>, pathPrefix?: string | null): string | null;
|
|
6
|
+
export declare function getMuxPosterUrl(video: CmsVideo, options?: {
|
|
7
|
+
time?: number;
|
|
8
|
+
}): string | null;
|
|
9
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE9E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,UAExC;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,iBAWzD;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,iBAGlD;AAED,wBAAgB,oBAAoB,CACnC,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAAC,EAChD,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,iBAM1B;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,iBAO/E"}
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function trimSlashes(value) {
|
|
2
|
+
return value.trim().replace(/^\/+|\/+$/g, '');
|
|
3
|
+
}
|
|
4
|
+
export function getContentEntryPath(entry) {
|
|
5
|
+
if (entry.type?.kind === 'pages') {
|
|
6
|
+
const websitePath = entry.website_path?.trim();
|
|
7
|
+
if (!websitePath || websitePath === '/')
|
|
8
|
+
return '/';
|
|
9
|
+
return websitePath.startsWith('/') ? websitePath : `/${websitePath}`;
|
|
10
|
+
}
|
|
11
|
+
const typeSlug = entry.type?.slug ? trimSlashes(entry.type.slug) : '';
|
|
12
|
+
const entrySlug = trimSlashes(entry.slug);
|
|
13
|
+
if (!typeSlug || !entrySlug)
|
|
14
|
+
return null;
|
|
15
|
+
return `/${typeSlug}/${entrySlug}`;
|
|
16
|
+
}
|
|
17
|
+
export function getContentTypePath(typeSlug) {
|
|
18
|
+
const normalized = trimSlashes(typeSlug);
|
|
19
|
+
return normalized ? `/${normalized}` : null;
|
|
20
|
+
}
|
|
21
|
+
export function getRosterProfilePath(profile, pathPrefix) {
|
|
22
|
+
const prefix = trimSlashes(pathPrefix || profile.type?.slug || '');
|
|
23
|
+
const slug = trimSlashes(profile.slug);
|
|
24
|
+
if (!prefix || !slug)
|
|
25
|
+
return null;
|
|
26
|
+
return `/${prefix}/${slug}`;
|
|
27
|
+
}
|
|
28
|
+
export function getMuxPosterUrl(video, options = {}) {
|
|
29
|
+
const playbackId = video.media.mux_playback_id;
|
|
30
|
+
if (!playbackId)
|
|
31
|
+
return video.media.poster_url;
|
|
32
|
+
const time = options.time ?? video.media.thumbnail_time;
|
|
33
|
+
const query = typeof time === 'number' && Number.isFinite(time) ? `?time=${time}` : '';
|
|
34
|
+
return `https://image.mux.com/${playbackId}/thumbnail.jpg${query}`;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,WAAW,CAAC,KAAa;IACxC,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAsB;IACzD,IAAI,KAAK,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,EAAE,CAAC;QAClC,MAAM,WAAW,GAAG,KAAK,CAAC,YAAY,EAAE,IAAI,EAAE,CAAC;QAC/C,IAAI,CAAC,WAAW,IAAI,WAAW,KAAK,GAAG;YAAE,OAAO,GAAG,CAAC;QACpD,OAAO,WAAW,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC;IACtE,CAAC;IAED,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACtE,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,CAAC,QAAQ,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IACzC,OAAO,IAAI,QAAQ,IAAI,SAAS,EAAE,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IAClD,MAAM,UAAU,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IACzC,OAAO,UAAU,CAAC,CAAC,CAAC,IAAI,UAAU,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,oBAAoB,CACnC,OAAgD,EAChD,UAA0B;IAE1B,MAAM,MAAM,GAAG,WAAW,CAAC,UAAU,IAAI,OAAO,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;IACnE,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAClC,OAAO,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAe,EAAE,UAA6B,EAAE;IAC/E,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC,eAAe,CAAC;IAC/C,IAAI,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC;IAE/C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,cAAc,CAAC;IACxD,MAAM,KAAK,GAAG,OAAO,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACvF,OAAO,yBAAyB,UAAU,iBAAiB,KAAK,EAAE,CAAC;AACpE,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type EyeballsRevalidationPayload = {
|
|
2
|
+
tags?: string[];
|
|
3
|
+
paths?: string[];
|
|
4
|
+
};
|
|
5
|
+
export type EyeballsRevalidateHandlerOptions = {
|
|
6
|
+
secret: string | null | undefined;
|
|
7
|
+
};
|
|
8
|
+
export declare function createEyeballsRevalidateHandler(options: EyeballsRevalidateHandlerOptions): (req: Request) => Promise<Response>;
|
|
9
|
+
//# sourceMappingURL=next.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,2BAA2B,GAAG;IACzC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,gCAAgC,GAAG;IAC9C,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAClC,CAAC;AA6BF,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,gCAAgC,IAG7D,KAAK,OAAO,uBA2BvC"}
|
package/dist/next.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { revalidatePath, revalidateTag } from 'next/cache';
|
|
2
|
+
function json(payload, status = 200) {
|
|
3
|
+
return new Response(JSON.stringify(payload), {
|
|
4
|
+
status,
|
|
5
|
+
headers: { 'content-type': 'application/json' },
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
function getHeader(req, name) {
|
|
9
|
+
return req.headers.get(name)?.trim() || '';
|
|
10
|
+
}
|
|
11
|
+
function normalizePath(path) {
|
|
12
|
+
if (typeof path !== 'string')
|
|
13
|
+
return null;
|
|
14
|
+
const trimmed = path.trim();
|
|
15
|
+
return trimmed.startsWith('/') ? trimmed : null;
|
|
16
|
+
}
|
|
17
|
+
function normalizeTag(tag) {
|
|
18
|
+
if (typeof tag !== 'string')
|
|
19
|
+
return null;
|
|
20
|
+
const trimmed = tag.trim();
|
|
21
|
+
return trimmed || null;
|
|
22
|
+
}
|
|
23
|
+
function isString(value) {
|
|
24
|
+
return typeof value === 'string';
|
|
25
|
+
}
|
|
26
|
+
export function createEyeballsRevalidateHandler(options) {
|
|
27
|
+
const expectedSecret = options.secret?.trim() || '';
|
|
28
|
+
return async function POST(req) {
|
|
29
|
+
if (!expectedSecret) {
|
|
30
|
+
return json({ ok: false, error: 'Missing revalidation secret' }, 500);
|
|
31
|
+
}
|
|
32
|
+
const providedSecret = getHeader(req, 'x-revalidate-secret');
|
|
33
|
+
if (providedSecret !== expectedSecret) {
|
|
34
|
+
return json({ ok: false, error: 'Unauthorized' }, 401);
|
|
35
|
+
}
|
|
36
|
+
let body = {};
|
|
37
|
+
try {
|
|
38
|
+
body = (await req.json());
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return json({ ok: false, error: 'Invalid JSON body' }, 400);
|
|
42
|
+
}
|
|
43
|
+
const tags = Array.isArray(body.tags) ? body.tags.map(normalizeTag).filter(isString) : [];
|
|
44
|
+
const paths = Array.isArray(body.paths)
|
|
45
|
+
? body.paths.map(normalizePath).filter(isString)
|
|
46
|
+
: [];
|
|
47
|
+
for (const tag of tags)
|
|
48
|
+
revalidateTag(tag, 'max');
|
|
49
|
+
for (const path of paths)
|
|
50
|
+
revalidatePath(path, 'page');
|
|
51
|
+
return json({ ok: true, tags, paths });
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=next.js.map
|
package/dist/next.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next.js","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export type CreditEntry = {
|
|
2
|
+
id: string;
|
|
3
|
+
role: string;
|
|
4
|
+
name: string;
|
|
5
|
+
url: string | null;
|
|
6
|
+
};
|
|
7
|
+
export type ContentFieldType = 'text' | 'textarea' | 'richtext' | 'boolean' | 'image' | 'date' | 'library_work' | 'content_blocks' | 'array';
|
|
8
|
+
export type SimpleContentFieldType = Exclude<ContentFieldType, 'library_work' | 'content_blocks' | 'array'>;
|
|
9
|
+
export type ArrayItemContentFieldType = SimpleContentFieldType | 'library_work';
|
|
10
|
+
export type ContentField = {
|
|
11
|
+
id: string;
|
|
12
|
+
type: ContentFieldType;
|
|
13
|
+
label: string;
|
|
14
|
+
multiple?: boolean;
|
|
15
|
+
required?: boolean;
|
|
16
|
+
itemFields?: ContentField[];
|
|
17
|
+
itemLabelFieldId?: string;
|
|
18
|
+
itemSecondaryLabelFieldId?: string;
|
|
19
|
+
};
|
|
20
|
+
export type TextBlock = {
|
|
21
|
+
id: string;
|
|
22
|
+
type: 'text';
|
|
23
|
+
content: string;
|
|
24
|
+
};
|
|
25
|
+
export type ImageBlock = {
|
|
26
|
+
id: string;
|
|
27
|
+
type: 'image';
|
|
28
|
+
src: string;
|
|
29
|
+
path: string;
|
|
30
|
+
alt: string;
|
|
31
|
+
showCaption: boolean;
|
|
32
|
+
};
|
|
33
|
+
export type ButtonBlock = {
|
|
34
|
+
id: string;
|
|
35
|
+
type: 'button';
|
|
36
|
+
text: string;
|
|
37
|
+
url: string;
|
|
38
|
+
};
|
|
39
|
+
export type ContentBlock = TextBlock | ImageBlock | ButtonBlock;
|
|
40
|
+
export type ContentBlocks = ContentBlock[];
|
|
41
|
+
export type CmsVideoMedia = {
|
|
42
|
+
mux_playback_id: string | null;
|
|
43
|
+
poster_url: string | null;
|
|
44
|
+
thumbnail_time: number | null;
|
|
45
|
+
mux_blur_placeholder: string | null;
|
|
46
|
+
mux_blur_aspect_ratio: number | null;
|
|
47
|
+
loop_start_time: number | null;
|
|
48
|
+
loop_end_time: number | null;
|
|
49
|
+
loop_mux_playback_id: string | null;
|
|
50
|
+
loop_mux_asset_status: string | null;
|
|
51
|
+
aspect_ratio: number | null;
|
|
52
|
+
custom_aspect_ratio: number | null;
|
|
53
|
+
duration_seconds: number | null;
|
|
54
|
+
};
|
|
55
|
+
export type CmsVideoRosterAssignment = {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
url: string | null;
|
|
59
|
+
roster: {
|
|
60
|
+
id: string;
|
|
61
|
+
name: string;
|
|
62
|
+
slug: string;
|
|
63
|
+
image_url: string | null;
|
|
64
|
+
description: string | null;
|
|
65
|
+
links: unknown;
|
|
66
|
+
} | null;
|
|
67
|
+
};
|
|
68
|
+
export type CmsVideoRosterGroup = {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
slug: string;
|
|
72
|
+
assignments: CmsVideoRosterAssignment[];
|
|
73
|
+
};
|
|
74
|
+
export type CmsVideo<Fields extends Record<string, unknown> = Record<string, unknown>> = {
|
|
75
|
+
id: string;
|
|
76
|
+
title: string;
|
|
77
|
+
slug: string;
|
|
78
|
+
description: string | null;
|
|
79
|
+
client: string | null;
|
|
80
|
+
credits: CreditEntry[];
|
|
81
|
+
tags: string[];
|
|
82
|
+
roster: CmsVideoRosterGroup[];
|
|
83
|
+
fields: Fields;
|
|
84
|
+
field_schema: ContentField[];
|
|
85
|
+
published_at: string | null;
|
|
86
|
+
release_date: string | null;
|
|
87
|
+
media: CmsVideoMedia;
|
|
88
|
+
created_at: string;
|
|
89
|
+
};
|
|
90
|
+
export type CmsRosterProfile<Fields extends Record<string, unknown> = Record<string, unknown>> = {
|
|
91
|
+
id: string;
|
|
92
|
+
name: string;
|
|
93
|
+
slug: string;
|
|
94
|
+
image_url: string | null;
|
|
95
|
+
description: string | null;
|
|
96
|
+
links: unknown;
|
|
97
|
+
fields: Fields;
|
|
98
|
+
status: string;
|
|
99
|
+
order_index: number;
|
|
100
|
+
created_at: string;
|
|
101
|
+
updated_at: string;
|
|
102
|
+
type: {
|
|
103
|
+
id: string;
|
|
104
|
+
name: string;
|
|
105
|
+
singular_name: string;
|
|
106
|
+
slug: string;
|
|
107
|
+
field_schema: ContentField[];
|
|
108
|
+
} | null;
|
|
109
|
+
featured_work: CmsVideo[];
|
|
110
|
+
};
|
|
111
|
+
export type CmsContentEntry<Fields extends Record<string, unknown> = Record<string, unknown>> = {
|
|
112
|
+
id: string;
|
|
113
|
+
name: string;
|
|
114
|
+
slug: string;
|
|
115
|
+
status: 'draft' | 'published' | 'archived';
|
|
116
|
+
entry_date: string | null;
|
|
117
|
+
website_path: string | null;
|
|
118
|
+
meta: {
|
|
119
|
+
title: string | null;
|
|
120
|
+
description: string | null;
|
|
121
|
+
image_url: string | null;
|
|
122
|
+
};
|
|
123
|
+
fields: Fields;
|
|
124
|
+
field_schema: ContentField[];
|
|
125
|
+
type: {
|
|
126
|
+
id: string;
|
|
127
|
+
name: string;
|
|
128
|
+
slug: string;
|
|
129
|
+
kind: 'pages' | 'collection';
|
|
130
|
+
} | null;
|
|
131
|
+
published_at: string | null;
|
|
132
|
+
created_at: string;
|
|
133
|
+
updated_at: string;
|
|
134
|
+
};
|
|
135
|
+
export type EyeballsCmsErrorPayload = {
|
|
136
|
+
error: string;
|
|
137
|
+
};
|
|
138
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
# Content API Reference
|
|
2
|
+
|
|
3
|
+
This is the canonical reference for the Eyeballs CMS/content API used by starter sites,
|
|
4
|
+
website projects, external integrations, and coding agents. Update this file whenever the
|
|
5
|
+
public CMS API response shape, routing behavior, custom field behavior, or website settings
|
|
6
|
+
change.
|
|
7
|
+
|
|
8
|
+
The API is implemented in the Eyeballs app by `src/app/api/cms/*`. Response shaping lives mostly
|
|
9
|
+
in `src/lib/cmsData.ts`. Field/schema normalization lives in `src/lib/content.ts`. Website preview
|
|
10
|
+
and revalidation URL helpers live in `src/lib/websiteProjectUrl.ts` and
|
|
11
|
+
`src/lib/publicRevalidation.ts`.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
Use Dashboard > Settings > API settings to create a CMS API token. Starter sites normally store:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
EYEBALLS_CMS_URL=https://app.example.com
|
|
19
|
+
EYEBALLS_CMS_TOKEN=eyb_...
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Starter sites should use the shared package when available:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { createEyeballsCms } from '@eyeballs/cms';
|
|
26
|
+
|
|
27
|
+
const cms = createEyeballsCms({
|
|
28
|
+
baseUrl: process.env.EYEBALLS_CMS_URL!,
|
|
29
|
+
token: process.env.EYEBALLS_CMS_TOKEN!,
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Every request must include either a bearer token or an `x-api-key` header:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
async function cmsFetch(path: string) {
|
|
37
|
+
const res = await fetch(`${process.env.EYEBALLS_CMS_URL}${path}`, {
|
|
38
|
+
headers: {
|
|
39
|
+
authorization: `Bearer ${process.env.EYEBALLS_CMS_TOKEN}`,
|
|
40
|
+
},
|
|
41
|
+
next: { tags: ['eyeballs-cms'] },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!res.ok) throw new Error(`CMS request failed: ${res.status}`);
|
|
45
|
+
return res.json();
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Authentication
|
|
50
|
+
|
|
51
|
+
Tokens are generated from Dashboard > Settings > API settings and stored hashed in `api_tokens`.
|
|
52
|
+
The raw token is only shown once. Tokens can be sent as:
|
|
53
|
+
|
|
54
|
+
```http
|
|
55
|
+
Authorization: Bearer eyb_...
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
or:
|
|
59
|
+
|
|
60
|
+
```http
|
|
61
|
+
x-api-key: eyb_...
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Missing, revoked, or unknown tokens return:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{ "error": "Missing API token" }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
or:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{ "error": "Invalid API token" }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
with HTTP `401`.
|
|
77
|
+
|
|
78
|
+
## Endpoints
|
|
79
|
+
|
|
80
|
+
| Endpoint | Response | Notes |
|
|
81
|
+
| ----------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
82
|
+
| `GET /api/cms/videos` | `{ "videos": CmsVideo[] }` | Published library work, newest first. |
|
|
83
|
+
| `GET /api/cms/videos/{slug}` | `{ "video": CmsVideo }` | Published library work by slug. |
|
|
84
|
+
| `GET /api/cms/roster` | `{ "roster": CmsRosterProfile[] }` | Published roster profiles. Empty when the account plan has no creatives access. |
|
|
85
|
+
| `GET /api/cms/roster/{slug}` | `{ "roster": CmsRosterProfile }` | Published roster profile by slug. |
|
|
86
|
+
| `GET /api/cms/content` | `{ "content": CmsContentEntry[] }` | Published collection entries and all pages. Supports `type` and `slug` query params. |
|
|
87
|
+
| `GET /api/cms/content/{slug}` | `{ "content": CmsContentEntry }` | Finds by entry slug across content types. Pages are preferred if multiple entries share a slug. |
|
|
88
|
+
|
|
89
|
+
### Content Query Parameters
|
|
90
|
+
|
|
91
|
+
`GET /api/cms/content?type=posts`
|
|
92
|
+
|
|
93
|
+
Returns entries whose content type slug is `posts`.
|
|
94
|
+
|
|
95
|
+
`GET /api/cms/content?type=posts&slug=my-post`
|
|
96
|
+
|
|
97
|
+
Returns a single entry when both `type` and `slug` are present.
|
|
98
|
+
|
|
99
|
+
`GET /api/cms/content?slug=about`
|
|
100
|
+
|
|
101
|
+
Returns a list of matching entries unless both `type` and `slug` are present. For most website
|
|
102
|
+
detail pages, prefer passing both `type` and `slug`.
|
|
103
|
+
|
|
104
|
+
## Visibility Rules
|
|
105
|
+
|
|
106
|
+
Videos only appear when `published_at` is set.
|
|
107
|
+
|
|
108
|
+
Roster profiles only appear when `status` is `published`. Roster endpoints also depend on the
|
|
109
|
+
account plan having creatives access. Without access, the list endpoint returns an empty list and
|
|
110
|
+
single-profile requests return `404`.
|
|
111
|
+
|
|
112
|
+
Content collection entries only appear when `status` is `published`.
|
|
113
|
+
|
|
114
|
+
Page entries are returned regardless of status. Pages are treated as website structure, and their
|
|
115
|
+
field schema is stored on each page entry rather than inherited from the Pages content type.
|
|
116
|
+
|
|
117
|
+
## Video Shape
|
|
118
|
+
|
|
119
|
+
`CmsVideo` is returned by video endpoints and by hydrated `library_work` custom fields.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
type CmsVideo = {
|
|
123
|
+
id: string;
|
|
124
|
+
title: string;
|
|
125
|
+
slug: string;
|
|
126
|
+
description: string | null;
|
|
127
|
+
client: string | null;
|
|
128
|
+
credits: CreditEntry[];
|
|
129
|
+
tags: string[];
|
|
130
|
+
roster: VideoCreativeGroup[];
|
|
131
|
+
fields: Record<string, unknown>;
|
|
132
|
+
field_schema: ContentField[];
|
|
133
|
+
published_at: string | null;
|
|
134
|
+
release_date: string | null;
|
|
135
|
+
media: {
|
|
136
|
+
mux_playback_id: string | null;
|
|
137
|
+
poster_url: string | null;
|
|
138
|
+
thumbnail_time: number | null;
|
|
139
|
+
mux_blur_placeholder: string | null;
|
|
140
|
+
mux_blur_aspect_ratio: number | null;
|
|
141
|
+
loop_start_time: number | null;
|
|
142
|
+
loop_end_time: number | null;
|
|
143
|
+
loop_mux_playback_id: string | null;
|
|
144
|
+
loop_mux_asset_status: string | null;
|
|
145
|
+
aspect_ratio: number | null;
|
|
146
|
+
custom_aspect_ratio: number | null;
|
|
147
|
+
duration_seconds: number | null;
|
|
148
|
+
};
|
|
149
|
+
created_at: string;
|
|
150
|
+
};
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`roster` groups the people or roles assigned to a video:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
type VideoCreativeGroup = {
|
|
157
|
+
id: string;
|
|
158
|
+
name: string;
|
|
159
|
+
slug: string;
|
|
160
|
+
assignments: Array<{
|
|
161
|
+
id: string;
|
|
162
|
+
name: string;
|
|
163
|
+
url: string | null;
|
|
164
|
+
roster: {
|
|
165
|
+
id: string;
|
|
166
|
+
name: string;
|
|
167
|
+
slug: string;
|
|
168
|
+
image_url: string | null;
|
|
169
|
+
description: string | null;
|
|
170
|
+
links: unknown;
|
|
171
|
+
} | null;
|
|
172
|
+
}>;
|
|
173
|
+
};
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Roster Shape
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
type CmsRosterProfile = {
|
|
180
|
+
id: string;
|
|
181
|
+
name: string;
|
|
182
|
+
slug: string;
|
|
183
|
+
image_url: string | null;
|
|
184
|
+
description: string | null;
|
|
185
|
+
links: unknown;
|
|
186
|
+
fields: Record<string, unknown>;
|
|
187
|
+
status: string;
|
|
188
|
+
order_index: number;
|
|
189
|
+
created_at: string;
|
|
190
|
+
updated_at: string;
|
|
191
|
+
type: {
|
|
192
|
+
id: string;
|
|
193
|
+
name: string;
|
|
194
|
+
singular_name: string;
|
|
195
|
+
slug: string;
|
|
196
|
+
field_schema: ContentField[];
|
|
197
|
+
} | null;
|
|
198
|
+
featured_work: CmsVideo[];
|
|
199
|
+
};
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`featured_work` contains published videos selected for that roster profile.
|
|
203
|
+
|
|
204
|
+
## Content Entry Shape
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
type CmsContentEntry = {
|
|
208
|
+
id: string;
|
|
209
|
+
name: string;
|
|
210
|
+
slug: string;
|
|
211
|
+
status: 'draft' | 'published' | 'archived';
|
|
212
|
+
entry_date: string | null;
|
|
213
|
+
website_path: string | null;
|
|
214
|
+
meta: {
|
|
215
|
+
title: string | null;
|
|
216
|
+
description: string | null;
|
|
217
|
+
image_url: string | null;
|
|
218
|
+
};
|
|
219
|
+
fields: Record<string, unknown>;
|
|
220
|
+
field_schema: ContentField[];
|
|
221
|
+
type: {
|
|
222
|
+
id: string;
|
|
223
|
+
name: string;
|
|
224
|
+
slug: string;
|
|
225
|
+
kind: 'pages' | 'collection';
|
|
226
|
+
} | null;
|
|
227
|
+
published_at: string | null;
|
|
228
|
+
created_at: string;
|
|
229
|
+
updated_at: string;
|
|
230
|
+
};
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
For collection entries, the default website path convention is:
|
|
234
|
+
|
|
235
|
+
```txt
|
|
236
|
+
/{contentType.slug}/{entry.slug}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
For page entries, use `website_path` when it is set. `website_path` should be normalized with a
|
|
240
|
+
leading slash in website projects. The root page normally uses `/`.
|
|
241
|
+
|
|
242
|
+
## Custom Field Schemas
|
|
243
|
+
|
|
244
|
+
Custom field schemas are arrays of `ContentField` objects:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
type ContentField = {
|
|
248
|
+
id: string;
|
|
249
|
+
type:
|
|
250
|
+
| 'text'
|
|
251
|
+
| 'textarea'
|
|
252
|
+
| 'richtext'
|
|
253
|
+
| 'boolean'
|
|
254
|
+
| 'image'
|
|
255
|
+
| 'date'
|
|
256
|
+
| 'library_work'
|
|
257
|
+
| 'content_blocks'
|
|
258
|
+
| 'array';
|
|
259
|
+
label: string;
|
|
260
|
+
multiple?: boolean;
|
|
261
|
+
required?: boolean;
|
|
262
|
+
itemFields?: ContentField[];
|
|
263
|
+
itemLabelFieldId?: string;
|
|
264
|
+
itemSecondaryLabelFieldId?: string;
|
|
265
|
+
};
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
The `id` is the API key used inside `fields`. The `label` is display text in the dashboard.
|
|
269
|
+
Agents and website projects should use field IDs, not labels, when reading API responses.
|
|
270
|
+
|
|
271
|
+
## Field Naming Rules
|
|
272
|
+
|
|
273
|
+
When a new custom field is created, its initial `id` is generated from the label with
|
|
274
|
+
`cleanSlug(label)`:
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
function cleanSlug(base: string) {
|
|
278
|
+
return base
|
|
279
|
+
.toLowerCase()
|
|
280
|
+
.trim()
|
|
281
|
+
.replace(/[^\w\s-]/g, '')
|
|
282
|
+
.replace(/\s+/g, '-')
|
|
283
|
+
.replace(/-+/g, '-');
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Examples:
|
|
288
|
+
|
|
289
|
+
| Label | Field ID |
|
|
290
|
+
| --------------- | --------------- |
|
|
291
|
+
| `Hero Title` | `hero-title` |
|
|
292
|
+
| `Client Quote` | `client-quote` |
|
|
293
|
+
| `SEO / Intro` | `seo-intro` |
|
|
294
|
+
| `Featured Work` | `featured-work` |
|
|
295
|
+
|
|
296
|
+
If the generated ID already exists in the same schema, the editor appends a numeric suffix:
|
|
297
|
+
`hero-title`, `hero-title-2`, `hero-title-3`.
|
|
298
|
+
|
|
299
|
+
After a field exists, renaming the label does not rename the saved field ID. This preserves the
|
|
300
|
+
API contract and existing saved content. When telling an agent about a field setup, include either
|
|
301
|
+
the exact field IDs or enough label context to infer the initial slugged IDs.
|
|
302
|
+
|
|
303
|
+
Good agent prompt:
|
|
304
|
+
|
|
305
|
+
```txt
|
|
306
|
+
I created a Posts content type with slug "journal". Its fields are:
|
|
307
|
+
- Hero Image -> hero-image, image
|
|
308
|
+
- Intro Text -> intro-text, richtext
|
|
309
|
+
- Related Work -> related-work, library_work multiple
|
|
310
|
+
|
|
311
|
+
Use the Content API docs to build /journal and /journal/[slug].
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Field Value Shapes
|
|
315
|
+
|
|
316
|
+
Values are returned in `entry.fields`, `video.fields`, or `roster.fields` by field ID.
|
|
317
|
+
|
|
318
|
+
| Field type | API value |
|
|
319
|
+
| ------------------------------------- | ------------------------------------------------ | ----- |
|
|
320
|
+
| `text` | `string` |
|
|
321
|
+
| `textarea` | `string` |
|
|
322
|
+
| `richtext` | `string` containing HTML |
|
|
323
|
+
| `boolean` | `boolean` |
|
|
324
|
+
| `image` | image URL string |
|
|
325
|
+
| `date` | date string |
|
|
326
|
+
| `library_work` with `multiple: false` | `CmsVideo | null` |
|
|
327
|
+
| `library_work` with multiple/default | `CmsVideo[]` |
|
|
328
|
+
| `content_blocks` | `ContentBlock[]` |
|
|
329
|
+
| `array` | array of objects with `_key` plus item field IDs |
|
|
330
|
+
|
|
331
|
+
`library_work` values are hydrated by the API. Website projects receive full published video
|
|
332
|
+
objects, not raw video IDs. Missing or unpublished referenced videos are omitted from arrays or
|
|
333
|
+
returned as `null` for single-video fields.
|
|
334
|
+
|
|
335
|
+
Array item fields can use simple fields and single `library_work`. Nested arrays are not allowed.
|
|
336
|
+
|
|
337
|
+
## Content Blocks
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
type ContentBlock =
|
|
341
|
+
| {
|
|
342
|
+
id: string;
|
|
343
|
+
type: 'text';
|
|
344
|
+
content: string;
|
|
345
|
+
}
|
|
346
|
+
| {
|
|
347
|
+
id: string;
|
|
348
|
+
type: 'image';
|
|
349
|
+
src: string;
|
|
350
|
+
path: string;
|
|
351
|
+
alt: string;
|
|
352
|
+
showCaption: boolean;
|
|
353
|
+
}
|
|
354
|
+
| {
|
|
355
|
+
id: string;
|
|
356
|
+
type: 'button';
|
|
357
|
+
text: string;
|
|
358
|
+
url: string;
|
|
359
|
+
};
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Text block `content` is HTML and should be sanitized or rendered through a trusted rich-text
|
|
363
|
+
renderer in website projects.
|
|
364
|
+
|
|
365
|
+
## Website Settings And Preview Links
|
|
366
|
+
|
|
367
|
+
Website preview/view links in the dashboard are driven by the frontend revalidation webhook URL.
|
|
368
|
+
The app derives the website base URL from the saved revalidation URL:
|
|
369
|
+
|
|
370
|
+
```txt
|
|
371
|
+
https://client-site.com/api/revalidate -> https://client-site.com
|
|
372
|
+
https://client-site.com/custom/api/revalidate -> https://client-site.com/custom
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
The helper is `getWebsiteProjectBaseUrl(revalidateUrl)`. If no webhook exists or the URL is
|
|
376
|
+
invalid, preview links are not shown.
|
|
377
|
+
|
|
378
|
+
`FRONTEND_REVALIDATION_URL_OVERRIDE` can override the saved webhook URL for revalidation behavior.
|
|
379
|
+
When the override is present, content edit pages do not derive a preview base URL from the saved
|
|
380
|
+
webhook.
|
|
381
|
+
|
|
382
|
+
### Page Preview Links
|
|
383
|
+
|
|
384
|
+
Page entries use `website_path` to build their view URL:
|
|
385
|
+
|
|
386
|
+
```txt
|
|
387
|
+
baseUrl + website_path
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Examples:
|
|
391
|
+
|
|
392
|
+
| Page name | Entry slug | Website path | View URL path |
|
|
393
|
+
| --------- | ---------- | ------------ | ------------- |
|
|
394
|
+
| Home | `home` | `/` | `/` |
|
|
395
|
+
| About | `about` | `/about` | `/about` |
|
|
396
|
+
| Contact | `contact` | `/contact` | `/contact` |
|
|
397
|
+
|
|
398
|
+
Only super users can edit page `website_path` and page field schemas.
|
|
399
|
+
|
|
400
|
+
### Roster Preview Links
|
|
401
|
+
|
|
402
|
+
Roster profile links use the roster type website route plus the creative slug:
|
|
403
|
+
|
|
404
|
+
```txt
|
|
405
|
+
/{website_path_prefix || creative_type.slug}/{creative.slug}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Examples:
|
|
409
|
+
|
|
410
|
+
| Roster type slug | Website profile route | Creative slug | View URL path |
|
|
411
|
+
| ---------------- | --------------------- | ------------- | ---------------------- |
|
|
412
|
+
| `directors` | empty | `sam-smith` | `/directors/sam-smith` |
|
|
413
|
+
| `photographers` | `artists` | `jane-lee` | `/artists/jane-lee` |
|
|
414
|
+
|
|
415
|
+
Only super users can edit the roster type website profile route.
|
|
416
|
+
|
|
417
|
+
### Collection Routes
|
|
418
|
+
|
|
419
|
+
Collection content entries do not currently store a custom `website_path`. Starter sites should
|
|
420
|
+
use:
|
|
421
|
+
|
|
422
|
+
```txt
|
|
423
|
+
/{contentType.slug}
|
|
424
|
+
/{contentType.slug}/{entry.slug}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
If a website project wants friendlier route names, set the content type slug accordingly in the
|
|
428
|
+
dashboard.
|
|
429
|
+
|
|
430
|
+
## Revalidation
|
|
431
|
+
|
|
432
|
+
Dashboard > Settings > API settings stores one frontend revalidation webhook per account:
|
|
433
|
+
|
|
434
|
+
```txt
|
|
435
|
+
revalidate_url: https://client-site.com/api/revalidate
|
|
436
|
+
secret: REVALIDATE_SECRET
|
|
437
|
+
enabled: true
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
When library, roster, or content changes affect a website, Eyeballs calls the starter site's
|
|
441
|
+
revalidation endpoint with paths and tags. Starter sites should support:
|
|
442
|
+
|
|
443
|
+
```http
|
|
444
|
+
POST /api/revalidate
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
with the saved `REVALIDATE_SECRET`.
|
|
448
|
+
|
|
449
|
+
Content revalidation tags include:
|
|
450
|
+
|
|
451
|
+
```txt
|
|
452
|
+
content
|
|
453
|
+
content-type:{typeSlug}
|
|
454
|
+
content:{entrySlug}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Roster revalidation tags include:
|
|
458
|
+
|
|
459
|
+
```txt
|
|
460
|
+
roster-profile:{creativeSlug}
|
|
461
|
+
roster:{typeRouteSlug}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Library work changes can also revalidate work pages, roster profiles, and content entries that
|
|
465
|
+
reference the changed video through `library_work` fields.
|
|
466
|
+
|
|
467
|
+
## Starter Site Recipes
|
|
468
|
+
|
|
469
|
+
### List Published Work
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
const { videos } = await cmsFetch('/api/cms/videos');
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Work Detail Page
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
const { video } = await cmsFetch(`/api/cms/videos/${params.slug}`);
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Collection List
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
const { content } = await cmsFetch('/api/cms/content?type=journal');
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Collection Detail
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
const { content: entry } = await cmsFetch(`/api/cms/content?type=journal&slug=${params.slug}`);
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Page By Slug
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
const { content: page } = await cmsFetch('/api/cms/content/home');
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
For page routing, prefer matching by `website_path` from the content list when the route is not
|
|
500
|
+
the same as the entry slug.
|
|
501
|
+
|
|
502
|
+
### Roster List And Profile
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
const { roster } = await cmsFetch('/api/cms/roster');
|
|
506
|
+
const { roster: profile } = await cmsFetch(`/api/cms/roster/${params.slug}`);
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
## Maintenance Checklist
|
|
510
|
+
|
|
511
|
+
When the API changes, update:
|
|
512
|
+
|
|
513
|
+
1. `packages/cms/docs/content-api.md`
|
|
514
|
+
2. `packages/cms/src/*` shared types, client helpers, or Next helpers if the contract changed
|
|
515
|
+
3. `/dashboard/settings/api` if token or revalidation settings changed
|
|
516
|
+
4. Any starter template API helper or examples
|
|
517
|
+
5. `AGENTS.md` if agent workflow expectations changed
|
|
518
|
+
|
|
519
|
+
Check these source files before documenting behavior:
|
|
520
|
+
|
|
521
|
+
```txt
|
|
522
|
+
src/app/api/cms/*
|
|
523
|
+
src/lib/cmsData.ts
|
|
524
|
+
src/lib/cmsAuth.ts
|
|
525
|
+
src/lib/content.ts
|
|
526
|
+
src/lib/contentFieldValues.ts
|
|
527
|
+
src/lib/contentLibraryWork.ts
|
|
528
|
+
src/lib/websiteProjectUrl.ts
|
|
529
|
+
src/lib/publicRevalidation.ts
|
|
530
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eyeballs/cms",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared Eyeballs CMS API types, client helpers, and documentation.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"docs",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./next": {
|
|
23
|
+
"types": "./dist/next.d.ts",
|
|
24
|
+
"import": "./dist/next.js"
|
|
25
|
+
},
|
|
26
|
+
"./docs/content-api.md": "./docs/content-api.md"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -p tsconfig.json",
|
|
30
|
+
"prepack": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"next": ">=15"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"next": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"typescript": "^6"
|
|
42
|
+
}
|
|
43
|
+
}
|