Skip to content

Commit 140b932

Browse files
authored
feat: add videoAnalytics Sanity document type (#669)
Separate YouTube stats from content docs to avoid webhook noise. Includes contentRef, denormalized contentType, readOnly stats fields, and 1:1 uniqueness guidance for CF Worker cron.
1 parent 18dee75 commit 140b932

3 files changed

Lines changed: 140 additions & 0 deletions

File tree

lib/types/video-analytics.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export interface VideoAnalytics {
2+
_id: string;
3+
_type: 'videoAnalytics';
4+
_createdAt: string;
5+
_updatedAt: string;
6+
contentRef: {
7+
_type: 'reference';
8+
_ref: string;
9+
};
10+
contentType: 'post' | 'podcast' | 'automatedVideo';
11+
youtubeId: string;
12+
youtubeShortId?: string;
13+
viewCount: number;
14+
likeCount: number;
15+
commentCount: number;
16+
favoriteCount: number;
17+
lastFetchedAt?: string;
18+
}

sanity.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import sponsorshipRequest from "@/sanity/schemas/documents/sponsorshipRequest";
4848
import contentIdea from "@/sanity/schemas/documents/contentIdea";
4949
import automatedVideo from "@/sanity/schemas/documents/automatedVideo";
5050
import mediaAsset from "@/sanity/schemas/documents/mediaAsset";
51+
import videoAnalytics from "@/sanity/schemas/documents/videoAnalytics";
5152
import sponsorLead from "@/sanity/schemas/documents/sponsorLead";
5253
import sponsorPool from "@/sanity/schemas/documents/sponsorPool";
5354
import tableSchema, { rowType } from "@/sanity/schemas/custom/table";
@@ -152,6 +153,7 @@ export default defineConfig({
152153
contentIdea,
153154
automatedVideo,
154155
mediaAsset,
156+
videoAnalytics,
155157
sponsorLead,
156158
sponsorPool,
157159
],
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { defineField, defineType } from 'sanity';
2+
import { HiOutlineChartBar } from 'react-icons/hi';
3+
4+
// One videoAnalytics document per content document (1:1 relationship).
5+
// Sanity cannot enforce uniqueness at schema level — the CF Worker cron
6+
// that creates these docs must check for existing docs before creating:
7+
// *[_type == "videoAnalytics" && contentRef._ref == $docId][0]
8+
export default defineType({
9+
name: 'videoAnalytics',
10+
title: 'Video Analytics',
11+
type: 'document',
12+
icon: HiOutlineChartBar,
13+
fields: [
14+
defineField({
15+
name: 'contentRef',
16+
title: 'Content Reference',
17+
type: 'reference',
18+
to: [
19+
{ type: 'post' },
20+
{ type: 'podcast' },
21+
{ type: 'automatedVideo' },
22+
],
23+
description: 'The content document these analytics belong to',
24+
validation: (Rule) => Rule.required(),
25+
}),
26+
defineField({
27+
name: 'contentType',
28+
title: 'Content Type',
29+
type: 'string',
30+
description: 'Denormalized content type for efficient GROQ filtering without dereferencing',
31+
options: {
32+
list: [
33+
{ title: 'Post', value: 'post' },
34+
{ title: 'Podcast', value: 'podcast' },
35+
{ title: 'Automated Video', value: 'automatedVideo' },
36+
],
37+
},
38+
validation: (Rule) => Rule.required(),
39+
readOnly: true,
40+
}),
41+
defineField({
42+
name: 'youtubeId',
43+
title: 'YouTube Video ID',
44+
type: 'string',
45+
description: 'YouTube video ID (e.g., dQw4w9WgXcQ)',
46+
validation: (Rule) => Rule.required(),
47+
readOnly: true,
48+
}),
49+
defineField({
50+
name: 'youtubeShortId',
51+
title: 'YouTube Short ID',
52+
type: 'string',
53+
description: 'YouTube Short video ID (for automatedVideo with both formats)',
54+
}),
55+
defineField({
56+
name: 'viewCount',
57+
title: 'View Count',
58+
type: 'number',
59+
description: 'YouTube view count',
60+
initialValue: 0,
61+
readOnly: true,
62+
}),
63+
defineField({
64+
name: 'likeCount',
65+
title: 'Like Count',
66+
type: 'number',
67+
description: 'YouTube like count',
68+
initialValue: 0,
69+
readOnly: true,
70+
}),
71+
defineField({
72+
name: 'commentCount',
73+
title: 'Comment Count',
74+
type: 'number',
75+
description: 'YouTube comment count',
76+
initialValue: 0,
77+
readOnly: true,
78+
}),
79+
defineField({
80+
name: 'favoriteCount',
81+
title: 'Favorite Count',
82+
type: 'number',
83+
description: 'YouTube favorite count',
84+
initialValue: 0,
85+
readOnly: true,
86+
}),
87+
defineField({
88+
name: 'lastFetchedAt',
89+
title: 'Last Fetched At',
90+
type: 'datetime',
91+
description: 'When stats were last pulled from the YouTube Data API',
92+
readOnly: true,
93+
}),
94+
],
95+
orderings: [
96+
{
97+
title: 'Views (High to Low)',
98+
name: 'viewCountDesc',
99+
by: [{ field: 'viewCount', direction: 'desc' }],
100+
},
101+
{
102+
title: 'Last Fetched',
103+
name: 'lastFetchedAtDesc',
104+
by: [{ field: 'lastFetchedAt', direction: 'desc' }],
105+
},
106+
],
107+
preview: {
108+
select: {
109+
contentType: 'contentType',
110+
youtubeId: 'youtubeId',
111+
viewCount: 'viewCount',
112+
},
113+
prepare({ contentType, youtubeId, viewCount }) {
114+
return {
115+
title: youtubeId || 'No YouTube ID',
116+
subtitle: `${contentType || 'unknown'} · ${(viewCount ?? 0).toLocaleString()} views`,
117+
};
118+
},
119+
},
120+
});

0 commit comments

Comments
 (0)