Skip to content

Commit 89de84c

Browse files
authored
chore(release): promote Shorts story, Gemini 2.5 + TTS fallback, and AEO pack to main (#721)
* feat(syndication): add syndicate-post workflow and dispatch runner (#708) * fix(syndication): update Hashnode tag shape to PublishPostTagInput (#710) * fix(syndication): update Hashnode coverImage input to string URL (#713) * feat(seo): add VideoObject structured data and Gemini video storyboard pipeline (#715) * fix(video): upgrade to Gemini 2.5 Pro with native Google AI and OpenRouter support (#717) * feat: complete Shorts story (/shorts shelf + kinetic captions + sync), Gemini 2.5/TTS upgrades, and TechArticle/Auto-FAQ AEO pack (#720)
1 parent b99f52d commit 89de84c

31 files changed

Lines changed: 1378 additions & 66 deletions

‎.github/workflows/sync-shorts.yml‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Sync YouTube Shorts to Sanity
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
dataset:
7+
description: "Target Sanity Dataset"
8+
required: true
9+
default: "production"
10+
type: choice
11+
options:
12+
- production
13+
- dev
14+
schedule:
15+
- cron: "30 6 * * *"
16+
17+
jobs:
18+
sync-shorts:
19+
runs-on: ubuntu-latest
20+
environment: production
21+
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- uses: pnpm/action-setup@v4
26+
27+
- uses: actions/setup-node@v4
28+
with:
29+
node-version-file: ".nvmrc"
30+
cache: "pnpm"
31+
32+
- name: Install dependencies
33+
run: pnpm install --frozen-lockfile
34+
35+
- name: Sync Shorts from Automated Videos
36+
run: node scripts/sync-shorts-from-automated-videos.mjs
37+
env:
38+
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
39+
SANITY_API_WRITE_TOKEN: ${{ secrets.SANITY_API_WRITE_TOKEN }}
40+
SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID || 'hfh83o0w' }}
41+
SANITY_STUDIO_DATASET: ${{ inputs.dataset || 'production' }}
42+
YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }}

‎apps/sanity/extract.json‎

