Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
90fd9bf
feat: add scene type fields to automatedVideo schema
Mar 4, 2026
daf26c9
feat: add sponsor bridge + restore detailed Gemini system instruction
Mar 4, 2026
02035cf
feat: wire sponsor bridge into Stripe webhook + idempotency guard + v…
Mar 4, 2026
9c18b5b
fix: migration script downloads only originals, strips transformation…
Mar 4, 2026
7b1f3f3
chore: add orphan asset cleanup script
Mar 4, 2026
06c1f8d
fix: RSS feed improvements — podcast iTunes support, proper enclosure…
codercatdev Mar 4, 2026
4e9937c
fix: add source-path to all notebook-scoped RPC calls
codercatdev Mar 4, 2026
7d88229
fix: RSS feeds, runtime Cloudinary cleanup, dead code removal (#600)
codercatdev Mar 4, 2026
5a51429
feat: add Vercel Analytics, Speed Insights, and Umami analytics
codercatdev Mar 4, 2026
99c7105
feat: wire up SiteAnalytics in layouts and add Vercel packages to pac…
codercatdev Mar 4, 2026
e1ca775
feat: add SiteAnalytics to dashboard layout, add Vercel packages to p…
codercatdev Mar 4, 2026
980d5b8
fix: capture fresh Set-Cookie headers from NotebookLM homepage
codercatdev Mar 4, 2026
eb37117
debug: add URL and body length logging to rpcCall + cache:no-store
codercatdev Mar 4, 2026
ac794b8
fix: extract notebook ID from index 2, not index 0
codercatdev Mar 5, 2026
dea459a
fix: rewrite pollResearch response parsing for actual API format
codercatdev Mar 5, 2026
4c555e5
feat: add source URLs to notebook + increase research timeout to 10min
codercatdev Mar 5, 2026
9ba76c6
feat: pass trend signal URLs as notebook sources for research
codercatdev Mar 5, 2026
cd578f2
fix(notebooklm): add getSourceIds(), auto-fetch source IDs for artifa…
Mar 5, 2026
e844d3b
feat(research): generate multiple infographics per topic, infographic…
Mar 5, 2026
e271c98
fix(ingest): update to handle infographicUrls array from research pay…
Mar 5, 2026
4ceb80f
Merge feat/scene-type-schema into dev — scene type fields for automat…
Mar 5, 2026
5b88bfe
Merge feat/sponsor-bridge into dev — sponsor bridge function + restor…
Mar 5, 2026
ead86dd
Merge feat/sponsor-bridge-webhook into dev — Stripe webhook bridge wi…
Mar 5, 2026
ec1ff93
Merge feature/cloudinary-to-sanity-migration into dev — RSS improveme…
Mar 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AppSidebar } from "@/components/app-sidebar";
import { SiteHeader } from "@/components/site-header";
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
import { Toaster } from "@/components/ui/sonner";
import { SiteAnalytics } from "@/components/analytics";

const nunito = Nunito({
subsets: ["latin"],
Expand Down Expand Up @@ -83,6 +84,7 @@ export default async function DashboardLayout({
)}
<Toaster />
</ThemeProvider>
<SiteAnalytics />
</body>
</html>
);
Expand Down
2 changes: 1 addition & 1 deletion app/(main)/(course)/courses/rss.xml/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export async function GET() {
});
return new Response(feed.rss2(), {
headers: {
"content-type": "text/xml",
"content-type": "application/rss+xml; charset=utf-8",
"cache-control": "max-age=0, s-maxage=3600",
},
});
Expand Down
11 changes: 4 additions & 7 deletions app/(main)/(podcast)/podcasts/rss.xml/route.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
export const dynamic = "force-dynamic"; // defaults to auto

import { buildFeed } from "@/lib/rss";
import { ContentType } from "@/lib/types";
import { buildPodcastFeed } from "@/lib/rss";

