April 18, 2024Profile photoTony Masek

Writing simple Chrome extension

Recently, Google removed clickable map links from search results in the EU. Let's see how we can write a Chrome extension to fix it.

So recently, Google removed clickable map links from search results in the EU. Over time (as short as it was) I got so annoyed by it, that I decided I had to fix it by writing an extension. Only when I was finished did I notice, that someone beat me to it so if you are just after the extension to fix this issue you can go and install their extension here.

However, since I already wrote it I decided to share how to do it for anyone who wants to try to write their extension.

Creating new project

Writing a basic extension like this is pretty straightforward. Create a folder to house your files and in total, we will need just two: manifest.json and content.js. The manifest.json contains information about our extension and the content.js contains the logic that should be executed.

Manifest.json

I will start by showing you the entire manifest.json file and then we will take a look at it in greater detail.

{
  "manifest_version": 3,
  "version": "1.0.0",
  "name": "Return Google Map Links",
  "description": "This extension restores the functionality of clickable map results within Google for users located in the European Union.",
  "content_scripts": [
    {
      "matches": [
        "*://www.google.com/search*",
        "*://google.com/search*"
      ],
      "js": [
        "content.js"
      ]
    }
  ]
}

By manifest version, we are specifying the version of the manifest we are using (duh). Next, we need to specify the version of the extension. We will start with 1.0.0 and in case of any change or fix we can update the version accordingly. The name and description are self-explanatory.

The interesting part is content_scripts. Here we can specify which JavaScript to load every time a page, that matches a URL pattern is opened. So if we quickly inspect Google search results we can see, that for us the URL is https://www.google.com/search. The * at the beginning and the end is a wildcard, that matches anything. So we will match both http as well as https protocols and anything that comes after /search. We also added one pattern with www prefix. If any of these patterns match the opened URL we will load scripts inside js array. In our case, it is content.js.

Testing the extension

Before we start to code the extension let's test whether it works. Chrome enables us to load our extension manually for development purposes. Let's start by adding the following line console.log("It works!") to our content.js file. Next, open Google Chrome go to the Manage Extensions page, turn on Developer mode in the top right corner and finally click the Load unpacked button in the top left corner. Now find the folder in which you have your project and select it.

This should load the extension and if you open up your developer console and search for something in Google you should see It works! printed out in the console. Hurray, we have a working extension.

One thing to keep in mind is, that whenever you make changes to your extension you need to go to the Manage extensions tab and reload it using the arrow button.

The script

There are a lot of ways to make this work, but let's try to keep it simple. If we search for a few places we can see, that we have two possibilities where the map is located. Either a large rectangle above the results or in the case of a business it is a small rectangle in the top right corner of the page. So let's inspect the source.

Regular place

Let's search for O'Brien Street Sydney. We get the large rectangle which is a div with an id of lu_map. To get the query we will need to use for opening the map we can use for example this selector b > div. This returns the div containing the address below the map.

Business

Now let's search for Makuto Bondi Beach. We get the small rectangle which is an img tag id of lu_map. To get the URL we can use this selector div[data-place] which has a data-place attribute which contains the necessary URL.

Putting it together

Now that we know all the necessary information all we need to do is to check whether there is a map on the search results page and if so insert <a> with the corresponding href. I will paste the entire code below:

(async () => {
    const mapElement = document.getElementById('lu_map');
    if (!mapElement) return;

    if (isAlreadyClickable(mapElement)) return;
    const isBusiness = mapElement.tagName.toLowerCase() === 'img'

    getHref(isBusiness ? 300 : 0).then(href => {
        createLink(isBusiness, mapElement, href)
    })

    // In case the user is outisde the EU we don't need to make it clickable
    function isAlreadyClickable(mapElement) {
        return mapElement.parentElement.getAttribute('href')?.startsWith('/maps/place/')
    }

    function getHref(delay) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                const dataPlace = document.querySelector('[data-place]');
                if (dataPlace) {
                    resolve('https://google.com' + dataPlace.getAttribute('data-place'));
                    return
                }

                const query = document.querySelector('b > div')
                    ?.innerText
                    ?.replace("\n", " ")

                if (query) {
                    resolve('https://google.com/maps/place/' + encodeURIComponent(query))
                    return
                }

                reject()
            }, delay)
        })
    }

    function createLink(isBusiness, element, href) {
        if (!href) return;

        if (isBusiness) {
            // The img tag for business has a parent which already is <a> tag.
            // So we will just set the `href` attribute
            element.parentElement.setAttribute('href', href)
            return
        }

        const anchorElement = document.createElement('a');
        anchorElement.innerHTML = element.innerHTML;
        anchorElement.href = href;

        element.parentNode.replaceChild(anchorElement, element);
    }
})()

Summary

So there you have it - the working MVP of such an extension. It is obviously far from perfect and could use a lot of attention, but this was the proof of concept I quickly put together before I noticed another extension already exists.

I hope this was helpful and if you want to know more, the best place to go is the official docs.

Until next time,
Tony.