Lines changed: 98 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,36 @@
702702
}
703703
}
704704
},
705+
{
706+
"type": "type",
707+
"name": "automatedVideo.reference",
708+
"value": {
709+
"type": "object",
710+
"attributes": {
711+
"_ref": {
712+
"type": "objectAttribute",
713+
"value": {
714+
"type": "string"
715+
}
716+
},
717+
"_type": {
718+
"type": "objectAttribute",
719+
"value": {
720+
"type": "string",
721+
"value": "reference"
722+
}
723+
},
724+
"_weak": {
725+
"type": "objectAttribute",
726+
"value": {
727+
"type": "boolean"
728+
},
729+
"optional": true
730+
}
731+
},
732+
"dereferencesTo": "automatedVideo"
733+
}
734+
},
705735
{
706736
"type": "type",
707737
"name": "category.reference",
@@ -782,13 +812,27 @@
782812
},
783813
"optional": true
784814
},
815+
"description": {
816+
"type": "objectAttribute",
817+
"value": {
818+
"type": "string"
819+
},
820+
"optional": true
821+
},
785822
"youtube": {
786823
"type": "objectAttribute",
787824
"value": {
788825
"type": "string"
789826
},
790827
"optional": true
791828
},
829+
"shortVideoUrl": {
830+
"type": "objectAttribute",
831+
"value": {
832+
"type": "string"
833+
},
834+
"optional": true
835+
},
792836
"thumbnail": {
793837
"type": "objectAttribute",
794838
"value": {
@@ -844,6 +888,14 @@
844888
},
845889
"optional": true
846890
},
891+
"sourceAutomatedVideo": {
892+
"type": "objectAttribute",
893+
"value": {
894+
"type": "inline",
895+
"name": "automatedVideo.reference"
896+
},
897+
"optional": true
898+
},
847899
"publishedAt": {
848900
"type": "objectAttribute",
849901
"value": {
@@ -858,6 +910,52 @@
858910
},
859911
"optional": true
860912
},
913+
"statistics": {
914+
"type": "objectAttribute",
915+
"value": {
916+
"type": "object",
917+
"attributes": {
918+
"youtube": {
919+
"type": "objectAttribute",
920+
"value": {
921+
"type": "object",
922+
"attributes": {
923+
"viewCount": {
924+
"type": "objectAttribute",
925+
"value": {
926+
"type": "number"
927+
},
928+
"optional": true
929+
},
930+
"likeCount": {
931+
"type": "objectAttribute",
932+
"value": {
933+
"type": "number"
934+
},
935+
"optional": true
936+
},
937+
"commentCount": {
938+
"type": "objectAttribute",
939+
"value": {
940+
"type": "number"
941+
},
942+
"optional": true
943+
},
944+
"favoriteCount": {
945+
"type": "objectAttribute",
946+
"value": {
947+
"type": "number"
948+
},
949+
"optional": true
950+
}
951+
}
952+
},
953+
"optional": true
954+
}
955+
}
956+
},
957+
"optional": true
958+
},
861959
"categories": {
862960
"type": "objectAttribute",
863961
"value": {
@@ -1183,36 +1281,6 @@
11831281
}
11841282
}
11851283
},
1186-
{
1187-
"type": "type",
1188-
"name": "automatedVideo.reference",
1189-
"value": {
1190-
"type": "object",
1191-
"attributes": {
1192-
"_ref": {
1193-
"type": "objectAttribute",
1194-
"value": {
1195-
"type": "string"
1196-
}
1197-
},
1198-
"_type": {
1199-
"type": "objectAttribute",
1200-
"value": {
1201-
"type": "string",
1202-
"value": "reference"
1203-
}
1204-
},
1205-
"_weak": {
1206-
"type": "objectAttribute",
1207-
"value": {
1208-
"type": "boolean"
1209-
},
1210-
"optional": true
1211-
}
1212-
},
1213-
"dereferencesTo": "automatedVideo"
1214-
}
1215-
},
12161284
{
12171285
"name": "videoAnalytics",
12181286
"type": "document",

‎apps/sanity/functions/sync-youtube-analytics/index.ts‎

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,69 @@ export const handler = scheduledEventHandler(async ({ context }) => {
7070
useCdn: false,
7171
});
7272

73-
// Query all posts and podcasts with YouTube URLs
73+
// Step 0: Sync any published automatedVideo Shorts into standalone `_type == "short"` documents
74+
const autoShortDocs = await client.fetch<
75+
Array<{
76+
_id: string;
77+
_createdAt: string;
78+
title: string;
79+
slug?: { current?: string };
80+
youtubeShortId: string;
81+
shortUrl?: string;
82+
hook?: string;
83+
}>
84+
>(
85+
`*[_type == "automatedVideo" && defined(youtubeShortId) && !(_id in path("drafts.**"))]{
86+
_id,
87+
_createdAt,
88+
title,
89+
slug,
90+
youtubeShortId,
91+
shortUrl,
92+
"hook": script.hook
93+
}`,
94+
);
95+
96+
for (const autoShort of autoShortDocs) {
97+
const shortId = `short-${autoShort.youtubeShortId}`;
98+
const slugCurrent =
99+
autoShort.slug?.current ||
100+
autoShort.title
101+
.toLowerCase()
102+
.replace(/[^a-z0-9]+/g, "-")
103+
.replace(/(^-|-$)/g, "")
104+
.slice(0, 90);
105+
await client
106+
.createIfNotExists({
107+
_id: shortId,
108+
_type: "short",
109+
title: autoShort.title,
110+
slug: { _type: "slug", current: slugCurrent },
111+
description: autoShort.hook || undefined,
112+
youtube: `https://www.youtube.com/shorts/${autoShort.youtubeShortId}`,
113+
shortVideoUrl: autoShort.shortUrl || undefined,
114+
sourceAutomatedVideo: {
115+
_type: "reference",
116+
_ref: autoShort._id,
117+
},
118+
publishedAt: autoShort._createdAt,
119+
duration: 60,
120+
})
121+
.catch((err) => {
122+
console.warn(
123+
`[Sync YouTube Analytics] Could not createIfNotExists short ${shortId}:`,
124+
err,
125+
);
126+
});
127+
}
128+
129+
// Query all posts, podcasts, and shorts with YouTube URLs
74130
console.log(
75131
`[Sync YouTube Analytics] Fetching documents with YouTube URLs in project ${projectId}, dataset ${dataset}...`,
76132
);
77133

78134
const docs = await client.fetch<TargetVideoDoc[]>(
79-
`*[_type in ["post", "podcast"] && (defined(youtube) || defined(listenLinks.youtube)) && !(_id in path("drafts.**"))]{
135+
`*[_type in ["post", "podcast", "short"] && (defined(youtube) || defined(listenLinks.youtube)) && !(_id in path("drafts.**"))]{
80136
_id,
81137
_type,
82138
title,

‎apps/sanity/schemas/documents/short.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,25 @@ export default defineType({
2727
},
2828
validation: (rule) => rule.required(),
2929
}),
30+
defineField({
31+
name: "description",
32+
title: "Description / Hook",
33+
type: "text",
34+
rows: 3,
35+
description: "Short hook or summary for SEO/AEO and card previews",
36+
}),
3037
defineField({
3138
name: "youtube",
3239
title: "YouTube",
3340
type: "string",
3441
description: "YouTube Shorts video ID or URL",
3542
}),
43+
defineField({
44+
name: "shortVideoUrl",
45+
title: "Direct MP4 Video URL",
46+
type: "url",
47+
description: "Direct Sanity CDN or R2 MP4 URL for native vertical video playback",
48+
}),
3649
defineField({
3750
name: "thumbnail",
3851
title: "Thumbnail",
@@ -49,6 +62,13 @@ export default defineType({
4962
to: [{ type: "podcast" }],
5063
description: "Long-form episode this Short was clipped from",
5164
}),
65+
defineField({
66+
name: "sourceAutomatedVideo",
67+
title: "Source Automated Video",
68+
type: "reference",
69+
to: [{ type: "automatedVideo" }],
70+
description: "Automated video pipeline run that produced this Short",
71+
}),
5272
defineField({
5373
name: "publishedAt",
5474
title: "Published At",
@@ -60,6 +80,24 @@ export default defineType({
6080
type: "number",
6181
description: "Duration in seconds",
6282
}),
83+
defineField({
84+
name: "statistics",
85+
title: "Statistics",
86+
type: "object",
87+
fields: [
88+
defineField({
89+
name: "youtube",
90+
title: "YouTube Statistics",
91+
type: "object",
92+
fields: [
93+
defineField({ name: "viewCount", title: "Views", type: "number" }),
94+
defineField({ name: "likeCount", title: "Likes", type: "number" }),
95+
defineField({ name: "commentCount", title: "Comments", type: "number" }),
96+
defineField({ name: "favoriteCount", title: "Favorites", type: "number" }),
97+
],
98+
}),
99+
],
100+
}),
63101
defineField({
64102
name: "categories",
65103
title: "Categories",

‎apps/sanity/schemas/singletons/engineConfig.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,8 @@ export default defineType({
327327
type: 'string',
328328
fieldset: 'aiGeneration',
329329
description:
330-
'The Google Gemini model used for script generation and content analysis',
331-
initialValue: 'gemini-2.0-flash',
330+
'The Google Gemini model used for script generation and content analysis (e.g., gemini-2.5-flash, gemini-2.5-pro)',
331+
initialValue: 'gemini-2.5-flash',
332332
}),
333333
defineField({
334334
name: 'infographicModel',

‎apps/sanity/schemas/singletons/pipelineConfig.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export default defineType({
1010
name: "geminiModel",
1111
title: "Gemini Model",
1212
type: "string",
13-
description: "The Google Gemini model used for script generation and content analysis (e.g., gemini-2.0-flash, gemini-2.5-pro)",
14-
initialValue: "gemini-2.0-flash",
13+
description: "The Google Gemini model used for script generation and content analysis (e.g., gemini-2.5-flash, gemini-2.5-pro)",
14+
initialValue: "gemini-2.5-flash",
1515
}),
1616
defineField({
1717
name: "elevenLabsVoiceId",

‎apps/site/src/components/Header.astro‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import ThemeToggle from "./ThemeToggle.astro";
55
import SearchModal from "./SearchModal.astro";
66
77
const settings = await Astro.locals.sanity.loadQuery(settingsQuery);
8-
const navLinks = settings?.navLinks ?? [];
8+
const rawNavLinks = settings?.navLinks ?? [];
9+
const navLinks = rawNavLinks.some((link) => link?.path === "/shorts")
10+
? rawNavLinks
11+
: [
12+
...rawNavLinks,
13+
{ _key: "shorts-nav", path: "/shorts", title: "Shorts", sideOnly: false },
14+
];
915
1016
// The Next header split these across a desktop <nav> and a mobile Sheet;
1117
// `sideOnly` links appear only in the drawer.

0 commit comments

Comments
 (0)