A Complete Guide To Building a Bookmarklet App in Javascript

How to design and build a cross-browser bookmarklet app with a settings screen that injects itself into webpages.

Book·mark·let

is a bookmark stored in a web browser that contains JavaScript commands that add one-click functions to enhance or extend the functionality of a browser or web page. The bookmarklet is run by loading the browser bookmark normally. Wikipedia

I wanted to be able to export GPX file from Strava.com, which is a network for athletes to upload and share their workout data. What I was mostly interested in was the GPS location data.

Unfortunately exporting other Strava.com user's GPX files is a paid feature.

Paywalled!

Paywalled!

So I built my own export tool

Get it!

Existing work and the Strava API

There are browser extensions available for most modern browsers out there. What I wanted was a single simple solution that worked in all browsers. Extensions weren't an option as I didn't want to be stuck with maintaining multiple versions of the same code for each browser and potentially different browser versions. Extensions also have the downside that they require a specific install/setup step, are managed in a separate subsystem in the browsers and can have more extensive permissions than what you can do on a website.

Bookmarklets have none of these issues.

Strava API restrictions

Strava.com offers a really nice public API for their service. But after going through its terms and condition I noticed that they've made it very clear that:

Strava reserves the right to cancel or limit any uses of the API by you [...] that replicate the Strava sites, services or products. link

They've even applied this clause on other websites that provided this GPX export feature in the past. Okay, so I couldn't use their API then.

After digging around their website with the browser developer tab open I noticed that to render the activity page they utilize XHR Javascript calls that bring back exactly the GPS data that I am interested in.

The public XHR requests made by the strava.com/activities/ pages

The public XHR requests made by the strava.com/activities/ pages

So if I build a piece of script that only executes on data that the current user has already downloaded and viewed through their webpage, data that they already have full access to, I should be free and clear from violating their T&C.

So, a Bookmarklet it is then...

GPX Configuration

Another design consideration was that the current GPX export on Strava.com doesn't give you any configuration choice of what route data to export. This can be troublesome as many GPS applications and devices only implement a portion of the GPX Schema and are very sensitive to unexpected elements (even though they're specified in the official schema docs, e.g. Garmin Bootcamp). Due to this I wanted to be able to offer the user some choice over the level of data exported.

How Bookmarklets work

How to install the Strava to GPX bookmarklet

How to install the Strava to GPX bookmarklet

A bookmarklet is executed within the context of the currently displayed page in the browser. This means that they have the normal script permissions that this website has and cannot do anything else.

If you have javascript disabled then this also effectively disables bookmarklets

Their code is stored in the URL section of the bookmark and start with the protocol prefix javascript: which instructs the browser to execute the code that follows as a Javascript code. This code is executed within the context of the currently loaded URL and has access to the entire DOM of that page. The code cannot be very long as the text that fits into a bookmarklet is very short, no more than a couple thousand characters normally. Wikipedia has more about bookmarklets.

The HTTP/1.1 protocol specification suggests that the request-line length (URL) should be 8000 octets. But the reality is that if you want to target the most popular web browsers you should not have URLs of more than 2,000 characters. More info.

How to structure the code

I knew that my bookmarklet code would probably be very close to this limit so I needed a way to have the bookmark code include and call into an external js file that contains my actual code.

Another consideration was that I wanted to regularly update and provide bug fixes for my code. Currently there is no way for a bookmarklet to download an updated version of its URL portion. Meaning once a user installs my bookmarklet the code in the URL is locked and cannot be updated or fixed anymore without the user re-installing it. Therefore I had to minimize the amount of code inside the bookmark URL to as little as possible and treat whatever I put in there as un-patchable.

My design is therefore as follows

The design of the bookmarklet app

The design of the bookmarklet app

I ended up having the bookmarklet loading jQuery as well and decided that in the interest of versioning that I would host its code as well.

The bookmarklet code itself contains one public entry function getGpx() which is called after all the supporting code has been loaded. The code is as follows:

