Skip to content

Commit 33fb349

Browse files
authored
fix: remove 9 redundant env var fallbacks + add Sanity schema descriptions
Sanity config singletons are now the single source of truth for all runtime config. Env vars only used for secrets/credentials.\n\nRemoved process.env fallbacks: ELEVENLABS_VOICE_ID, REMOTION_AWS_REGION, REMOTION_SERVE_URL, REMOTION_FUNCTION_NAME, GCS_BUCKET, GCS_PROJECT_ID, GEMINI_MODEL (x2), YOUTUBE_UPLOAD_VISIBILITY\n\nAdded descriptions to all 30+ fields across 6 Sanity config schemas.\n\nAlso includes revalidateTag fix for Next.js 16 (requires 2 args).
1 parent e28b8a6 commit 33fb349

File tree

13 files changed

+76
-35
lines changed

13 files changed

+76
-35
lines changed

app/api/webhooks/sanity-revalidate/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export async function POST(request: NextRequest) {
2525

2626
// Revalidate all sanity-tagged caches (the "heavy hammer" approach)
2727
// This is a backup for when no visitors are active to trigger SanityLive revalidation
28-
revalidateTag("sanity");
28+
// Next.js 16 requires a second argument — { expire: 0 } for immediate invalidation
29+
revalidateTag("sanity", { expire: 0 });
2930

3031
return NextResponse.json({
3132
revalidated: true,

lib/gemini.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { GoogleGenerativeAI } from "@google/generative-ai";
2+
import { getConfigValue } from "@/lib/config";
23

34
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || "");
45

@@ -12,8 +13,9 @@ export async function generateWithGemini(
1213
prompt: string,
1314
systemInstruction?: string,
1415
): Promise<string> {
16+
const geminiModel = await getConfigValue("pipeline_config", "geminiModel", "gemini-2.0-flash");
1517
const model = genAI.getGenerativeModel({
16-
model: process.env.GEMINI_MODEL || "gemini-2.5-flash",
18+
model: geminiModel,
1719
...(systemInstruction && { systemInstruction }),
1820
});
1921
const result = await model.generateContent(prompt);

lib/services/elevenlabs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ async function getElevenLabsConfig(): Promise<ElevenLabsConfig> {
6767
const apiKey = process.env.ELEVENLABS_API_KEY;
6868
const voiceId = await getConfigValue(
6969
"pipeline_config", "elevenLabsVoiceId",
70-
process.env.ELEVENLABS_VOICE_ID || "pNInz6obpgDQGcFmaJgB"
70+
"pNInz6obpgDQGcFmaJgB"
7171
);
7272

7373
if (!apiKey) {

lib/services/gcs.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
* Authenticates via service account JWT → OAuth2 access token exchange.
66
*
77
* Requires env vars:
8-
* - GCS_BUCKET
9-
* - GCS_PROJECT_ID
108
* - GCS_CLIENT_EMAIL
119
* - GCS_PRIVATE_KEY
1210
*
11+
* GCS bucket and project config is read from Sanity singletons via getConfigValue().
12+
*
1313
* @module lib/services/gcs
1414
*/
1515

@@ -58,27 +58,30 @@ const TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000; // 5 minutes
5858
// ---------------------------------------------------------------------------
5959

6060
/**
61-
* Get GCS configuration from environment variables.
62-
* Throws if any required env var is missing.
61+
* Get GCS configuration from Sanity config + environment variables.
62+
* Bucket and project ID come from Sanity; credentials from env vars.
63+
* Throws if any required value is missing.
6364
*
6465
* The private key may contain literal `\\n` sequences from the env var;
6566
* these are converted to real newline characters.
6667
*/
6768
export async function getGCSConfig(): Promise<GCSConfig> {
68-
const bucket = await getConfigValue("gcs_config", "bucketName", process.env.GCS_BUCKET);
69-
const projectId = await getConfigValue("gcs_config", "projectId", process.env.GCS_PROJECT_ID);
69+
const bucket = await getConfigValue("gcs_config", "bucketName", "codingcatdev-content-engine");
70+
const projectId = await getConfigValue("gcs_config", "projectId", "codingcatdev");
7071
const clientEmail = process.env.GCS_CLIENT_EMAIL;
7172
let privateKey = process.env.GCS_PRIVATE_KEY;
7273

7374
if (!bucket || !projectId || !clientEmail || !privateKey) {
7475
const missing = [
75-
!bucket && "GCS_BUCKET",
76-
!projectId && "GCS_PROJECT_ID",
76+
!bucket && "gcs_config.bucketName",
77+
!projectId && "gcs_config.projectId",
7778
!clientEmail && "GCS_CLIENT_EMAIL",
7879
!privateKey && "GCS_PRIVATE_KEY",
7980
].filter(Boolean);
8081
throw new Error(
81-
`[GCS] Missing required environment variables: ${missing.join(", ")}`
82+
`[GCS] Missing required configuration: ${missing.join(", ")}. ` +
83+
`Bucket and project ID are managed in the Sanity GCS Config singleton. ` +
84+
`Credentials (GCS_CLIENT_EMAIL, GCS_PRIVATE_KEY) come from env vars.`
8285
);
8386
}
8487

lib/services/remotion.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
* Requires env vars:
1111
* - AWS_ACCESS_KEY_ID
1212
* - AWS_SECRET_ACCESS_KEY
13-
* - REMOTION_AWS_REGION
14-
* - REMOTION_SERVE_URL (generated during Lambda deployment)
15-
* - REMOTION_FUNCTION_NAME (optional, defaults to DEFAULT_FUNCTION_NAME)
13+
*
14+
* Remotion-specific config (region, serveUrl, functionName) is read from
15+
* Sanity singletons via getConfigValue().
1616
*
1717
* @module lib/services/remotion
1818
*/
@@ -76,7 +76,7 @@ export interface RenderResult {
7676
// Constants
7777
// ---------------------------------------------------------------------------
7878

79-
/** Default Lambda function name if REMOTION_FUNCTION_NAME is not set */
79+
/** Default Lambda function name if not configured in Sanity */
8080
const DEFAULT_FUNCTION_NAME = "remotion-render-4-0-431";
8181

8282
/** Polling interval in ms when waiting for render completion */
@@ -128,26 +128,28 @@ function mapInputProps(input: RenderInput): Record<string, unknown> {
128128
// ---------------------------------------------------------------------------
129129

130130
/**
131-
* Get Remotion Lambda configuration from environment variables.
132-
* Throws if any required env var is missing.
131+
* Get Remotion Lambda configuration from Sanity config + environment variables.
132+
* AWS credentials come from env vars; Remotion-specific config from Sanity.
133+
* Throws if any required value is missing.
133134
*/
134135
export async function getRemotionConfig(): Promise<RemotionLambdaConfig> {
135136
const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID;
136137
const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
137-
const region = await getConfigValue("remotion_config", "awsRegion", process.env.REMOTION_AWS_REGION);
138-
const serveUrl = await getConfigValue("remotion_config", "serveUrl", process.env.REMOTION_SERVE_URL);
138+
const region = await getConfigValue("remotion_config", "awsRegion", "us-east-1");
139+
const serveUrl = await getConfigValue("remotion_config", "serveUrl", undefined);
139140

140141
const missing: string[] = [];
141142
if (!awsAccessKeyId) missing.push("AWS_ACCESS_KEY_ID");
142143
if (!awsSecretAccessKey) missing.push("AWS_SECRET_ACCESS_KEY");
143-
if (!region) missing.push("REMOTION_AWS_REGION");
144-
if (!serveUrl) missing.push("REMOTION_SERVE_URL");
144+
if (!region) missing.push("remotion_config.awsRegion");
145+
if (!serveUrl) missing.push("remotion_config.serveUrl");
145146

146147
if (missing.length > 0) {
147148
throw new Error(
148-
`[REMOTION] Missing required environment variables: ${missing.join(", ")}. ` +
149-
`Set these before calling any render function. ` +
150-
`REMOTION_SERVE_URL is generated by running deployRemotionLambda().`
149+
`[REMOTION] Missing required configuration: ${missing.join(", ")}. ` +
150+
`AWS credentials come from env vars. Remotion config (region, serveUrl, functionName) ` +
151+
`is managed in the Sanity Remotion Config singleton. ` +
152+
`serveUrl is generated by running deployRemotionLambda().`
151153
);
152154
}
153155

@@ -160,10 +162,10 @@ export async function getRemotionConfig(): Promise<RemotionLambdaConfig> {
160162
}
161163

162164
/**
163-
* Get the Lambda function name from env or use the default.
165+
* Get the Lambda function name from Sanity config or use the default.
164166
*/
165167
async function getFunctionName(): Promise<string> {
166-
return getConfigValue("remotion_config", "functionName", process.env.REMOTION_FUNCTION_NAME || DEFAULT_FUNCTION_NAME);
168+
return getConfigValue("remotion_config", "functionName", "remotion-render-4-0-431-mem2048mb-disk2048mb-240sec");
167169
}
168170

169171
// ---------------------------------------------------------------------------
@@ -239,7 +241,7 @@ async function startRender(
239241
throw new Error(
240242
`[REMOTION] Failed to trigger Lambda render for "${composition}": ${message}. ` +
241243
`Ensure the Lambda function "${functionName}" is deployed in ${region} ` +
242-
`and REMOTION_SERVE_URL points to a valid Remotion bundle.`
244+
`and the Remotion Config serveUrl points to a valid Remotion bundle.`
243245
);
244246
}
245247
}

lib/sponsor/gemini-intent.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { GoogleGenerativeAI } from '@google/generative-ai'
2+
import { getConfigValue } from '@/lib/config'
23

34
const SPONSORSHIP_TIERS = [
45
'dedicated-video',
@@ -33,8 +34,9 @@ export async function extractSponsorIntent(message: string): Promise<SponsorInte
3334
}
3435
}
3536

37+
const geminiModel = await getConfigValue('pipeline_config', 'geminiModel', 'gemini-2.0-flash')
3638
const genAI = new GoogleGenerativeAI(apiKey)
37-
const model = genAI.getGenerativeModel({ model: process.env.GEMINI_MODEL || 'gemini-2.5-flash' })
39+
const model = genAI.getGenerativeModel({ model: geminiModel })
3840

3941
const prompt = `You are analyzing an inbound sponsorship inquiry for CodingCat.dev, a developer education platform with YouTube videos, podcasts, blog posts, and newsletters.
4042

lib/youtube-upload.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { google } from "googleapis";
22
import { Readable } from "node:stream";
3+
import { getConfigValue } from "@/lib/config";
34

45
const oauth2Client = new google.auth.OAuth2(
56
process.env.YOUTUBE_CLIENT_ID,
@@ -11,12 +12,12 @@ oauth2Client.setCredentials({
1112

1213
const youtube = google.youtube({ version: "v3", auth: oauth2Client });
1314

14-
function getDefaultPrivacyStatus(): "public" | "private" | "unlisted" {
15-
const envValue = process.env.YOUTUBE_UPLOAD_VISIBILITY?.toLowerCase();
16-
if (envValue === "public" || envValue === "private" || envValue === "unlisted") {
17-
return envValue;
15+
async function getDefaultPrivacyStatus(): Promise<"public" | "private" | "unlisted"> {
16+
const value = await getConfigValue("pipeline_config", "youtubeUploadVisibility", "private");
17+
if (value === "public" || value === "private" || value === "unlisted") {
18+
return value;
1819
}
19-
return "private"; // Default to private when not set
20+
return "private"; // Default to private when not set or invalid
2021
}
2122

2223
interface UploadOptions {
@@ -36,7 +37,7 @@ export async function uploadVideo(opts: UploadOptions): Promise<{ videoId: strin
3637
const response = await fetch(opts.videoUrl);
3738
if (!response.ok) throw new Error(`Failed to fetch video: ${response.statusText}`);
3839

39-
const resolvedPrivacyStatus = opts.privacyStatus || getDefaultPrivacyStatus();
40+
const resolvedPrivacyStatus = opts.privacyStatus || await getDefaultPrivacyStatus();
4041
console.log(`[youtube-upload] Uploading "${opts.title.slice(0, 60)}" with privacy: ${resolvedPrivacyStatus}`);
4142
// Convert Web ReadableStream to Node.js Readable stream
4243
// googleapis expects a Node.js stream with .pipe(), not a Web ReadableStream

sanity/schemas/singletons/contentConfig.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default defineType({
1010
name: "rssFeeds",
1111
title: "RSS Feeds",
1212
type: "array",
13+
description: "RSS/Atom feeds to monitor for trending topics. The ingest cron checks these daily for new content ideas",
1314
of: [
1415
{
1516
type: "object",
@@ -42,6 +43,7 @@ export default defineType({
4243
name: "trendSourcesEnabled",
4344
title: "Trend Sources Enabled",
4445
type: "object",
46+
description: "Toggle individual trend discovery sources on/off. Disabling a source skips it during the daily ingest scan",
4547
fields: [
4648
defineField({
4749
name: "hn",
@@ -79,24 +81,28 @@ export default defineType({
7981
name: "systemInstruction",
8082
title: "System Instruction",
8183
type: "text",
84+
description: "The AI system prompt used for script generation. Defines the writing style, tone, and format for all generated video scripts",
8285
initialValue: "You are a content strategist and scriptwriter for CodingCat.dev, a web development education channel run by Alex Patterson.\n\nYour style is inspired by Cleo Abram's \"Huge If True\" — you make complex technical topics feel exciting, accessible, and important. Key principles:\n- Start with a BOLD claim or surprising fact that makes people stop scrolling\n- Use analogies and real-world comparisons to explain technical concepts\n- Build tension: \"Here's the problem... here's why it matters... here's the breakthrough\"\n- Keep energy HIGH — short sentences, active voice, conversational tone\n- End with a clear takeaway that makes the viewer feel smarter\n- Target audience: developers who want to stay current but don't have time to read everything\n\nScript format: 60-90 second explainer videos. Think TikTok/YouTube Shorts energy with real educational depth.\n\nCodingCat.dev covers: React, Next.js, TypeScript, Svelte, web APIs, CSS, Node.js, cloud services, AI/ML for developers, and web platform updates.",
8386
}),
8487
defineField({
8588
name: "targetVideoDurationSec",
8689
title: "Target Video Duration (sec)",
8790
type: "number",
91+
description: "Target duration for generated videos in seconds. Scripts are calibrated to this length",
8892
initialValue: 90,
8993
}),
9094
defineField({
9195
name: "sceneCountMin",
9296
title: "Scene Count Min",
9397
type: "number",
98+
description: "Minimum number of scenes per video. The AI generates at least this many visual segments",
9499
initialValue: 3,
95100
}),
96101
defineField({
97102
name: "sceneCountMax",
98103
title: "Scene Count Max",
99104
type: "number",
105+
description: "Maximum number of scenes per video. Keeps videos focused and within duration targets",
100106
initialValue: 5,
101107
}),
102108
],

sanity/schemas/singletons/distributionConfig.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,30 @@ export default defineType({
1010
name: "notificationEmails",
1111
title: "Notification Emails",
1212
type: "array",
13+
description: "Email addresses that receive notifications when a new video is published",
1314
of: [{ type: "string" }],
1415
initialValue: ["alex@codingcat.dev"],
1516
}),
1617
defineField({
1718
name: "youtubeDescriptionTemplate",
1819
title: "YouTube Description Template",
1920
type: "text",
21+
description: "Template for YouTube video descriptions. Use {{title}} and {{summary}} placeholders",
2022
initialValue: "{{title}}\n\n{{summary}}\n\n🔗 Learn more at https://codingcat.dev\n\n#webdev #coding #programming",
2123
}),
2224
defineField({
2325
name: "youtubeDefaultTags",
2426
title: "YouTube Default Tags",
2527
type: "array",
28+
description: "Default tags applied to all uploaded YouTube videos. Topic-specific tags are added by AI",
2629
of: [{ type: "string" }],
2730
initialValue: ["web development", "coding", "programming", "tutorial", "codingcat"],
2831
}),
2932
defineField({
3033
name: "resendFromEmail",
3134
title: "Resend From Email",
3235
type: "string",
36+
description: "Sender email address for notification emails via Resend",
3337
initialValue: "content@codingcat.dev",
3438
}),
3539
],

sanity/schemas/singletons/gcsConfig.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ export default defineType({
1010
name: "bucketName",
1111
title: "Bucket Name",
1212
type: "string",
13+
description: "Google Cloud Storage bucket name for storing video assets, audio files, and renders",
1314
initialValue: "codingcatdev-content-engine",
1415
}),
1516
defineField({
1617
name: "projectId",
1718
title: "Project ID",
1819
type: "string",
20+
description: "Google Cloud project ID that owns the GCS bucket",
1921
initialValue: "codingcatdev",
2022
}),
2123
],

0 commit comments

Comments
 (0)