Automate Your Daily News Digest Summarize with Gemini and Get Daily Email Updates¶
I wanted to read summary of news daily and built an automation with google gemini, google apps script, gmail. And it's free!
Our needs lead to solutions. As a software engineer I always observe the world to find a solution for our problems. So here is my problem; wanted to read the news daily, also improve my English without wasting a lot of time.
Clarifying the Problem¶
The news websites has RSS feature. We can get the news daily basis by utilizing rss pages. I like brainstorming with LLMs before directly trying to build solutions. Here is the first and short prompt I sent to the gemini
I want to build an automation that summarizes the news and emails me. I will be improving my English and also reading news as benefits. I want to use Google gemini, Gmail and python if needed. let's brainstorm
Gemini generated core components, approaches and recommendations. I mainly work with python programming language. It is easy to work with but at the same time it would need deployment. I thought to try without python, and used Google Apps Script before. Prompted again;
Is it possible to do with only gemini and Gmail? maybe also appsscript?
Received the expected answer! Yes, of course.
I'm not expert in Google Apps Script and did not know we can request to external urls. I requested core functions from Gemini.
summarizeWithGemini(text), getNewsSummaries(), extractTextFromHtml(html), sendDailyDigest() are the functions we will be used.
Getting API Key from Google AI Studio¶
To use google gemini service we need an api key. I visited https://aistudio.google.com/ and clicked "Get API Key" and after that clicked "Create API Key" and a pop up will appear with "Select a project from your existing Google Cloud projects, Search Google Cloud projects", you can select a project from your google cloud. If you don't have any project you can create by following https://console.cloud.google.com/projectcreate link.
After creation of your api key, copy it and make sure you store it securely.
Creating a Google Apps Script¶
Google has apps script that we can use javascript to automate some tasks. Such as accessing google docs, sheets and manupilating the data and sending mails with gmail.
Go to https://script.google.com/ and click "New project" button to create a new script.
We can define functions here and run them. Google Apps Script has some builtin classes to be used. Such as GmailApp, UrlFetchApp, XmlService, Utilities etc.
Defining the functions¶
summarizeWithGemini¶
- In following I use
gemini-2.0-flash
you can change the model by your need.
/**
* Summarizes the provided text using the Gemini API.
* @param {string} text The text of the article to summarize.
* @return {string} The summarized text.
*/
function summarizeWithGemini(text) {
const GEMINI_API_KEY = PropertiesService.getScriptProperties().getProperty("GEMINI_API_KEY");
const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`;
const prompt = `Summarize the following news article in a single paragraph of no more than 200 words. Focus on the main events and key facts. The article is: \n\n${text}`;
const requestBody = {
contents: [
{
parts: [{ text: prompt }],
},
],
};
const options = {
method: "post",
contentType: "application/json",
payload: JSON.stringify(requestBody),
};
try {
const response = UrlFetchApp.fetch(endpoint, options);
const data = JSON.parse(response.getContentText());
// Check if the response contains a candidate with parts
if (
data.candidates &&
data.candidates.length > 0 &&
data.candidates[0].content.parts.length > 0
) {
return data.candidates[0].content.parts[0].text;
} else {
console.error(
"Gemini API response did not contain a valid summary.",
data
);
return "Could not generate a summary.";
}
} catch (e) {
console.error("Error calling Gemini API: " + e.message);
return "Could not generate a summary due to an error.";
}
}
Storing Gemini API Key inside Properties¶
- We need to store the api key in properties.
- On scipts.google.com page in left side bar there is "Project settings". Click it to open the settings and in the end of the page there is "Script Properties". Click
Add Script Property
button. - There are 2 input box. Property name is GEMINI_API_KEY and the value is your api key(copy and paste it here.)
- Save script properties and go back to editor by clicking (left side bar you can see)
getNewsSummaries¶
MAX_NEWS_AMOUNT
is the number of news I want the ai summarize. This is important because AI api limits can be problem.SLEEP_SECONDS
is for delaying because AI api quota is 15 (request per minute)- You can change above values for your need.
- rssFeeds has news websites objects(with name and url property)
/**
* Fetches news from an RSS feed, summarizes each article, and returns the results.
* @return {Array<Object>} An array of objects, each containing an article's title, link, and summary.
*/
function getNewsSummaries() {
const rssFeeds = [
{ name: "Adnan Kaya", url: "https://www.example.com.tr/en/rss/"},
// add more news website rss
];
let summaries = [];
const MAX_NEWS_AMOUNT = 12; // you can change here
const SLEEP_SECONDS = 4000; // you can change here
for (const feed of rssFeeds) {
const rssUrl = feed.url;
const sourceName = feed.name;
try {
Logger.log(`------- Processing for ${rssUrl} -------`);
const response = UrlFetchApp.fetch(rssUrl);
if (response.getResponseCode() !== 200) {
throw new Error(
`Failed to fetch URL with status code: ${response.getResponseCode()}`
);
}
const xml = response.getContentText();
const document = XmlService.parse(xml);
const root = document.getRootElement();
if (!root) {
throw new Error("Root element is null. Invalid XML structure.");
}
let items = [];
let namespace = null;
let titleElement = "title";
let linkElement = "link";
const rootName = root.getName();
// Determine the feed format and adjust parsing logic
if (rootName === "rss") {
// RSS 2.0 format (most common)
namespace = root.getNamespace();
const channel = root.getChild("channel", namespace);
if (channel) {
items = channel.getChildren("item", namespace);
}
} else if (rootName === "feed") {
// Atom format
namespace = root.getNamespace();
items = root.getChildren("entry", namespace);
titleElement = "title";
linkElement = "link";
} else if (rootName === "RDF") {
// RSS 1.0 (RDF) format
const rss1Namespace = XmlService.getNamespace(
"http://purl.org/rss/1.0/"
);
items = root.getChildren("item", rss1Namespace);
// Set the namespace for title/link extraction to the RSS 1.0 namespace
namespace = rss1Namespace;
} else {
throw new Error(`Unsupported RSS feed format for URL: ${rssUrl}`);
}
if (items.length === 0) {
Logger.log(`No items found in feed: ${rssUrl}`);
continue;
}
// Limit to a few articles
for (let i = 0; i < Math.min(items.length, MAX_NEWS_AMOUNT); i++) {
try {
const item = items[i];
const title = item.getChildText(titleElement, namespace);
let link = item.getChildText(linkElement, namespace);
const pubDate =
item.getChildText("pubDate", namespace) ||
item.getChildText("updated", namespace);
// Handle Atom link format where link is an element with an attribute
if (!link && rootName === "feed") {
const linkElementNode = item.getChild(linkElement, namespace);
if (linkElementNode) {
link = linkElementNode.getAttribute("href")?.getValue();
}
}
if (!link) {
throw new Error("Article link is null or missing.");
}
const articleHtml = UrlFetchApp.fetch(link).getContentText();
const articleText = extractTextFromHtml(articleHtml);
if (!articleText || articleText.trim() === "") {
throw new Error("Extracted article text is empty.");
}
// Assuming summarizeWithGemini is a function you have defined elsewhere
const summary = summarizeWithGemini(articleText);
summaries.push({
title: title,
link: link,
summary: summary,
source: sourceName,
pubDate: pubDate,
});
// Add a sleep function to stay within the API quota
Utilities.sleep(SLEEP_SECONDS);
} catch (e) {
console.error(
`Error processing article from ${rssUrl}: ${e.message}`
);
continue;
}
}
} catch (e) {
console.error(`Error processing RSS feed ${rssUrl}: ${e.message}`);
}
}
return summaries;
}
extractTextFromHtml¶
/**
* A basic function to attempt to extract the main article text from HTML.
* This will likely need to be customized for specific websites.
* @param {string} html The HTML content of the webpage.
* @return {string} The extracted text.
*/
function extractTextFromHtml(html) {
// Use a regular expression to try and find the main article content within common tags
// This is a very rough approach but can work for some sites.
const regex = /<p[^>]*>(.*?)<\/p>/g;
let matches = [];
let match;
while ((match = regex.exec(html)) !== null) {
// Basic cleanup of HTML tags
matches.push(match[1].replace(/<[^>]*>/g, ""));
}
return matches.join("\n\n");
}
sendDailyDigest¶
- Change
recipient
andmailFrom
by replacing your mail addresses.
/**
* Fetches news summaries and sends them as an email digest.
*/
function sendDailyDigest() {
const summaries = getNewsSummaries();
const botName = "daily news bot;
const recipient = "to-adnankaya@example.com"; // change here!!!!
const mailFrom = "from-adnankaya@example.com"; // change here!!!!!
const subject = `Your Daily News Digest - ${new Date().toLocaleDateString()}`;
// Start building the HTML email body with a container table
let htmlEmailBody = `
<html>
<body style="font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 20px;">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center">
<table width="600" border="0" cellspacing="0" cellpadding="20" style="background-color: #ffffff; border-radius: 8px;">
<tr>
<td>
<h1 style="color: #333333; font-size: 24px;">Daily News Digest</h1>
<p style="color: #666666; font-size: 14px;">Here is your daily dose of news summaries:</p>
<hr style="border: 0; height: 1px; background: #ddd; margin: 20px 0;">
`;
if (summaries.length === 0) {
htmlEmailBody += `
<p style="color: #666666; font-style: italic;">No news articles were summarized today.</p>
`;
} else {
for (const item of summaries) {
let formattedDate = item.pubDate
? new Date(item.pubDate).toLocaleString()
: "";
htmlEmailBody += `
<table width="100%" border="0" cellspacing="0" cellpadding="0" style="margin-bottom: 20px;">
<tr>
<td>
<h3 style="color: #0056b3; font-size: 18px; margin-top: 0; margin-bottom: 5px;">${
item.title
}</h3>
<p style="color: #666666; font-size: 12px; margin-top: 0; margin-bottom: 10px;">
<span style="font-weight: bold;">
${
formattedDate
? ` | <span style="font-weight: bold;">Published:</span> ${formattedDate}`
: ""
}
</p>
<p style="color: #333333; font-size: 14px; margin-top: 0;">${item.summary.replace(
/\n/g,
"<br>"
)}</p>
<a href="${
item.link
}" style="color: #007BFF; font-size: 12px; text-decoration: none;">Read the full article here.</a>
</td>
</tr>
</table>
<p style="color: #ccc; font-size: 12px; margin-top: 25px; margin-bottom: 5px;">${
item.source
}</p>
<hr style="border: 0; height: 1px; background: #eee; margin: 20px 0;">
`;
}
}
// End the HTML body
htmlEmailBody += `
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`;
var options = {
name: botName,
from: mailFrom
htmlBody: htmlEmailBody,
charset: "UTF-8",
};
GmailApp.sendEmail(recipient, subject, "", options);
//Logger.log("Email sending attempt completed. Check your inbox and Apps Script Executions for status.");
}
Running the script manually¶
- After copying above functions and editing important parts, on the top of the editor page you will see Save, Run, Debug, dropdown(has our 4 function names)
- Make sure sendDailyDigest function is selected on the top in the dropdown
- If you click to Run a pop up will appear
Authorization required This app might not work as expected without providing all requested permissions.
- Click "Review Permissions" -> Choose your google account -> "Google hasn’t verified this app" will appear -> Click Advanced on the bottom left -> Go to Untitled project (unsafe) -> "Select what Untitled project can access" Select all -> Continue
- Why you get
"Google hasn’t verified this app" will appear
? Because you don't use paid google workspace. - If the script runs successfully you should get the email, check your inbox.
Adding Trrigger to Automate¶
- On left sidebar there is triggers Link button, click it.
- Click Add Trigger button on bottom right
- A pop up will appear.
- Choose which function to run : sendDailyDigest
- Choose which deployment should run: Head
- Select event source: Time-driven
- Select type of time based trigger: Day timer (You can choose another based on your need)
- Select time of day: 8pm to 9m (I selected)
- On right there is Failure notification settings: Notify me daily (I left as it is)
- scroll down and click
Save
Conclusion¶
This project is a perfect example of turning a daily need into a free, automated solution. By leveraging Gemini, Apps Script, and Gmail, I built something truly useful without spending any money. I've not only solved a personal problem but also picked up some practical skills in APIs and scripting along the way.
The coolest part is how easy it is to make this your own. You're not stuck with what's here; you can—and should—tweak it. Want more news? Just add more RSS feeds. Want a different kind of summary? Play around with the prompt you send to Gemini. The whole setup is a great starting point for more complex automations.