Skip to content

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 and mailFrom 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.