export async function GET() {
const feed = await buildFeed({
type: ContentType.podcast,
});
return new Response(feed.rss2(), {
const xml = await buildPodcastFeed({});
return new Response(xml, {
headers: {
"content-type": "text/xml",
"content-type": "application/rss+xml; charset=utf-8",
"cache-control": "max-age=0, s-maxage=3600",
},
});
Expand Down
2 changes: 1 addition & 1 deletion app/(main)/(post)/blog/rss.xml/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export async function GET() {
});
return new Response(feed.rss2(), {
headers: {
"content-type": "text/xml",
"content-type": "application/rss+xml; charset=utf-8",
"cache-control": "max-age=0, s-maxage=3600",
},
});
Expand Down
2 changes: 2 additions & 0 deletions app/(main)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { toPlainText } from "next-sanity";
import { VisualEditing } from "next-sanity/visual-editing";
import { DisableDraftMode } from "@/components/disable-draft-mode";
import { ModeToggle } from "@/components/mode-toggle";
import { SiteAnalytics } from "@/components/analytics";

const nunito = Nunito({
subsets: ["latin"],
Expand Down Expand Up @@ -156,6 +157,7 @@ export default async function RootLayout({
</ThemeProvider>
</PlayerProvider>
</CookiesProviderClient>
<SiteAnalytics />
</body>
</html>
);
Expand Down
28 changes: 22 additions & 6 deletions app/api/cron/ingest/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ const FALLBACK_TRENDS: TrendResult[] = [
slug: "webassembly-web-apps",
score: 60,
signals: [{ source: "blog", title: "WebAssembly", url: "https://webassembly.org/", score: 60 }],
whyTrending: "WASM adoption growing in production apps",
whyTrending: "WASM adoption growing in [REDACTED SECRET: NEXT_PUBLIC_SANITY_DATASET] apps",
suggestedAngle: "Real-world use cases where WASM outperforms JS",
},
];
Expand All @@ -109,8 +109,19 @@ const FALLBACK_TRENDS: TrendResult[] = [
// Gemini Script Generation
// ---------------------------------------------------------------------------

const SYSTEM_INSTRUCTION =
"You are a content strategist for CodingCat.dev, a web development education channel. You create engaging, Cleo Abram-style explainer video scripts that are educational, energetic, and concise (60-90 seconds).";
const SYSTEM_INSTRUCTION = `You are a content strategist and scriptwriter for CodingCat.dev, a web development education channel run by Alex Patterson.

Your style is inspired by Cleo Abram's "Huge If True" — you make complex technical topics feel exciting, accessible, and important. Key principles:
- Start with a BOLD claim or surprising fact that makes people stop scrolling
- Use analogies and real-world comparisons to explain technical concepts
- Build tension: "Here's the problem... here's why it matters... here's the breakthrough"
- Keep energy HIGH — short sentences, active voice, conversational tone
- End with a clear takeaway that makes the viewer feel smarter
- Target audience: developers who want to stay current but don't have time to read everything

Script format: 60-90 second explainer videos. Think TikTok/YouTube Shorts energy with real educational depth.

CodingCat.dev covers: React, Next.js, TypeScript, Svelte, web APIs, CSS, Node.js, cloud services, AI/ML for developers, and web platform updates.`;

function buildPrompt(trends: TrendResult[], research?: ResearchPayload): string {
const topicList = trends
Expand Down Expand Up @@ -152,8 +163,8 @@ function buildPrompt(trends: TrendResult[], research?: ResearchPayload): string
}
}

if (research.infographicUrl) {
researchContext += `\n### Infographic Available\nAn infographic has been generated for this topic. Use sceneType "narration" with bRollUrl pointing to the infographic for at least one scene.\n`;
if (research.infographicUrls && research.infographicUrls.length > 0) {
researchContext += `\n### Infographics Available (${research.infographicUrls.length})\nMultiple infographics have been generated for this topic. Use sceneType "narration" with bRollUrl pointing to an infographic for visual scenes.\n`;
}
}

Expand Down Expand Up @@ -403,7 +414,12 @@ export async function GET(request: NextRequest) {
if (process.env.ENABLE_NOTEBOOKLM_RESEARCH === "true") {
console.log(`[CRON/ingest] Conducting research on: "${trends[0].topic}"...`);
try {
research = await conductResearch(trends[0].topic);
// Extract source URLs from trend signals to seed the notebook
const sourceUrls = (trends[0].signals ?? [])
.map((s: { url?: string }) => s.url)
.filter((u): u is string => !!u && u.startsWith("http"))
.slice(0, 5);
research = await conductResearch(trends[0].topic, { sourceUrls });
console.log(`[CRON/ingest] Research complete: ${research.sources.length} sources, ${research.sceneHints.length} scene hints`);
} catch (err) {
console.warn("[CRON/ingest] Research failed, continuing without:", err);
Expand Down
18 changes: 17 additions & 1 deletion app/api/webhooks/stripe-sponsor/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
import { sanityWriteClient } from '@/lib/sanity-write-client'
import { bridgeSponsorLeadToSponsor } from '@/lib/sponsor/sponsor-bridge'

/**
* Stripe webhook handler for sponsor invoices.
Expand Down Expand Up @@ -69,6 +70,12 @@ export async function POST(request: Request) {
break
}

// Idempotency guard — skip if already paid (Stripe retries on 5xx)
if (lead.status === 'paid') {
console.log('[SPONSOR] Lead already paid, skipping (idempotent):', lead._id)
break
}

// Update status to 'paid'
await sanityWriteClient
.patch(lead._id)
Expand All @@ -80,9 +87,18 @@ export async function POST(request: Request) {

console.log('[SPONSOR] Updated sponsorLead to paid:', lead._id)

// Bridge: create/link sponsor doc for content attribution
try {
await bridgeSponsorLeadToSponsor(lead._id)
console.log('[SPONSOR] Sponsor bridge completed for lead:', lead._id)
} catch (bridgeError) {
// Non-fatal — don't fail the webhook if bridge fails
console.error('[SPONSOR] Sponsor bridge failed (non-fatal):', bridgeError)
}

// Find next available automatedVideo (status script_ready or later, no sponsorSlot assigned)
const availableVideo = await sanityWriteClient.fetch(
`*[_type == "automatedVideo" && status in ["script_ready", "media_ready", "ready_to_publish"] && !defined(bookedSlot)][0]{
`*[_type == "automatedVideo" && status in ["script_ready", "media_ready", "ready_to_publish"] && !defined(bookedSlot)] | order(_createdAt asc) [0]{
_id,
title,
status
Expand Down
5 changes: 2 additions & 3 deletions app/api/youtube/rss.xml/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ export async function GET() {
updated: new Date(),
generator: "Next.js using Feed for Node.js",
feedLinks: {
json: `${process.env.NEXT_PUBLIC_BASE_URL}/api/podcast-feed`,
atom: `${process.env.NEXT_PUBLIC_BASE_URL}/api/podcast-feed?format=atom`,
rss2: `${process.env.NEXT_PUBLIC_BASE_URL || "https://codingcat.dev"}/api/youtube/rss.xml`,
},
});

Expand Down Expand Up @@ -93,7 +92,7 @@ export async function GET() {

return new Response(feed.rss2(), {
headers: {
"content-type": "text/xml",
"content-type": "application/rss+xml; charset=utf-8",
"cache-control": "max-age=0, s-maxage=3600",
},
});
Expand Down
21 changes: 21 additions & 0 deletions components/analytics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client";

import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
import Script from "next/script";

export function SiteAnalytics() {
return (
<>
<Analytics />
<SpeedInsights />
{process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
<Script
src={process.env.NEXT_PUBLIC_UMAMI_URL || "https://analytics.codingcat.dev/script.js"}
data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
strategy="afterInteractive"
/>
)}
</>
);
}
64 changes: 64 additions & 0 deletions docs/umami-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Umami Analytics Setup

## Overview
Umami is self-hosted on Vercel + Supabase for $0/month analytics.

## Setup Steps

### 1. Fork Umami
Fork https://github.com/umami-is/umami to the CodingCatDev GitHub org.

### 2. Create Umami tables in Supabase
Run the Umami PostgreSQL schema in your Supabase SQL editor.
See: https://umami.is/docs/install

### 3. Deploy to Vercel
- Import the forked repo in Vercel
- Set environment variable: `DATABASE_URL` = your Supabase connection string
- Deploy

### 4. Configure
- Visit your Umami instance (e.g., analytics.codingcat.dev)
- Create a website entry for codingcat.dev
- Copy the Website ID

### 5. Set environment variables in codingcat.dev
```
NEXT_PUBLIC_UMAMI_WEBSITE_ID=<your-website-id>
NEXT_PUBLIC_UMAMI_URL=https://analytics.codingcat.dev/script.js
```

### 6. Custom domain (optional)
Add `analytics.codingcat.dev` as a custom domain in Vercel for the Umami project.

## Querying Analytics Data
Since Umami writes to your Supabase database, you can query analytics directly:

```sql
-- Top pages last 30 days
SELECT url_path, COUNT(*) as views
FROM website_event
WHERE website_id = '<your-id>'
AND created_at > NOW() - INTERVAL '30 days'
AND event_type = 1
GROUP BY url_path
ORDER BY views DESC
LIMIT 20;

-- Sponsor report: views by content type
SELECT
CASE
WHEN url_path LIKE '/post/%' THEN 'Blog Post'
WHEN url_path LIKE '/podcast/%' THEN 'Podcast'
WHEN url_path LIKE '/course/%' THEN 'Course'
ELSE 'Other'
END as content_type,
COUNT(*) as views,
COUNT(DISTINCT session_id) as unique_visitors
FROM website_event
WHERE website_id = '<your-id>'
AND created_at > NOW() - INTERVAL '30 days'
AND event_type = 1
GROUP BY content_type
ORDER BY views DESC;
```
Loading
Loading