@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 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.
@@ -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":"AASA,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAC1C,MAAM,CAAS;IACf,OAAO,CAAU;IAEjB,YAAY,OAAe,EAAE,MAAc,EAAE,OAAgB;QAC5D,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;QAC/B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACxB,CAAC;CACD;AAED,SAAS,gBAAgB,CAAC,OAAe;IACxC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACnD,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;IAClE,OAAO,OAAO,CAAC;AAChB,CAAC;AAED,SAAS,QAAQ,CAAC,OAAe,EAAE,IAAY;IAC9C,OAAO,GAAG,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;AACjD,CAAC;AAED,SAAS,SAAS,CAAC,IAAY,EAAE,MAAiD;IACjF,MAAM,KAAK,GAAG,IAAI,eAAe,EAAE,CAAC;IACpC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnD,MAAM,OAAO,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC;QAC9B,IAAI,OAAO;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtC,CAAC;IACD,MAAM,WAAW,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC;IACrC,OAAO,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAiC;IAClE,MAAM,OAAO,GAAG,gBAAgB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IACnC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IAE9D,MAAM,YAAY,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC;IAE5C,KAAK,UAAU,OAAO,CAAI,IAAY,EAAE,OAAoB,EAAE;QAC7D,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;QACjE,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;QAC3E,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,UAAU,KAAK,EAAE,CAAC,CAAC;QAEhD,MAAM,GAAG,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE;YACvD,GAAG,OAAO,CAAC,kBAAkB;YAC7B,GAAG,IAAI;YACP,OAAO;SACP,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAE/C,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACb,MAAM,OAAO,GACZ,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,IAAI,OAAO;gBAC3D,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;gBACvB,CAAC,CAAC,gCAAgC,GAAG,CAAC,MAAM,EAAE,CAAC;YACjD,MAAM,IAAI,gBAAgB,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC1D,CAAC;QAED,OAAO,OAAY,CAAC;IACrB,CAAC;IAED,OAAO;QACN,OAAO;QACP,MAAM,EAAE;YACP,KAAK,CAAC,IAAI;gBACT,MAAM,OAAO,GAAG,MAAM,OAAO,CAAiC,iBAAiB,CAAC,CAAC;gBACjF,OAAO,OAAO,CAAC,MAAM,CAAC;YACvB,CAAC;YACD,KAAK,CAAC,GAAG,CACR,IAAY;gBAEZ,MAAM,OAAO,GAAG,MAAM,OAAO,CAC5B,mBAAmB,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAC7C,CAAC;gBACF,OAAO,OAAO,CAAC,KAAK,CAAC;YACtB,CAAC;SACD;QACD,MAAM,EAAE;YACP,KAAK,CAAC,IAAI;gBACT,MAAM,OAAO,GAAG,MAAM,OAAO,CAC5B,iBAAiB,CACjB,CAAC;gBACF,OAAO,OAAO,CAAC,MAAM,CAAC;YACvB,CAAC;YACD,KAAK,CAAC,GAAG,CACR,IAAY;gBAEZ,MAAM,OAAO,GAAG,MAAM,OAAO,CAC5B,mBAAmB,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAC7C,CAAC;gBACF,OAAO,OAAO,CAAC,MAAM,CAAC;YACvB,CAAC;SACD;QACD,OAAO,EAAE;YACR,KAAK,CAAC,IAAI,CAAmE,OAG5E;gBACA,MAAM,OAAO,GAAG,MAAM,OAAO,CAC5B,SAAS,CAAC,kBAAkB,EAAE;oBAC7B,IAAI,EAAE,OAAO,EAAE,IAAI;oBACnB,IAAI,EAAE,OAAO,EAAE,IAAI;iBACnB,CAAC,CACF,CAAC;gBACF,OAAO,OAAO,CAAC,OAAO,CAAC;YACxB,CAAC;YACD,KAAK,CAAC,SAAS,CACd,IAAY;gBAEZ,MAAM,OAAO,GAAG,MAAM,OAAO,CAC5B,oBAAoB,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAC9C,CAAC;gBACF,OAAO,OAAO,CAAC,OAAO,CAAC;YACxB,CAAC;YACD,KAAK,CAAC,gBAAgB,CAEpB,IAAY,EAAE,IAAY;gBAC3B,MAAM,OAAO,GAAG,MAAM,OAAO,CAC5B,SAAS,CAAC,kBAAkB,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAC7C,CAAC;gBACF,OAAO,OAAO,CAAC,OAAO,CAAC;YACxB,CAAC;SACD;KACD,CAAC;AACH,CAAC"}
@@ -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"}
@@ -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"}
@@ -0,0 +1,4 @@
1
+ export * from './client.js';
2
+ export * from './helpers.js';
3
+ export * from './types.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -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,4 @@
1
+ export * from './client.js';
2
+ export * from './helpers.js';
3
+ export * from './types.js';
4
+ //# sourceMappingURL=index.js.map
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next.js","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
+ }