Automated AI Business Dashboard
Build a weekly AI dashboard that fetches YouTube trends and posts.
Introduction
30 Second Summary
Staying on top of AI and business trends takes hours every week. Most professionals either pay for expensive tools or fall behind.
In this project, you will build a fully automated dashboard that fetches trending YouTube videos about AI and business, sends them to an AI model for analysis, and displays actionable insights and ready-to-post LinkedIn content on a live website.
What You'll Build
A Next.js dashboard powered by a Node.js automation script, GitHub Actions for weekly scheduling, and Gemini 2.5 Flash via OpenRouter for AI-powered analysis.
By the end of this project, you'll have:
- A Node.js automation script that fetches trending YouTube videos and generates AI-powered business insights and LinkedIn posts.
- A live Next.js dashboard displaying trend cards, business insights, and copy-to-clipboard LinkedIn posts.
- A GitHub Actions workflow that runs every Monday, commits fresh data, and triggers a site rebuild automatically.
- Secret Mission: Add a "Download Weekly Brief" button that generates a printable PDF of the week's insights.
Are there any prerequisites?
You'll need a GitHub account, an OpenRouter account with credits, and a Google account (for a free YouTube API key). You'll set up the YouTube API key from scratch in Step 2.
Set Up the Project
Before you can build your automated business intelligence dashboard, you need a working development environment. Next.js is the framework that powers your dashboard, and it requires a specific version of Node.js to run.
In this step, you will verify your Node.js version, scaffold a brand new Next.js 16 project, and create the folder structure your automation pipeline needs.
In this step, get ready to:
- Verify Node.js is at version 20.9 or higher and scaffold a Next.js project.
- Create the project folder structure and environment file.
- Confirm the development server runs successfully.
Verify Node.js and scaffold your Next.js project
Next.js 16 requires Node.js version 20.9.0 or higher. Let's confirm your version meets this requirement.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), then typing Terminal and pressing Enter.
- Check your Node.js version by running this command:
node --version
You should see a version number printed in your terminal.
✔️ I see version 20.9 or higher
Your Node.js version is compatible with Next.js 16. You are ready to scaffold the project.
ⓧ I see a version lower than 20.9
Your installed Node.js is too old for Next.js 16. You need to upgrade to at least 20.9.0.
- Go to nodejs.org and download the latest LTS version.
- Run the installer and follow the prompts to complete the upgrade.
- Close and reopen your terminal, then run node --version again to confirm the update.
macOS users with an older system Node
macOS sometimes ships with an older Node.js that shadows newer installs. If you still see an old version after installing, try using the full path to the new binary or a version manager like nvm.
ⓧ Command not found
Node.js is not installed on your machine. You need to install version 20.9.0 or higher.
- Go to nodejs.org and download the latest LTS version.
- Run the installer and follow the prompts.
- Close and reopen your terminal, then run node --version to confirm it is installed.
Now that Node.js is confirmed, you can scaffold the Next.js project.
- Move to your Desktop by running this command:
cd ~/Desktop
- Scaffold your Next.js project by running this command:
npx create-next-app@latest ai-business-weekly
What will this command do?
This command downloads the latest version of create-next-app and creates a new Next.js 16 project in a folder called ai-business-weekly. It will launch an interactive wizard that asks you a few setup questions.
The wizard will first ask for your project name, then ask if you want to use the recommended defaults.
- When prompted with "What is your project named?", press Enter to accept ai-business-weekly (it is already filled in from the command).
- When prompted with "Would you like to use the recommended Next.js defaults?", select Yes, use recommended defaults and press Enter.
What do the recommended defaults include?
The recommended defaults set up TypeScript, ESLint, Tailwind CSS, and App Router. Turbopack is the default bundler in Next.js 16, so it is enabled automatically without needing a separate prompt.
The src/ directory is not used (your code lives at the project root in the app/ folder), and the import alias defaults to @/*. This is exactly the configuration the project needs.
The CLI will install all dependencies. This may take a minute depending on your internet speed.
- Confirm the folder was created by running:
ls ai-business-weekly
You should see files like package.json, app, tsconfig.json, and other project files listed.
Scaffold command failed?
If you see a network error, check your internet connection and try again. If npx is not recognized, your Node.js installation may not have completed properly. Reinstall from nodejs.org.
help me troubleshoot why npx create-next-app failed
Create the project folders and environment file
Your automation pipeline needs a few extra folders that the scaffold does not create. You also need an environment file to store your API keys securely.
- Open VS Code by pressing Cmd+Space (macOS) or the Windows key (Windows), typing VS Code, and pressing Enter.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the ai-business-weekly folder, and click Open.
Now open the integrated terminal inside VS Code to create the additional folders.
- Open the terminal in VS Code by pressing Ctrl+` (backtick key, same shortcut on macOS and Windows).
- Create the folders your project needs by running these commands:
mkdir scripts
mkdir -p public/data
mkdir -p .github/workflows
What are these folders for?
The scripts/ folder holds your automation script that fetches YouTube videos and generates AI insights.
The public/data/ folder stores the JSON output that your dashboard reads.
The .github/workflows/ folder holds your GitHub Actions workflow file that automates everything weekly.
- Confirm the folders exist by running:
ls
You should see scripts listed alongside the existing files like app and package.json.
- Create the environment file by running this command:
touch .env.local
- In the VS Code Explorer sidebar on the left, click on .env.local to open it.
- Paste the following placeholder content into the file:
YOUTUBE_API_KEY=your-youtube-api-key-here
OPENROUTER_API_KEY=your-openrouter-api-key-here
SEARCH_QUERY=AI business automation tools 2025
What is this file for?
The .env.local file stores environment variables that your scripts need (API keys and search configuration). Next.js automatically loads this file during development. You will replace the placeholder values with real keys in the next step.
- Save .env.local by pressing Cmd+S (macOS) or Ctrl+S (Windows).
✔️ Awesome, I've got everything!
Great. Double check you have saved .env.local and that it contains all three lines.
ⓧ I'd like to double check the full code
Here is the complete .env.local file for comparison:
YOUTUBE_API_KEY=your-youtube-api-key-here
OPENROUTER_API_KEY=your-openrouter-api-key-here
SEARCH_QUERY=AI business automation tools 2025
Don't see .env.local in the sidebar?
Files starting with a dot are hidden by default on some systems. In VS Code, the Explorer sidebar shows all files regardless. If you do not see it, make sure you ran touch .env.local from inside the ai-business-weekly folder (check with pwd).
help me find my .env.local file in VS Code
Verify the development server
The final check is to confirm Next.js starts correctly. This proves your scaffold is healthy and all dependencies installed properly.
- Start the development server by running this command in your VS Code terminal:
npm run dev
- Open your browser and navigate to http://localhost:3000.
You should see the Next.js welcome page with the Next.js logo and links to documentation.
- Return to your VS Code terminal and press Ctrl+C to stop the development server.
Page not loading at localhost:3000?
Make sure you ran npm run dev from inside the ai-business-weekly folder. Check your terminal output for errors. If port 3000 is already in use, Next.js will suggest a different port in the terminal output.
help me fix my Next.js dev server not starting
Your development environment is fully set up. Next up, you will get the API keys that power your automation script.
Get Your API Keys
Your project needs two external services to work. The YouTube Data API v3 fetches trending video data, and OpenRouter sends that data to an AI model for analysis.
In this step, you will create a Google Cloud project, enable the YouTube API, and verify that both API keys can successfully connect to their respective services.
In this step, get ready to:
- Create a Google Cloud project and get a YouTube Data API v3 key.
- Configure your OpenRouter API key in the project.
- Verify both keys return successful responses.
Get your YouTube Data API v3 key
The YouTube Data API v3 gives your script programmatic access to search results, video metadata, and view counts. You need to create a Google Cloud project and enable this API before you can make requests.
- Open your browser and go to console.cloud.google.com.
- Sign in with your Google account.
- Click the project dropdown at the top of the page.
- Click New Project.
- Enter AI Business Weekly as the project name.
- Click Create.
- Wait a few seconds for the project to be created, then select it from the project dropdown if it does not auto-select.
Now enable the YouTube Data API v3 for this project.
- In the left sidebar, click APIs & Services, then click Library.
- In the search bar, type YouTube Data API v3 and press Enter.
- Click on YouTube Data API v3 in the results.
- Click Enable.
With the API enabled, create your API key.
- In the left sidebar, click APIs & Services, then click Credentials.
- Click Create Credentials at the top of the page.
- Select API key from the dropdown menu.
- A dialog appears with your new API key string. Copy it now.
- Click Close.
What is the YouTube Data API v3?
The YouTube Data API v3 provides programmatic access to YouTube's public data. Your script uses it to search for trending videos and fetch their view counts.
The free tier gives you 100 search calls per day in its own quota bucket, plus 10,000 units per day for other calls like fetching video statistics. Your weekly script uses just 2 calls per run, well within these limits.
Now paste your YouTube API key into the project's environment file.
- Switch back to VS Code.
- Open the .env.local file from the Explorer sidebar.
- Find the line that says YOUTUBE_API_KEY=your-youtube-api-key-here.
- Replace your-youtube-api-key-here with the API key you just copied from Google Cloud Console.
- Save the file.
Keep your API key safe
The .env.local file is already in .gitignore by default in Next.js projects. This means your keys never get committed to GitHub.
Configure your OpenRouter key
OpenRouter provides access to dozens of AI models through a single API. Your script sends video data to Gemini 2.5 Flash for analysis, which costs roughly $0.02 per weekly run from your existing credits.
- Open openrouter.ai/settings/keys in your browser.
- Copy your existing API key.
- Switch back to .env.local in VS Code.
- Find the line that says OPENROUTER_API_KEY=your-openrouter-api-key-here.
- Replace your-openrouter-api-key-here with your OpenRouter API key.
- Save the file.
Your SEARCH_QUERY value is already set to AI business automation tools 2025 from Step 1. No changes needed there.
Why OpenRouter instead of calling Google directly?
OpenRouter acts as a gateway to many AI models with a single API key. You already have credits loaded for Gemini models, so there is no extra account or billing setup required.
The model ID your script uses is google/gemini-2.5-flash, which costs $0.30 per million input tokens and $2.50 per million output tokens. For 10 videos per week, that works out to roughly $0.02 per run.
Verify both keys work
Before building the full script, confirm that both APIs accept your keys and return real data. Node.js can load your .env.local file directly using the --env-file flag.
- Test your YouTube API key by running this command in the VS Code terminal:
node --env-file=.env.local -e "fetch('https://www.googleapis.com/youtube/v3/search?part=snippet&q=AI&maxResults=1&key='+process.env.YOUTUBE_API_KEY).then(r=>r.json()).then(d=>{if(d.items)console.log('YouTube API works! Found:',d.items[0].snippet.title);else console.log('Error:',JSON.stringify(d))})"
What does this command do?
The --env-file=.env.local flag loads your environment variables into the Node.js process. The -e flag runs an inline JavaScript expression.
The script calls the YouTube search endpoint with your API key and prints the title of the first result. If the key is invalid, it prints the error response instead.
- You should see output like YouTube API works! Found: followed by a video title.
- Test your OpenRouter API key by running this command:
node --env-file=.env.local -e "fetch('https://openrouter.ai/api/v1/chat/completions',{method:'POST',headers:{'Authorization':'Bearer '+process.env.OPENROUTER_API_KEY,'Content-Type':'application/json'},body:JSON.stringify({model:'google/gemini-2.5-flash',messages:[{role:'user',content:'Say hello in 5 words'}]})}).then(r=>r.json()).then(d=>{if(d.choices)console.log('OpenRouter works! Response:',d.choices[0].message.content);else console.log('Error:',JSON.stringify(d))})"
- You should see output like OpenRouter works! Response: followed by a short AI-generated greeting.
Seeing an error from YouTube?
If you see a 403 error or a message about the API not being enabled, go back to the Google Cloud Console and confirm the YouTube Data API v3 shows as Enabled under APIs & Services. Make sure you selected the correct project (AI Business Weekly) from the project dropdown.
If you see a 400 error about an invalid key, double-check that you copied the full key string into .env.local with no extra spaces or line breaks.
help me debug my YouTube API key error
Seeing an error from OpenRouter?
If you see a 401 or authentication error, make sure your OpenRouter key starts with sk-or- and has no trailing whitespace in .env.local.
If you see a credits or billing error, check that your OpenRouter account has available credits at openrouter.ai/settings/credits.
help me debug my OpenRouter API key error
Both APIs are connected and returning data. Next up, you will build the automation script that fetches trending videos and sends them to AI for analysis.
Build the Automation Script
Your API keys are configured and verified. Now it's time to build the script that ties them together into a fully automated pipeline.
The script you're about to write is the engine behind everything. It fetches trending YouTube videos, sends each one to Gemini 2.5 Flash for business analysis, and saves the results as a JSON file your dashboard will read.
In this step, get ready to:
- Build a function that fetches trending YouTube videos with view counts.
- Build a function that sends video data to AI and gets back business insights and LinkedIn posts.
- Run the full pipeline and generate a JSON data file for your dashboard.
Create the script and fetch trending videos
The YouTube Data API v3 has two endpoints you'll combine. The search endpoint finds videos matching your query, and the videos endpoint fetches their view counts.
- In VS Code, open the scripts/ folder in the Explorer sidebar.
- Create a new file called generate-weekly-pulse.mjs inside it.
- Paste the following code into the file:
import fs from "node:fs";
import path from "node:path";
const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY;
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY;
const SEARCH_QUERY = process.env.SEARCH_QUERY || "AI business automation tools 2025";
async function fetchTrendingVideos(apiKey) {
// Calculate the date 7 days ago so we only get recent videos
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
const publishedAfter = sevenDaysAgo.toISOString();
// Build the search query parameters
const searchParams = new URLSearchParams({
part: "snippet",
q: SEARCH_QUERY,
type: "video",
order: "viewCount",
publishedAfter: publishedAfter,
maxResults: "10",
key: apiKey,
});
// Call the YouTube search endpoint
const searchResponse = await fetch(
`https://www.googleapis.com/youtube/v3/search?${searchParams}`
);
if (!searchResponse.ok) {
throw new Error(`YouTube Search API error: ${searchResponse.status}`);
}
const searchData = await searchResponse.json();
const videoIds = searchData.items.map((item) => item.id.videoId).join(",");
// Fetch view counts for each video using the videos endpoint
const statsParams = new URLSearchParams({
part: "statistics",
id: videoIds,
key: apiKey,
});
const statsResponse = await fetch(
`https://www.googleapis.com/youtube/v3/videos?${statsParams}`
);
if (!statsResponse.ok) {
throw new Error(`YouTube Videos API error: ${statsResponse.status}`);
}
const statsData = await statsResponse.json();
const statsMap = new Map(statsData.items.map((item) => [item.id, item.statistics]));
// Combine search results with view counts into a clean object
return searchData.items.map((item) => ({
videoId: item.id.videoId,
title: item.snippet.title,
channel: item.snippet.channelTitle,
thumbnail: item.snippet.thumbnails.high.url,
url: `https://www.youtube.com/watch?v=${item.id.videoId}`,
views: parseInt(statsMap.get(item.id.videoId)?.viewCount || "0"),
description: item.snippet.description,
}));
}
What does this code do?
The imports pull in Node.js built-in modules for file system operations and path handling. The three constants read your API keys and search query from environment variables.
The fetchTrendingVideos() function makes two API calls. First, it searches for videos matching your query from the past 7 days, sorted by view count. Then it fetches the actual view count statistics for each video. It combines both into a clean array of video objects.
Why use the .mjs file extension?
Next.js projects don't set "type": "module" in package.json. The .mjs extension enables ES module syntax (like import and export) without modifying project configuration.
- Save generate-weekly-pulse.mjs.
- You won't see output yet. The main() function in the final substep ties everything together and runs the pipeline.
Build the AI analysis and data assembly functions
Each video needs to be analyzed individually by AI. The OpenRouter API accepts the same format as OpenAI's chat completions endpoint, so you send a system prompt plus the video details and get structured JSON back.
- In generate-weekly-pulse.mjs, add the following code below the fetchTrendingVideos function:
async function analyzeVideoWithAI(video, apiKey) {
// Send the video details to Gemini 2.5 Flash via OpenRouter
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "google/gemini-2.5-flash",
messages: [
{
role: "system",
content:
"You are a business intelligence analyst. Given a YouTube video title and description, extract exactly 3 actionable business insights and write one LinkedIn post (150-200 words) that a business owner could post about this topic. Return ONLY valid JSON with this exact schema: { \"insights\": [\"string\", \"string\", \"string\"], \"linkedin_post\": \"string\" }",
},
{
role: "user",
content: `Video Title: ${video.title}\nChannel: ${video.channel}\nDescription: ${video.description}`,
},
],
temperature: 0.7,
}),
});
if (!response.ok) {
throw new Error(`OpenRouter API error: ${response.status}`);
}
const data = await response.json();
const content = data.choices[0].message.content;
// Strip markdown code fences if the model wraps its JSON response in them
const jsonStr = content.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
try {
return JSON.parse(jsonStr);
} catch {
// Fallback if the AI returns malformed JSON
return {
insights: [
"AI is transforming business operations",
"Automation reduces manual workload significantly",
"Early adopters gain competitive advantage",
],
linkedin_post:
"Exciting developments in AI and business automation. The tools available today would have cost thousands just a year ago. Now any small business can leverage them.",
};
}
}
What does this code do?
The analyzeVideoWithAI() function sends each video's title, channel, and description to Gemini 2.5 Flash through OpenRouter. The system prompt instructs the model to return exactly 3 business insights and 1 LinkedIn post as structured JSON.
The response parsing strips markdown code fences (which AI models sometimes add around JSON) and attempts to parse it. If parsing fails, a sensible fallback ensures your pipeline never crashes on a single bad response.
- Now add the getMonday() helper and the buildWeeklyPulse() function below analyzeVideoWithAI:
// Helper to get the Monday of the current week for labeling
function getMonday() {
const today = new Date();
const day = today.getDay();
const diff = today.getDate() - day + (day === 0 ? -6 : 1);
return new Date(today.setDate(diff));
}
async function buildWeeklyPulse(videos, analyses) {
// Combine each video with its AI analysis into a trend object
const trends = videos.map((video, index) => ({
rank: index + 1,
title: video.title,
channel: video.channel,
views: video.views,
url: video.url,
thumbnail: video.thumbnail,
insights: analyses[index].insights,
linkedin_post: analyses[index].linkedin_post,
}));
// Pick the top 5 LinkedIn posts for the dashboard's "Ready to Post" section
const topLinkedInPosts = trends.slice(0, 5).map((trend) => trend.linkedin_post);
return {
generated_at: new Date().toISOString(),
week_of: getMonday().toISOString().split("T")[0],
trends: trends,
total_videos_analyzed: videos.length,
top_linkedin_posts: topLinkedInPosts,
};
}
What does this code do?
The getMonday() helper calculates the Monday of the current week. This labels each weekly pulse so you know which week's data you're looking at.
The buildWeeklyPulse() function merges videos and their AI analyses into the final JSON structure. It ranks each video, attaches its insights and LinkedIn post, and selects the top 5 posts for the dashboard's "Ready to Post" section.
- Save generate-weekly-pulse.mjs.
- You won't see output yet. The next substep adds the main() function that runs the full pipeline.
Add the main function and run the pipeline
The script needs a main orchestrator that calls each function in sequence. It also needs a seed data fallback so the dashboard always has content to display, even when API keys aren't available (like during local development or CI builds).
- Add the generateSeedData() function below buildWeeklyPulse:
function generateSeedData() {
return {
generated_at: new Date().toISOString(),
week_of: getMonday().toISOString().split("T")[0],
trends: [
{
rank: 1,
title: "How AI Agents Are Replacing Entire Marketing Teams in 2025",
channel: "AI Automation Hub",
views: 245000,
url: "https://www.youtube.com/watch?v=example1",
thumbnail: "https://via.placeholder.com/480x360.png?text=AI+Agents",
insights: [
"AI agents can now handle content scheduling, audience analysis, and ad optimization without human intervention",
"Small businesses using AI marketing agents report 40% reduction in marketing costs",
"The key differentiator is not the AI itself but the workflow design connecting multiple agents",
],
linkedin_post:
"The marketing team of the future might be a team of one. AI agents are now handling content creation, scheduling, audience analysis, and ad optimization end-to-end. I have been testing this with three clients and the results are staggering: 40% cost reduction while maintaining output quality. The secret is not just picking the right AI tool. It is designing the workflow that connects multiple specialized agents into a cohesive system. Think of it like hiring specialists who never sleep and never forget your brand guidelines. If you are still doing marketing the old way, you are leaving money on the table. The barrier to entry has never been lower.",
},
{
rank: 2,
title: "Build a $10K/Month Automation Agency From Scratch",
channel: "The Automation Academy",
views: 189000,
url: "https://www.youtube.com/watch?v=example2",
thumbnail: "https://via.placeholder.com/480x360.png?text=Automation+Agency",
insights: [
"Automation agencies are charging $2K-5K per client for setups that take 2-3 hours to build",
"The highest-demand services are CRM automation, email sequences, and invoice processing",
"Client retention is highest when you include a monthly monitoring and optimization retainer",
],
linkedin_post:
"There is a new service business model emerging that most people are sleeping on: automation agencies. Here is the math that makes it compelling: clients pay $2K-5K for automations that take 2-3 hours to build. The highest-demand setups right now are CRM workflows, email sequences, and invoice processing. But the real revenue is in the retainer. Charge monthly for monitoring and optimization and your client lifetime value goes through the roof. I have seen solo operators hit $10K/month within 90 days by specializing in one vertical and one platform. The opportunity window is still wide open.",
},
{
rank: 3,
title: "Claude AI vs ChatGPT for Business: The Definitive Comparison",
channel: "Tech Business Today",
views: 156000,
url: "https://www.youtube.com/watch?v=example3",
thumbnail: "https://via.placeholder.com/480x360.png?text=Claude+vs+ChatGPT",
insights: [
"Claude excels at long-form analysis and following complex instructions while ChatGPT leads in creative generation",
"For business document processing, Claude handles 200K token contexts making it ideal for contract analysis",
"Cost per task varies significantly: batch processing with Claude can be 50% cheaper for analytical work",
],
linkedin_post:
"After running both Claude and ChatGPT across 50 real business tasks, here is what I found: they are not interchangeable. Claude dominates at long-form analysis, contract review, and following multi-step instructions. Its 200K token context window means you can feed it entire documents without chunking. ChatGPT still leads in creative generation and conversational tasks. The smart play for business owners: use both. Route analytical tasks to Claude and creative tasks to ChatGPT. The cost difference is significant too. Batch processing with Claude is roughly 50% cheaper for analytical workloads. Stop asking which one is better. Start asking which one is better for each specific task.",
},
{
rank: 4,
title: "No-Code AI Tools That Actually Work for Small Business",
channel: "SMB Tech Reviews",
views: 132000,
url: "https://www.youtube.com/watch?v=example4",
thumbnail: "https://via.placeholder.com/480x360.png?text=No-Code+AI",
insights: [
"No-code AI platforms have matured enough for production use in customer service and data entry",
"The ROI breakeven for most no-code AI implementations is under 30 days",
"Integration with existing tools like Google Sheets and Slack is the primary adoption driver",
],
linkedin_post:
"Forget the hype. Here are no-code AI tools that are actually generating ROI for small businesses right now. The common thread: they integrate with tools you already use. Google Sheets, Slack, your CRM. No migration needed. The breakeven point for most implementations is under 30 days. Customer service bots, data entry automation, and lead scoring are the three highest-impact starting points. You do not need a technical team. You do not need a massive budget. You need one afternoon and a willingness to experiment. The businesses that figure this out first in their niche will have an unfair advantage for years.",
},
{
rank: 5,
title: "The Real Cost of AI in Business: What Nobody Tells You",
channel: "Business Reality Check",
views: 98000,
url: "https://www.youtube.com/watch?v=example5",
thumbnail: "https://via.placeholder.com/480x360.png?text=AI+Costs",
insights: [
"Hidden AI costs include prompt engineering time, error correction, and integration maintenance",
"Most businesses overestimate AI capabilities and underestimate the human oversight still required",
"The most cost-effective approach is starting with one high-volume repetitive task rather than broad deployment",
],
linkedin_post:
"Everyone talks about AI saving money. Nobody talks about the hidden costs. Here is what I have learned after implementing AI across 12 businesses: Prompt engineering takes time. Error correction is ongoing. Integration maintenance is real. Most businesses overestimate what AI can do autonomously and underestimate the human oversight still needed. The winning strategy is not broad deployment. It is surgical precision. Pick your single highest-volume repetitive task. Automate that one thing completely. Measure the ROI. Then expand. Businesses that try to automate everything at once usually automate nothing well.",
},
],
total_videos_analyzed: 5,
top_linkedin_posts: [],
};
}
What does the seed data do?
The generateSeedData() function returns 5 realistic dummy trends with pre-written insights and LinkedIn posts. This ensures your dashboard always has content to display, even when API keys are unavailable (like during CI builds or first-time setup).
- Add the main() function and its error handler at the very end of the file, below generateSeedData:
async function main() {
console.log("Starting AI Business Weekly generation...");
// If API keys are missing, fall back to seed data
if (!YOUTUBE_API_KEY || !OPENROUTER_API_KEY) {
console.log("API keys not found. Generating seed data...");
const seedData = generateSeedData();
seedData.top_linkedin_posts = seedData.trends.slice(0, 5).map((t) => t.linkedin_post);
const outputPath = path.join(process.cwd(), "public", "data", "weekly-pulse.json");
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(seedData, null, 2));
console.log("Seed data saved to public/data/weekly-pulse.json");
return;
}
// Step 1: Fetch trending videos from YouTube
console.log("Fetching trending YouTube videos...");
const videos = await fetchTrendingVideos(YOUTUBE_API_KEY);
console.log(`Found ${videos.length} videos`);
// Step 2: Analyze each video with AI sequentially
console.log("Analyzing videos with AI...");
const analyses = [];
for (const video of videos) {
console.log(` Analyzing: ${video.title.substring(0, 50)}...`);
const analysis = await analyzeVideoWithAI(video, OPENROUTER_API_KEY);
analyses.push(analysis);
}
console.log("All videos analyzed");
// Step 3: Build the final JSON structure and write to disk
console.log("Building weekly pulse...");
const weeklyPulse = await buildWeeklyPulse(videos, analyses);
const outputPath = path.join(process.cwd(), "public", "data", "weekly-pulse.json");
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(weeklyPulse, null, 2));
console.log("Weekly pulse saved to public/data/weekly-pulse.json");
console.log(`${weeklyPulse.total_videos_analyzed} videos analyzed`);
console.log(`${weeklyPulse.top_linkedin_posts.length} LinkedIn posts ready`);
}
main().catch((error) => {
console.error("Error:", error.message);
process.exit(1);
});
What does this code do?
The main() function is the orchestrator. It first checks whether API keys exist. If they're missing, it writes seed data and exits early. Otherwise, it runs the full pipeline: fetch videos, analyze each one sequentially (with progress logging), assemble the final JSON, and write it to disk.
The main().catch() at the bottom ensures any unhandled error gets logged cleanly and the process exits with a non-zero code. This is important for GitHub Actions to detect failures.
- Save generate-weekly-pulse.mjs.
- Run the script by executing this command in your terminal:
node --env-file=.env.local scripts/generate-weekly-pulse.mjs
What does this command do?
The --env-file flag (available in Node.js 20.6+) loads your .env.local file as environment variables before running the script. This avoids needing to install a separate package like dotenv.
The script takes about 30-60 seconds to complete as it analyzes each video sequentially. You should see progress output like this:
Starting AI Business Weekly generation...
Fetching trending YouTube videos...
Found 10 videos
Analyzing videos with AI...
Analyzing: How AI Agents Are Replacing Entire Mar...
Analyzing: Build a $10K/Month Automation Agency F...
...
All videos analyzed
Building weekly pulse...
Weekly pulse saved to public/data/weekly-pulse.json
10 videos analyzed
5 LinkedIn posts ready
- Confirm the output file was created by running:
ls public/data/
- You should see weekly-pulse.json in the output.
Seeing an API error?
- If you see YouTube Search API error: 403, make sure the YouTube Data API v3 is enabled in your Google Cloud project (APIs and Services > Library).
- If you see OpenRouter API error: 401, double-check that your OPENROUTER_API_KEY in .env.local is correct and has available credits.
- If you see TypeError: fetch is not a function, your Node.js version is below 18. Run node --version and upgrade to 20.9 or higher.
Help me debug my generation script errors.
✔️ Awesome, I've got everything!
Great. Double-check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's the complete scripts/generate-weekly-pulse.mjs file for comparison:
import fs from "node:fs";
import path from "node:path";
const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY;
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY;
const SEARCH_QUERY = process.env.SEARCH_QUERY || "AI business automation tools 2025";
async function fetchTrendingVideos(apiKey) {
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
const publishedAfter = sevenDaysAgo.toISOString();
const searchParams = new URLSearchParams({
part: "snippet",
q: SEARCH_QUERY,
type: "video",
order: "viewCount",
publishedAfter: publishedAfter,
maxResults: "10",
key: apiKey,
});
const searchResponse = await fetch(
`https://www.googleapis.com/youtube/v3/search?${searchParams}`
);
if (!searchResponse.ok) {
throw new Error(`YouTube Search API error: ${searchResponse.status}`);
}
const searchData = await searchResponse.json();
const videoIds = searchData.items.map((item) => item.id.videoId).join(",");
const statsParams = new URLSearchParams({
part: "statistics",
id: videoIds,
key: apiKey,
});
const statsResponse = await fetch(
`https://www.googleapis.com/youtube/v3/videos?${statsParams}`
);
if (!statsResponse.ok) {
throw new Error(`YouTube Videos API error: ${statsResponse.status}`);
}
const statsData = await statsResponse.json();
const statsMap = new Map(statsData.items.map((item) => [item.id, item.statistics]));
return searchData.items.map((item) => ({
videoId: item.id.videoId,
title: item.snippet.title,
channel: item.snippet.channelTitle,
thumbnail: item.snippet.thumbnails.high.url,
url: `https://www.youtube.com/watch?v=${item.id.videoId}`,
views: parseInt(statsMap.get(item.id.videoId)?.viewCount || "0"),
description: item.snippet.description,
}));
}
async function analyzeVideoWithAI(video, apiKey) {
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "google/gemini-2.5-flash",
messages: [
{
role: "system",
content:
"You are a business intelligence analyst. Given a YouTube video title and description, extract exactly 3 actionable business insights and write one LinkedIn post (150-200 words) that a business owner could post about this topic. Return ONLY valid JSON with this exact schema: { \"insights\": [\"string\", \"string\", \"string\"], \"linkedin_post\": \"string\" }",
},
{
role: "user",
content: `Video Title: ${video.title}\nChannel: ${video.channel}\nDescription: ${video.description}`,
},
],
temperature: 0.7,
}),
});
if (!response.ok) {
throw new Error(`OpenRouter API error: ${response.status}`);
}
const data = await response.json();
const content = data.choices[0].message.content;
const jsonStr = content.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
try {
return JSON.parse(jsonStr);
} catch {
return {
insights: [
"AI is transforming business operations",
"Automation reduces manual workload significantly",
"Early adopters gain competitive advantage",
],
linkedin_post:
"Exciting developments in AI and business automation. The tools available today would have cost thousands just a year ago. Now any small business can leverage them.",
};
}
}
function getMonday() {
const today = new Date();
const day = today.getDay();
const diff = today.getDate() - day + (day === 0 ? -6 : 1);
return new Date(today.setDate(diff));
}
async function buildWeeklyPulse(videos, analyses) {
const trends = videos.map((video, index) => ({
rank: index + 1,
title: video.title,
channel: video.channel,
views: video.views,
url: video.url,
thumbnail: video.thumbnail,
insights: analyses[index].insights,
linkedin_post: analyses[index].linkedin_post,
}));
const topLinkedInPosts = trends.slice(0, 5).map((trend) => trend.linkedin_post);
return {
generated_at: new Date().toISOString(),
week_of: getMonday().toISOString().split("T")[0],
trends: trends,
total_videos_analyzed: videos.length,
top_linkedin_posts: topLinkedInPosts,
};
}
function generateSeedData() {
return {
generated_at: new Date().toISOString(),
week_of: getMonday().toISOString().split("T")[0],
trends: [
{
rank: 1,
title: "How AI Agents Are Replacing Entire Marketing Teams in 2025",
channel: "AI Automation Hub",
views: 245000,
url: "https://www.youtube.com/watch?v=example1",
thumbnail: "https://via.placeholder.com/480x360.png?text=AI+Agents",
insights: [
"AI agents can now handle content scheduling, audience analysis, and ad optimization without human intervention",
"Small businesses using AI marketing agents report 40% reduction in marketing costs",
"The key differentiator is not the AI itself but the workflow design connecting multiple agents",
],
linkedin_post:
"The marketing team of the future might be a team of one. AI agents are now handling content creation, scheduling, audience analysis, and ad optimization end-to-end. I have been testing this with three clients and the results are staggering: 40% cost reduction while maintaining output quality. The secret is not just picking the right AI tool. It is designing the workflow that connects multiple specialized agents into a cohesive system. Think of it like hiring specialists who never sleep and never forget your brand guidelines. If you are still doing marketing the old way, you are leaving money on the table. The barrier to entry has never been lower.",
},
{
rank: 2,
title: "Build a $10K/Month Automation Agency From Scratch",
channel: "The Automation Academy",
views: 189000,
url: "https://www.youtube.com/watch?v=example2",
thumbnail: "https://via.placeholder.com/480x360.png?text=Automation+Agency",
insights: [
"Automation agencies are charging $2K-5K per client for setups that take 2-3 hours to build",
"The highest-demand services are CRM automation, email sequences, and invoice processing",
"Client retention is highest when you include a monthly monitoring and optimization retainer",
],
linkedin_post:
"There is a new service business model emerging that most people are sleeping on: automation agencies. Here is the math that makes it compelling: clients pay $2K-5K for automations that take 2-3 hours to build. The highest-demand setups right now are CRM workflows, email sequences, and invoice processing. But the real revenue is in the retainer. Charge monthly for monitoring and optimization and your client lifetime value goes through the roof. I have seen solo operators hit $10K/month within 90 days by specializing in one vertical and one platform. The opportunity window is still wide open.",
},
{
rank: 3,
title: "Claude AI vs ChatGPT for Business: The Definitive Comparison",
channel: "Tech Business Today",
views: 156000,
url: "https://www.youtube.com/watch?v=example3",
thumbnail: "https://via.placeholder.com/480x360.png?text=Claude+vs+ChatGPT",
insights: [
"Claude excels at long-form analysis and following complex instructions while ChatGPT leads in creative generation",
"For business document processing, Claude handles 200K token contexts making it ideal for contract analysis",
"Cost per task varies significantly: batch processing with Claude can be 50% cheaper for analytical work",
],
linkedin_post:
"After running both Claude and ChatGPT across 50 real business tasks, here is what I found: they are not interchangeable. Claude dominates at long-form analysis, contract review, and following multi-step instructions. Its 200K token context window means you can feed it entire documents without chunking. ChatGPT still leads in creative generation and conversational tasks. The smart play for business owners: use both. Route analytical tasks to Claude and creative tasks to ChatGPT. The cost difference is significant too. Batch processing with Claude is roughly 50% cheaper for analytical workloads. Stop asking which one is better. Start asking which one is better for each specific task.",
},
{
rank: 4,
title: "No-Code AI Tools That Actually Work for Small Business",
channel: "SMB Tech Reviews",
views: 132000,
url: "https://www.youtube.com/watch?v=example4",
thumbnail: "https://via.placeholder.com/480x360.png?text=No-Code+AI",
insights: [
"No-code AI platforms have matured enough for production use in customer service and data entry",
"The ROI breakeven for most no-code AI implementations is under 30 days",
"Integration with existing tools like Google Sheets and Slack is the primary adoption driver",
],
linkedin_post:
"Forget the hype. Here are no-code AI tools that are actually generating ROI for small businesses right now. The common thread: they integrate with tools you already use. Google Sheets, Slack, your CRM. No migration needed. The breakeven point for most implementations is under 30 days. Customer service bots, data entry automation, and lead scoring are the three highest-impact starting points. You do not need a technical team. You do not need a massive budget. You need one afternoon and a willingness to experiment. The businesses that figure this out first in their niche will have an unfair advantage for years.",
},
{
rank: 5,
title: "The Real Cost of AI in Business: What Nobody Tells You",
channel: "Business Reality Check",
views: 98000,
url: "https://www.youtube.com/watch?v=example5",
thumbnail: "https://via.placeholder.com/480x360.png?text=AI+Costs",
insights: [
"Hidden AI costs include prompt engineering time, error correction, and integration maintenance",
"Most businesses overestimate AI capabilities and underestimate the human oversight still required",
"The most cost-effective approach is starting with one high-volume repetitive task rather than broad deployment",
],
linkedin_post:
"Everyone talks about AI saving money. Nobody talks about the hidden costs. Here is what I have learned after implementing AI across 12 businesses: Prompt engineering takes time. Error correction is ongoing. Integration maintenance is real. Most businesses overestimate what AI can do autonomously and underestimate the human oversight still needed. The winning strategy is not broad deployment. It is surgical precision. Pick your single highest-volume repetitive task. Automate that one thing completely. Measure the ROI. Then expand. Businesses that try to automate everything at once usually automate nothing well.",
},
],
total_videos_analyzed: 5,
top_linkedin_posts: [],
};
}
async function main() {
console.log("Starting AI Business Weekly generation...");
if (!YOUTUBE_API_KEY || !OPENROUTER_API_KEY) {
console.log("API keys not found. Generating seed data...");
const seedData = generateSeedData();
seedData.top_linkedin_posts = seedData.trends.slice(0, 5).map((t) => t.linkedin_post);
const outputPath = path.join(process.cwd(), "public", "data", "weekly-pulse.json");
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(seedData, null, 2));
console.log("Seed data saved to public/data/weekly-pulse.json");
return;
}
console.log("Fetching trending YouTube videos...");
const videos = await fetchTrendingVideos(YOUTUBE_API_KEY);
console.log(`Found ${videos.length} videos`);
console.log("Analyzing videos with AI...");
const analyses = [];
for (const video of videos) {
console.log(` Analyzing: ${video.title.substring(0, 50)}...`);
const analysis = await analyzeVideoWithAI(video, OPENROUTER_API_KEY);
analyses.push(analysis);
}
console.log("All videos analyzed");
console.log("Building weekly pulse...");
const weeklyPulse = await buildWeeklyPulse(videos, analyses);
const outputPath = path.join(process.cwd(), "public", "data", "weekly-pulse.json");
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(weeklyPulse, null, 2));
console.log("Weekly pulse saved to public/data/weekly-pulse.json");
console.log(`${weeklyPulse.total_videos_analyzed} videos analyzed`);
console.log(`${weeklyPulse.top_linkedin_posts.length} LinkedIn posts ready`);
}
main().catch((error) => {
console.error("Error:", error.message);
process.exit(1);
});
Your automation engine is complete. The JSON file sitting in public/data/ now contains real AI-generated business insights and LinkedIn posts. Next up, you'll build the dashboard that brings this data to life.
Build the Dashboard
Your automation script is generating rich data. You have trending videos, AI-powered business insights, and ready-to-post LinkedIn content all sitting in a JSON file.
Now you need a way to actually see and use that data. In this step, you will build a Next.js dashboard with trend cards, business insights, and copy-to-clipboard LinkedIn posts.
In this step, get ready to:
- Create reusable TrendCard and LinkedInPost components with Tailwind CSS dark theme styling.
- Build the main dashboard page as a Server Component that reads your JSON data.
- Add SEO metadata and a pulse animation to bring the dashboard to life.
Create the TrendCard and LinkedInPost components
The dashboard needs two reusable components. One displays a video's rank, thumbnail, and business insights. The other displays a LinkedIn post with a copy button.
- Create a new folder and file at app/components/TrendCard.tsx by right-clicking the app folder in the VS Code Explorer sidebar and selecting New File.
- Type components/TrendCard.tsx as the filename. VS Code creates the components folder automatically.
- Paste the following code into app/components/TrendCard.tsx:
"use client";
interface TrendCardProps {
trend: {
rank: number;
title: string;
channel: string;
views: number;
url: string;
thumbnail: string;
insights: string[];
};
}
export default function TrendCard({ trend }: TrendCardProps) {
return (
<div className="trend-card bg-[#111111] border border-[#222222] rounded-xl p-6 hover:border-blue-500/50 transition-colors">
<div className="flex gap-4">
{/* Large rank number on the left */}
<span className="text-4xl font-bold text-blue-500 shrink-0">
{trend.rank}
</span>
<div className="flex-1 min-w-0">
{/* Thumbnail and video details row */}
<div className="flex flex-col md:flex-row gap-4">
<img
src={trend.thumbnail}
alt={trend.title}
className="w-full md:w-48 h-28 object-cover rounded-lg"
/>
<div className="flex-1">
<h3 className="text-lg font-semibold text-white leading-tight mb-1">
{trend.title}
</h3>
<p className="text-sm text-gray-400 mb-1">{trend.channel}</p>
<p className="text-sm text-gray-500">
{trend.views.toLocaleString()} views
</p>
</div>
</div>
{/* Business insights list */}
<div className="mt-4">
<h4 className="text-sm font-medium text-blue-400 uppercase tracking-wide mb-2">
Business Insights
</h4>
<ul className="space-y-2">
{trend.insights.map((insight, index) => (
<li
key={index}
className="text-sm text-gray-300 pl-4 border-l-2 border-blue-500/30"
>
{insight}
</li>
))}
</ul>
</div>
{/* Link to original video */}
<a
href={trend.url}
target="_blank"
rel="noopener noreferrer"
className="no-print inline-block mt-4 text-sm text-blue-400 hover:text-blue-300 transition-colors"
>
Watch on YouTube
</a>
</div>
</div>
</div>
);
}
What does this component do?
TrendCard is a client component that receives a single trend object as a prop and renders it as a styled card.
- The large blue rank number on the left gives each video a clear position in the list.
- The insights.map() loop renders each of the 3 AI-generated business insights with a blue left border.
- The no-print class on the YouTube link hides it when printing (you will add the print CSS later).
- Save app/components/TrendCard.tsx.
- You will not see a change yet. The next component and the main page will use this.
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before moving on.
ⓧ I'd like to double check the full code
"use client";
interface TrendCardProps {
trend: {
rank: number;
title: string;
channel: string;
views: number;
url: string;
thumbnail: string;
insights: string[];
};
}
export default function TrendCard({ trend }: TrendCardProps) {
return (
<div className="trend-card bg-[#111111] border border-[#222222] rounded-xl p-6 hover:border-blue-500/50 transition-colors">
<div className="flex gap-4">
<span className="text-4xl font-bold text-blue-500 shrink-0">
{trend.rank}
</span>
<div className="flex-1 min-w-0">
<div className="flex flex-col md:flex-row gap-4">
<img
src={trend.thumbnail}
alt={trend.title}
className="w-full md:w-48 h-28 object-cover rounded-lg"
/>
<div className="flex-1">
<h3 className="text-lg font-semibold text-white leading-tight mb-1">
{trend.title}
</h3>
<p className="text-sm text-gray-400 mb-1">{trend.channel}</p>
<p className="text-sm text-gray-500">
{trend.views.toLocaleString()} views
</p>
</div>
</div>
<div className="mt-4">
<h4 className="text-sm font-medium text-blue-400 uppercase tracking-wide mb-2">
Business Insights
</h4>
<ul className="space-y-2">
{trend.insights.map((insight, index) => (
<li
key={index}
className="text-sm text-gray-300 pl-4 border-l-2 border-blue-500/30"
>
{insight}
</li>
))}
</ul>
</div>
<a
href={trend.url}
target="_blank"
rel="noopener noreferrer"
className="no-print inline-block mt-4 text-sm text-blue-400 hover:text-blue-300 transition-colors"
>
Watch on YouTube
</a>
</div>
</div>
</div>
);
}
- Create a new file at app/components/LinkedInPost.tsx by right-clicking the components folder and selecting New File.
- Paste the following code into app/components/LinkedInPost.tsx:
"use client";
import { useState } from "react";
interface LinkedInPostProps {
post: string;
rank: number;
}
export default function LinkedInPost({ post, rank }: LinkedInPostProps) {
// Track whether the text was just copied
const [copied, setCopied] = useState(false);
async function handleCopy() {
await navigator.clipboard.writeText(post);
setCopied(true);
// Reset after 2 seconds
setTimeout(() => setCopied(false), 2000);
}
return (
<div className="bg-[#111111] border border-[#222222] rounded-xl p-6">
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-medium text-blue-400 bg-blue-500/10 px-2 py-1 rounded">
Post #{rank}
</span>
<button
onClick={handleCopy}
className="no-print text-xs px-3 py-1 rounded bg-blue-600 hover:bg-blue-500 text-white transition-colors"
>
{copied ? "Copied!" : "Copy to Clipboard"}
</button>
</div>
<p className="text-sm text-gray-300 leading-relaxed whitespace-pre-line">
{post}
</p>
</div>
);
}
What does this component do?
LinkedInPost displays an AI-generated post with a one-click copy button.
- The useState hook tracks whether the text was just copied. When copied is true, the button text changes to "Copied!" for 2 seconds.
- The navigator.clipboard.writeText() API copies the post text to the user's clipboard without any extra libraries.
- The no-print class hides the copy button when printing (useful for the Secret Mission later).
- Save app/components/LinkedInPost.tsx.
- You will not see a change yet. The main page imports both components in the next substep.
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before moving on.
ⓧ I'd like to double check the full code
"use client";
import { useState } from "react";
interface LinkedInPostProps {
post: string;
rank: number;
}
export default function LinkedInPost({ post, rank }: LinkedInPostProps) {
const [copied, setCopied] = useState(false);
async function handleCopy() {
await navigator.clipboard.writeText(post);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
return (
<div className="bg-[#111111] border border-[#222222] rounded-xl p-6">
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-medium text-blue-400 bg-blue-500/10 px-2 py-1 rounded">
Post #{rank}
</span>
<button
onClick={handleCopy}
className="no-print text-xs px-3 py-1 rounded bg-blue-600 hover:bg-blue-500 text-white transition-colors"
>
{copied ? "Copied!" : "Copy to Clipboard"}
</button>
</div>
<p className="text-sm text-gray-300 leading-relaxed whitespace-pre-line">
{post}
</p>
</div>
);
}
Replace the default page with the dashboard layout
Next.js Server Components can read files directly from the filesystem at build time. Your page will read weekly-pulse.json using Node.js fs and pass the data to your components.
- Open app/page.tsx in VS Code.
- Select all existing content (Cmd+A on macOS or Ctrl+A on Windows) and replace it with the following:
import fs from "node:fs";
import path from "node:path";
import TrendCard from "./components/TrendCard";
import LinkedInPost from "./components/LinkedInPost";
// Define the shape of the weekly pulse JSON
interface WeeklyPulse {
generated_at: string;
week_of: string;
trends: {
rank: number;
title: string;
channel: string;
views: number;
url: string;
thumbnail: string;
insights: string[];
linkedin_post: string;
}[];
total_videos_analyzed: number;
top_linkedin_posts: string[];
}
// Read the JSON file at build time (Server Component)
function getData(): WeeklyPulse | null {
try {
const filePath = path.join(process.cwd(), "public", "data", "weekly-pulse.json");
const raw = fs.readFileSync(filePath, "utf-8");
return JSON.parse(raw);
} catch {
return null;
}
}
export default function Home() {
const data = getData();
// Show a fallback message if no data exists yet
if (!data) {
return (
<main className="flex items-center justify-center min-h-screen">
<p className="text-gray-400 text-lg">
No data yet. Run the generation script to populate the dashboard.
</p>
</main>
);
}
// Format the last-updated date for display
const lastUpdated = new Date(data.generated_at).toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
return (
<main className="max-w-5xl mx-auto px-4 py-12">
{/* Hero section with title and live badge */}
<header className="text-center mb-12">
<div className="flex items-center justify-center gap-3 mb-4">
<h1 className="text-4xl md:text-5xl font-bold tracking-tight">
AI Business Weekly
</h1>
<span className="no-print pulse-live inline-block w-3 h-3 bg-green-500 rounded-full" />
</div>
<p className="text-gray-400 text-lg">
Free business intelligence, updated every Monday
</p>
<p className="text-gray-500 text-sm mt-2">Last updated: {lastUpdated}</p>
</header>
{/* Trend cards section */}
<section className="mb-16">
<h2 className="text-2xl font-bold mb-6 text-white">
This Week's Trends
</h2>
<div className="space-y-6">
{data.trends.map((trend) => (
<TrendCard key={trend.rank} trend={trend} />
))}
</div>
</section>
{/* LinkedIn posts section */}
<section className="mb-16">
<h2 className="text-2xl font-bold mb-6 text-white">Ready to Post</h2>
<div className="grid gap-4 md:grid-cols-2">
{data.top_linkedin_posts.map((post, index) => (
<LinkedInPost key={index} post={post} rank={index + 1} />
))}
</div>
</section>
{/* Footer */}
<footer className="no-print text-center text-gray-500 text-sm border-t border-[#222222] pt-8">
<p>
Built with AI Business Weekly | Free Forever |{" "}
<a
href="https://github.com"
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300"
>
View on GitHub
</a>
</p>
</footer>
</main>
);
}
What does this page do?
This is a Server Component. It reads your JSON file using Node.js fs at build time, so no client-side fetching is needed.
- The getData() function reads public/data/weekly-pulse.json and returns null if the file does not exist yet.
- The hero section displays the title, a pulsing green "Live" badge, and the last-updated date formatted as a readable string.
- The trends section maps over each trend and renders a TrendCard. The posts section renders the top 5 LinkedIn posts as LinkedInPost components.
- Save app/page.tsx.
- Start the dev server by running this command in your terminal:
npm run dev
- Open http://localhost:3000 in your browser. You should see the "AI Business Weekly" title, trend cards with thumbnails and insights, and LinkedIn post cards with copy buttons.
Seeing a blank page or error?
- If you see "No data yet", confirm that public/data/weekly-pulse.json exists from Step 3. Rerun the generation script if needed.
- If you see a module-not-found error for ./components/TrendCard, double-check that both component files are saved inside app/components/ (not in the project root).
- If styles look broken, make sure the dev server is running and try a hard refresh (Cmd+Shift+R on macOS or Ctrl+Shift+R on Windows).
Still stuck? Help me debug why my Next.js dashboard is not rendering.
✔️ Awesome, I've got everything!
Great. Keep your dev server running for the next substep.
ⓧ I'd like to double check the full code
import fs from "node:fs";
import path from "node:path";
import TrendCard from "./components/TrendCard";
import LinkedInPost from "./components/LinkedInPost";
interface WeeklyPulse {
generated_at: string;
week_of: string;
trends: {
rank: number;
title: string;
channel: string;
views: number;
url: string;
thumbnail: string;
insights: string[];
linkedin_post: string;
}[];
total_videos_analyzed: number;
top_linkedin_posts: string[];
}
function getData(): WeeklyPulse | null {
try {
const filePath = path.join(process.cwd(), "public", "data", "weekly-pulse.json");
const raw = fs.readFileSync(filePath, "utf-8");
return JSON.parse(raw);
} catch {
return null;
}
}
export default function Home() {
const data = getData();
if (!data) {
return (
<main className="flex items-center justify-center min-h-screen">
<p className="text-gray-400 text-lg">
No data yet. Run the generation script to populate the dashboard.
</p>
</main>
);
}
const lastUpdated = new Date(data.generated_at).toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
return (
<main className="max-w-5xl mx-auto px-4 py-12">
<header className="text-center mb-12">
<div className="flex items-center justify-center gap-3 mb-4">
<h1 className="text-4xl md:text-5xl font-bold tracking-tight">
AI Business Weekly
</h1>
<span className="no-print pulse-live inline-block w-3 h-3 bg-green-500 rounded-full" />
</div>
<p className="text-gray-400 text-lg">
Free business intelligence, updated every Monday
</p>
<p className="text-gray-500 text-sm mt-2">Last updated: {lastUpdated}</p>
</header>
<section className="mb-16">
<h2 className="text-2xl font-bold mb-6 text-white">
This Week's Trends
</h2>
<div className="space-y-6">
{data.trends.map((trend) => (
<TrendCard key={trend.rank} trend={trend} />
))}
</div>
</section>
<section className="mb-16">
<h2 className="text-2xl font-bold mb-6 text-white">Ready to Post</h2>
<div className="grid gap-4 md:grid-cols-2">
{data.top_linkedin_posts.map((post, index) => (
<LinkedInPost key={index} post={post} rank={index + 1} />
))}
</div>
</section>
<footer className="no-print text-center text-gray-500 text-sm border-t border-[#222222] pt-8">
<p>
Built with AI Business Weekly | Free Forever |{" "}
<a
href="https://github.com"
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300"
>
View on GitHub
</a>
</p>
</footer>
</main>
);
}
Update the layout and global styles
The dashboard is rendering, but it needs proper SEO metadata and a pulsing animation for the "Live" badge. You will update two files to finish the look.
- Open app/layout.tsx in VS Code.
- Select all existing content and replace it with the following:
import type { Metadata } from "next";
import "./globals.css";
// SEO metadata for the dashboard
export const metadata: Metadata = {
title: "AI Business Weekly | Free SMB Intelligence",
description:
"A free, automated business intelligence dashboard. Fresh AI and business insights every Monday, with ready-to-post LinkedIn content.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className="bg-[#0a0a0a] text-white min-h-screen antialiased">
{children}
</body>
</html>
);
}
What changed here?
- The metadata export sets the browser tab title and the SEO description for search engines.
- The body class sets the dark background color (#0a0a0a), white text, and full-height layout for the entire app.
- Save app/layout.tsx.
- Check your browser tab. It should now read "AI Business Weekly | Free SMB Intelligence".
✔️ Awesome, I've got everything!
Great. One more file to go.
ⓧ I'd like to double check the full code
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "AI Business Weekly | Free SMB Intelligence",
description:
"A free, automated business intelligence dashboard. Fresh AI and business insights every Monday, with ready-to-post LinkedIn content.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className="bg-[#0a0a0a] text-white min-h-screen antialiased">
{children}
</body>
</html>
);
}
- Open app/globals.css in VS Code.
- Select all existing content and replace it with the following:
@import "tailwindcss";
/* Pulsing glow animation for the Live badge */
@keyframes pulse-glow {
0%,
100% {
opacity: 1;
box-shadow: 0 0 8px #22c55e;
}
50% {
opacity: 0.6;
box-shadow: 0 0 16px #22c55e;
}
}
.pulse-live {
animation: pulse-glow 2s ease-in-out infinite;
}
/* Print-specific styles for PDF export */
@media print {
body {
background: white !important;
color: black !important;
}
.no-print {
display: none !important;
}
.print-only {
display: block !important;
}
.trend-card {
break-inside: avoid;
border: 1px solid #ddd;
padding: 1rem;
margin-bottom: 1rem;
}
}
What do these styles do?
- The pulse-glow keyframe makes the green dot beside the title gently pulse, signalling the dashboard is live and auto-updating.
- The @media print block defines how the page looks when printed or saved as a PDF. Elements with the no-print class (like copy buttons and the YouTube link) are hidden.
- The print-only class is used to show elements exclusively in print mode. You will use this in the Secret Mission.
- Save app/globals.css.
- Switch back to your browser at http://localhost:3000. The green dot next to "AI Business Weekly" should now pulse with a soft glow.
- Click a Copy to Clipboard button on any LinkedIn post card. The button text should change to "Copied!" for 2 seconds, and the post text is now on your clipboard.
- Stop the dev server by pressing Ctrl+C in your terminal.
Green dot not pulsing?
- Confirm that app/globals.css starts with @import "tailwindcss"; on the very first line. Missing this import breaks all Tailwind utility classes.
- Check that the class name in your CSS (.pulse-live) matches the class in app/page.tsx (pulse-live). CSS class names are case-sensitive.
- Try a hard refresh (Cmd+Shift+R on macOS or Ctrl+Shift+R on Windows) to clear cached styles.
Still stuck? Help me debug why my CSS animation is not working in Next.js.
✔️ Awesome, I've got everything!
Great. Double check you have saved all files before moving on.
ⓧ I'd like to double check the full code
@import "tailwindcss";
@keyframes pulse-glow {
0%,
100% {
opacity: 1;
box-shadow: 0 0 8px #22c55e;
}
50% {
opacity: 0.6;
box-shadow: 0 0 16px #22c55e;
}
}
.pulse-live {
animation: pulse-glow 2s ease-in-out infinite;
}
@media print {
body {
background: white !important;
color: black !important;
}
.no-print {
display: none !important;
}
.print-only {
display: block !important;
}
.trend-card {
break-inside: avoid;
border: 1px solid #ddd;
padding: 1rem;
margin-bottom: 1rem;
}
}
Your dashboard is live and looking sharp. Next up, you will automate the entire pipeline with GitHub Actions so fresh content appears every Monday without lifting a finger.
Set Up GitHub Actions
Your dashboard works locally, but you still have to run the generation script by hand every time you want fresh data.
In this step, you will create a GitHub Actions workflow that runs your script automatically every Monday morning, commits the updated data, and pushes it to your repository. That push triggers a site rebuild so your live dashboard always has this week's content.
In this step, get ready to:
- Create a GitHub Actions workflow with a cron schedule and manual trigger.
- Push your project to GitHub and configure repository secrets.
- Test the workflow and verify a new commit appears automatically.
Create the workflow file with triggers and setup steps
A GitHub Actions workflow is a YAML file that lives in .github/workflows/. GitHub reads this file and runs the jobs you define whenever the specified triggers fire.
- In VS Code, create a new file called weekly-update.yml inside the .github/workflows/ folder.
- Paste this code into the file:
name: Weekly Business Pulse Update
on:
schedule:
- cron: "0 7 * * 1"
workflow_dispatch:
jobs:
generate-pulse:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: npm ci
What does this code do?
The on block defines two triggers. The schedule trigger uses a cron expression (0 7 * * 1) to run every Monday at 7:00 AM UTC. The workflow_dispatch trigger adds a manual "Run workflow" button in the Actions tab so you can trigger it on demand.
The three steps check out your code, install Node.js 20, and run npm ci to install your project's dependencies from the lock file.
- Keep weekly-update.yml open. You will add more steps to it next.
Add the script execution and commit steps
The workflow needs to actually run your generation script and then commit the freshly generated JSON back into the repository so your live site picks up the changes.
- In weekly-update.yml, find the line that says run: npm ci.
- Add the following code below it (make sure the indentation lines up with the other steps):
- name: Run generation script
env:
YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
SEARCH_QUERY: ${{ secrets.SEARCH_QUERY }}
run: node scripts/generate-weekly-pulse.mjs
- name: Commit and push updated data
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add public/data/weekly-pulse.json
git diff --staged --quiet || git commit -m "Weekly pulse update - $(date +'%Y-%m-%d')"
git push
What does this code do?
The "Run generation script" step passes your API keys as environment variables. These values come from GitHub Secrets, which you will configure in the next substep. The ${{ secrets.YOUTUBE_API_KEY }} syntax tells GitHub to inject the secret at runtime without exposing it in logs.
The "Commit and push" step configures a bot identity for git, stages the updated weekly-pulse.json, and only commits if the file actually changed (the git diff --staged --quiet || guard prevents empty commits). Finally it pushes to main, which triggers your hosting provider to rebuild.
- Save weekly-update.yml.
✔️ Awesome, I've got everything!
Great. Double check you have saved the file before moving on.
ⓧ I'd like to double check the full code
name: Weekly Business Pulse Update
on:
schedule:
- cron: "0 7 * * 1"
workflow_dispatch:
jobs:
generate-pulse:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: npm ci
- name: Run generation script
env:
YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
SEARCH_QUERY: ${{ secrets.SEARCH_QUERY }}
run: node scripts/generate-weekly-pulse.mjs
- name: Commit and push updated data
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add public/data/weekly-pulse.json
git diff --staged --quiet || git commit -m "Weekly pulse update - $(date +'%Y-%m-%d')"
git push
Seeing YAML indentation errors?
YAML is indentation-sensitive. Every - name: line under steps: must be indented with exactly 6 spaces (or matching your existing pattern). Mixing tabs and spaces will break the file.
Help me fix YAML indentation errors in my workflow file.
Push to GitHub and configure secrets
Your workflow file references GitHub Secrets that do not exist yet. You need to push your project to a GitHub repository first, then add those secrets in the repository settings.
- Go to github.com and click the + icon in the top-right corner.
- Click New repository.
- Name it ai-business-weekly and leave all initialization options unchecked (no README, no .gitignore, no license).
- Click Create repository.
Why leave initialization options unchecked?
You already have a local project with files committed. If GitHub initializes with a README or .gitignore, you will get a merge conflict on your first push. Starting empty avoids this.
- Back in your VS Code terminal, commit all your project files and push them to GitHub by running these commands:
git add .
git commit -m "Initial commit"
git remote add origin [[REPO_URL="https://github.com/your-username/ai-business-weekly.git"]]
git push -u origin main
What do these commands do?
- git add . stages all project files for commit.
- git commit -m "Initial commit" saves a snapshot of your project.
- git remote add origin connects your local repo to the GitHub repository you just created.
- git push -u origin main uploads everything to GitHub and sets main as the default branch to push to.
- Refresh your GitHub repository page. You should see all your project files listed.
Your .env.local is safe
Next.js projects created with create-next-app include a .gitignore that excludes .env*.local files. Your API keys were never committed or pushed.
Now add your secrets so the workflow can access them at runtime.
- In your GitHub repository, click Settings in the top navigation bar.
- In the left sidebar, expand Secrets and variables and click Actions.
- Click New repository secret.
- Set Name to YOUTUBE_API_KEY and paste your YouTube API key as the Secret value. Click Add secret.
- Repeat for OPENROUTER_API_KEY (paste your OpenRouter key).
- Repeat for SEARCH_QUERY (enter AI business automation tools 2025).
You should now see all three secrets listed on the Actions secrets page.
- Click the Actions tab at the top of your repository.
- Click Weekly Business Pulse Update in the left sidebar.
- Click the Run workflow dropdown on the right side, then click the green Run workflow button.
- Wait 1-2 minutes for the workflow to complete. Refresh the page until you see a green checkmark next to the run.
- Click the Code tab and check your commit history. You should see a new commit with the message "Weekly pulse update" followed by today's date.
Workflow failed?
Click the failed run to see the logs. Common issues:
- "Secret not found" or empty API responses: double-check that your secret names match exactly (YOUTUBE_API_KEY, OPENROUTER_API_KEY, SEARCH_QUERY).
- Permission errors on push: go to Settings then Actions then General, scroll to "Workflow permissions", and select Read and write permissions. Save and re-run.
- YAML syntax errors: check that all indentation uses spaces (not tabs) and that every env: key has a value.
Help me debug my GitHub Actions workflow failure.
Secret mission
Download Weekly Brief as PDF
Your dashboard is great on screen, but what about sharing a clean summary in a meeting or over email? In this bonus challenge, you'll add a "Download Weekly Brief" button that generates a professional, printable PDF of the week's trends and LinkedIn posts using the browser's built-in print dialog.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project costs essentially nothing to keep running. The YouTube API is free tier, OpenRouter usage is roughly $0.02 per weekly run from your existing credits, and GitHub Actions is free for public repos (2,000 minutes/month for private repos).
Resources you used:
- Google Cloud project ("AI Business Weekly") with YouTube Data API v3 enabled.
- GitHub Actions workflow (weekly-update.yml) running on a weekly cron schedule.
- GitHub repository secrets (YOUTUBE_API_KEY, OPENROUTER_API_KEY, SEARCH_QUERY).
- Local ai-business-weekly project folder on your Desktop.
Keep everything running
No action needed. Choose this if you want the dashboard to keep updating every Monday with fresh AI insights.
- Your GitHub Actions workflow runs automatically every Monday at 7am UTC.
- Each run costs roughly $0.02 in OpenRouter credits from your existing balance.
- The YouTube API stays within free tier limits (1 search call per week out of 100/day).
- Your dashboard updates itself with zero ongoing effort.
Pause - I'll come back to this later
Disable the automated workflow so it stops running. Your code and data stay intact for when you return.
- Go to your repository on GitHub and click the **Actions** tab.
- In the left sidebar, click **Weekly Business Pulse Update**.
- Click the dropdown menu (the three dots or ellipsis icon) and select **Disable workflow**.
- When you want to resume, return to the same page and click **Enable workflow**.
Delete - I don't want to use this again
Remove all project resources completely. The dashboard will still display its last generated data if the site remains deployed, but nothing new will be generated.
Delete the Google Cloud project:
- Go to the **IAM & Admin Settings** page in the Google Cloud Console for your "AI Business Weekly" project.
- Click **Shut down**.
- Enter the project ID to confirm, then click **Shut down anyway**.
- This revokes your YouTube API key and removes all associated resources.
Remove GitHub Secrets:
- Go to your repository on GitHub.
- Click **Settings**, then **Secrets and variables**, then **Actions**.
- Delete each of the three secrets: YOUTUBE_API_KEY, OPENROUTER_API_KEY, and SEARCH_QUERY.
Delete the local project folder:
- Run the following command for your operating system.
macOS
rm -rf ~/Desktop/ai-business-weekly
Windows
Remove-Item -Recurse -Force ~\Desktop\ai-business-weekly
That's a Wrap!
That's a Wrap!
Nice work. You just built a fully automated business intelligence system that fetches trending YouTube videos, analyzes them with AI, and displays actionable insights on a live public dashboard.
You've learned how to:
- Built an automation script that integrates the YouTube Data API v3 and OpenRouter (Gemini 2.5 Flash) into a single pipeline that fetches videos and generates AI-powered business insights.
- Built a public Next.js dashboard with trend cards, business insights, and copy-to-clipboard LinkedIn posts ready to share.
- Set up a GitHub Actions workflow with a cron schedule that automates the entire process every Monday, commits fresh data, and triggers a site rebuild.
- Secret Mission: Added a Download Weekly Brief feature that generates a printable PDF of the week's trends and LinkedIn posts.
Ready to quiz yourself?