Check how Google is changing the appearance of your title tags

Check your Google page titles for all of your search queries, across different countries and languages. 600 requests per month, for free. πŸš€

Update: This script now includes a meta description checker + timestamp (thanks to Jose Luis Hernando).

Check out the GIF to see it in action πŸŽ₯

Or get the previous template that only includes page titles here.


How to add the Script to Google Sheets

1. Sign up to RapidAPI

Sign up to RapidAPI, an API marketplace, at https://rapidapi.com/auth/sign-up (you can even use your Google Account).


2. Subscribe to the Google Search API

Once you’re all set up on RapidAPI head to https://rapidapi.com/apigeek/api/google-search3/pricing and subscribe to the Basic Plan (Free). This plan has a hard 600 requests per month limit.


3. Get your API Key

The good thing about RapidAPI is that your API key can be used universally across any of the APIs in their marketplace. To copy your API key, go to https://rapidapi.com/developer/dashboard and head down to ‘My Apps’ and then ‘Security’. From there, you can copy your Application Key.


4. Copy the script below:

The script below also includes a cool script from https://script.gs/visually-display-status-when-looping-through-google-sheets-data/ that visually displays the status of each fetch as it’s called. πŸ™Œ

function onOpen() {
	const ui = SpreadsheetApp.getUi();
	ui.createMenu('Title Checker')
		.addItem('Check Titles', 'titlechecker')
		.addToUi();
}

function titlechecker() {

  // Add Moment.js library
  eval(UrlFetchApp.fetch('https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js').getContentText());  

	// add API key from https://rapidapi.com/apigeek/api/google-search3
	const apiKey = "";

	// get active spreadsheet
	const ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();

	// function to get last row of single column
	const getLastRowCol = (range) => {
		let rowNum = 0;
		let blank = false;
		for (let row = 0; row < range.length; row++) {

			if (range[row][0] === "" && !blank) {
				rowNum = row;
				blank = true;

			} else if (range[row][0] !== "") {
				blank = false;
			};
		};
		return rowNum;
	}

	// range variables to make sure data can always be appended
	const titleRange = ss.getRange('B:B').getValues();
	const titleLastRow = getLastRowCol(titleRange) + 1;
	const lastRowDiff = ss.getLastRow() - titleLastRow + 1;
	const queryRange = ss.getRange(titleLastRow, 1, lastRowDiff).getValues();

	// domain and language variables
	const domain = ss.getRange('J2').getValue().replace(/^(?:https?:\/\/)?(?:www\.)?/i, "").split('/')[0];
	const country = ss.getRange('K2').getValue();
	const language = ss.getRange('L2').getValue();

	// function to fetch SERPs from rapidapi
	const fetchSerps = (keyword, language, country) => {
		try {
			const options = {
				'method': 'GET',
				'contentType': 'application/json',
				'headers': {
					'x-rapidapi-key': apiKey,
					'x-rapidapi-host': 'google-search3.p.rapidapi.com'
				}
			};

			const serpResponse = UrlFetchApp.fetch(`https://google-search3.p.rapidapi.com/api/v1/search/q=${keyword}&gl=${country}&hl=lang_${language}&num=100`, options);

			const content = JSON.parse(serpResponse.getContentText());

			const organicResults = content.results;

			let row = [],
				data;
			for (i = 0; i < organicResults.length; i++) {

				let data = organicResults[i];

				const link = data.link;

				// if any of the top 100 ranking URLs include the domain, return data
				if (link.includes(domain)) {

					// scrape the ranking URL
					const urlResponse = UrlFetchApp.fetch(data.link).getContentText();
    
					const $ = Cheerio.load(urlResponse);

					// extract the title
					const title = $('title').first().text().trim();
          const description = $('meta[name="description"]').attr('content').trim();

					// check whether ranking title is different to page title
					const changed = title !== data.title ? "Changed" : "Unchanged";
          const changed2 = description !== data.description? "Changed" : "Unchanged";

					row.push(title, data.title, changed, description,data.description,changed2);

					return row;

				}
			}


		} catch (e) {
			return false;
		}
	}

	// loop over remaining URLs and set values while the loop is running
	queryRange.forEach(function(row, i) {
		row.forEach(function(col) {

			// visually display a fetch status. Script inspired by https://script.gs visually-display-status-when-looping-through-google-sheets-data/
			ss.getRange(titleLastRow + i, 9).setValue("Loading...");

			SpreadsheetApp.flush();

			const check = fetchSerps(col, language, country);

			// if the SERPs check is successful, return row 
			check ? ss.getRange(titleLastRow + i, 2, 1, 6).setValues([check]) : ss.getRange(titleLastRow + i, 2).setValue("No data");
			ss.getRange(titleLastRow + i, 9).setValue("Done");

            // Add timestamp of extraction
      ss.getRange(titleLastRow + i, 8).setValue(moment().format('llll'))

			SpreadsheetApp.flush();

		});
	});

}

5. Head over to Google Sheets

Or if you’re really smart, create a new sheet by going to: https://sheets.new

Select Script editor from the Tools menu.

Paste the script and replace line 14 with your API key:

  const apiKey = 'addapikeyhere';

Save it. You’ll also need to run the function in Apps Script first to authorise the script.


6. Add the Cheerio Library in Apps Script

Search for the Cheerio Library using the ID below. The Cheerio Library makes it easier to parse the HTML from the requested page.

1ReeQ6WO8kKNxoaA_O0XEQ589cIrRvEBA9qcWpNqdOP17i47u6N9M5Xh0

If you’re not sure how to add a library in Apps Script, follow the gif below:

7. Run the script by heading to the ‘Title Checker’ menu and selecting ‘Check Titles’

To run the script, it’s easier if you copy a version of the Google Sheets template at the start of this guide. If you do want to build your own template, you’ll need to make sure:

1. You have 9 headings: Query (A1), Page Title (B1), Google Search Title (C1), Changed? (D1), Meta Description (E1), Google Description (F1), Changed? (G1), Timestamp (H1), Status (I1).

2. You add all of your search queries in Column A, beneath the Query heading.

3. You add inputs for Domain (J2), Country (K2), and Language (L2). The template at the beginning of this guide has a drop-down menu to select all countries and languages.

See the gif at the beginning of guide for more detail.


Thanks for stopping by πŸ‘‹

I’m Andrew Charlton, the Google Sheets nerd behind Keywords in Sheets. πŸ€“

Questions? Get in touch with me on social or comment below πŸ‘‡


More Scripts

Submit your response

Your email address will not be published. Required fields are marked *

7 Responses on this post

  1. Very cool! What might be a nice addition, is to add URL’s and have them checked?

    Cheers,
    Vincent

  2. For some reason the sheet is only in ‘View’ access for me — any advice? I can’t use it if it is in view.

    1. All my sheets are ‘view’ access, so I can have a default template. You’ll just need to save a copy of the sheet and voila!

  3. Hi, very nice tool but I have a great majority of “no data” and very few results. Is there any chance you see where it could come from?

    Cheers πŸ™‚

    1. Thank you. You’ll get “no data” only for a few reasons:

      1) API Key has been input incorrectly
      2) You haven’t subscribed to the free plan of the search API
      3) The domain prohibits scraping

      Hope that helps! πŸ™‚

      1. Oh yes, that definitely helps! Is there any way to change the UA of the scraper ?

        Thank you πŸ™‚