javascript: (function() {
    functioncallback() {
        (function($) {
            varjQuery = $;
            functioncallback() {
                getGpx()
            }
            vars = document.createElement("script");
            s.src = "https://mapstogpx.com/strava/mapstogpxstrava.js";
            if (s.addEventListener) {
                s.addEventListener("load",
                    callback,
                    false)
            }
            elseif(s.readyState) {
                s.onreadystatechange = callback
            }
            document.body.appendChild(s);
        })(jQuery.noConflict(true))
    }
    vars = document.createElement("script");
    s.src = "https://mapstogpx.com/strava/jquery.min.js";
    if (s.addEventListener) {
        s.addEventListener("load",
            callback,
            false)
    }
    elseif(s.readyState) {
        s.onreadystatechange = callback
    }
    document.body.appendChild(s);
})()

To wrap all this code up for a bookmark I used Peter Coles' excellent Bookmarklet creator tool.

Server Caching

To get around browsers extensively caching my Javascript and CSS source files I place the following code at the end of the .htaccess on my Apachie web server. It blocks the existing chaching and expiration rules I have set up for content served from my server and basically tells the client that the files should never be cached locally.

# Don't want to cache the bookmarklet core code files at all!
<FilesMatch "(mapstogpxstrava.js|mapstogpxstravapopup.css)$">
    FileEtag None
    <ifModule mod_headers.c>
        Header Unset ETag
        Header Set Cache-Control "max-age=0, no-store, no-cache, must-revalidate"
        Header Set Pragma "no-cache"
        Header Set Expires "Thu, 1 Jan 1970 00:00:00 GMT"
    </ifModule>
</FilesMatch>

While my bookmarklet is still in active development I like to stick with this aggressive no-cache rules. However as soon as the code has stabilised I will consider relaxing this and allowing for some period of client caching.

The Javascript

I decided to merge all other dependant Javascript libraries into the main mapstogpxstrava.js Javascript file. This was to reduce the complexities on asynchonously loading dependant code and to simplify versioning and debugging as everything is served in the same file in the correct order.

The code in the file is not terribly complicated or particularly interesting apart from where I inject and then show a custom HTML element that I use to allow the user to customize the GPX output file that is generated.

To achieve this the script had to inject a CSS file into the document and then inject the HTML for the configuration dialog also directly into the strava.com HTML code.

// First add the stylesheet to the head of the document
var c =document.createElement("link");
c.rel = "stylesheet";   
c.media = "all"; 
c.href = "https://mapstogpx.com/strava/mapstogpxstravapopup.css"; 
c.type = "text/css";
document.getElementsByTagName("head")[0].appendChild(c);

// Now store and inject the html
var html = '';
html += '  <div class="mapstogpxstrava_popup-content">';
html += '    <div class="mapstogpxstrava_popup-text">';
// snip boring HTML code, the whole dialog can be
// viewed here: http://codepen.io/sverrirs/pen/JKAXKR
html += '    </div>';
html += '  </div>';

// Create the root div element for the popup
var h = document.createElement("div");
h.id = "mapstogpxstrava_popup"; 
h.class="mapstogpxstrava_popup";
h.innerHTML = html;
document.body.appendChild(h);   
The configuration window for the GPX output

The configuration window for the GPX output

CSS classes

When using CSS styling for the HTML elements in my configuration window I discovered that it was important to use the ID of the HTML element rather than a Class name. I ran into issues with form elements being overwritten by local website CSS rules unless I used the ID of the main div container in my CSS rules.

Meaning, this does not work:

.mapstogpxstrava_popup .input[type=text] {
  color: #444;
  padding: 0px;
}

but referencing the popup by its ID works flawlessly:

#mapstogpxstrava_popup .input[type=text] {
  color: #444;
  padding: 0px;
}

I don't have a clear understanding why this problem occurs in CSS but I would welcome any input from you in the comment section if you do.

Versioning

I decided early on that there would only be a single version of this bookmarklet in production at each time. I tried to structure the code that goes into the bookmark on the client end to be as thin as possible and only reference objects completely under my control.

I however can easily debug or have staging environments by privately deploy a complete new instance of the main js file and simply give it a slightly different name, e.g. https://mapstogpx.com/strava/mapstogpxstrava_v2.3.js . Then I can debug using a private bookmarklet link.

Conflicts in page

When injecting code into other webpages conflicts are bound to happen, be it style-sheets, javascript libraries, image assets or other resource files.

I've tried to prefix my assets with a namespace that should be unique to this tool. But there is nothing really stopping this conflicting with existing strava.com logic in the future in case they change something

Try the bookmarklet



Software Developer
For hire


Developer & Programmer with +15 years professional experience building software.


Seeking WFH, remoting or freelance opportunities.