Juha-Matti Santala
Community Builder. Dreamer. Adventurer.

My most used bookmarklets

Last week I shared my love for Firefox’s Keyword search feature. This week, I want to expand that love to bookmarklets.

Jeremy Keith wrote about his collection of useful bookmarklets for testing websites. That inspired me to take a look at my bookmarklets and how I use the technology. I love how they are kind of like lightweight extensions without the need to submit them to any markets, work the same on all browsers and are quicker to develop.

They are also often very personal: it requires intention to click it to activate so I can rely on myself only running them in the right context and don’t need to write defensive code (like error handling), making the bookmarklets simpler.

Caio Rordrigues’ Bookmarklet Maker makes creating these bookmarklets a breeze.

I tend to save my bookmarklets in full Javascript form in my GitHub, Gists or Obsidian notes so I can adjust them without needing to reverse engineer minified bookmarklet code.

Copy links in different formats

I copy links to be stored or used in other places all the time. I keep my digital notes in Markdown format, using Obsidian and I want my links there to be more descriptive instead of just pure URLs that are often not very human-friendly.

So when I read Joona’s blog post about his bookmarklet that copies the title and URL from the website ready for his notes, it was like Christmas to me. I modified it slightly and now I have two versions of it in my bookmarks toolbar.

First one copies it in Markdown format as [Site title](site url) so I can use it in my notes and another one that copies it in HTML format <a href="site url">Site title<a> that I can use on my website(s).

When I first shared this in Mastodon, Timo mentioned his similar approach that also enables highlighting text to have it included in blockquote. I have implemented that in my Markdown version.

Markdown version

(() => {
  const copy = (title, url, selection) => {
    const textArea = document.createElement("textarea");
    let text = "";
    if (selection && selection != "") {
      text = `> ${selection}\n`;
    }
    text += `[${title}](${url})`;
    textArea.value = text;
    textArea.style.top = "0";
    textArea.style.left = "0";
    textArea.style.position = "fixed";
    document.body.appendChild(textArea);
    textArea.focus();
    textArea.select();
    try {
      document.execCommand("copy");
    } catch (err) {
      console.error("Copying err", err);
    }

    document.body.removeChild(textArea);
  };

  copy(document.title, window.location.href, window.getSelection());
})();

HTML version

(() => {
  const copy = (title, url) => {
    const textArea = document.createElement("textarea");
    let text = `<a href="${url}">${title}</a>`;
    textArea.value = text;
    textArea.style.top = "0";
    textArea.style.left = "0";
    textArea.style.position = "fixed";
    document.body.appendChild(textArea);
    textArea.focus();
    textArea.select();
    try {
      document.execCommand("copy");
    } catch (err) {
      console.error("Copying err", err);
    }

    document.body.removeChild(textArea);
  };

  copy(document.title, window.location.href);
})();

Liiga.fi highlighter

The Finnish hockey league Liiga has a website that has a lot of stuff and I keep heavily adjusting it with custom CSS and a custom Javascript functionality provided by this bookmarklet to keep it more usable.

I use custom CSS to hide everything else from the main site than schedule / current games on the left and the standings on the right. With a bookmarklet, I add a functionality where hovering over a game on the schedule highlights the teams in the standings.

This bookmarklet breaks about all the time because the underlying HTML keeps changing. The standings table is implemented as a ton of spans instead of actual table so I’m constantly adjusting the code.

/**
 * A bookmarklet to highlight teams in standings in Liiga.fi
 * You can use https://caiorss.github.io/bookmarklet-maker/ to turn this into a minified bookmarklet
 */

/**
 * Injects our custom stylesheet into the `<head>`
 *
 * It will turn custom-highlight colors to black background and white background
 * and adjusts a few generated classes to force inherited color.
 *
 * It's very likely these dynamic class names change regularly so this will probably break all the time.
 */
function addCustomStyles() {
  const style = document.createElement("style");
  style.innerText = `.custom-highlight { background: black !important; color: white !important; } ._sairaTeamName_yum7u_18, ._dataElement_yum7u_388 { color: inherit !important;}`;

  document.querySelector("head").appendChild(style);
}

addCustomStyles();

const standings = document.querySelector("#main-standings-container");

/**
 * Removes custom-highlight class from everywhere
 */
function clearHighlights() {
  const highlighted = document.querySelectorAll(".custom-highlight");
  Object.values(highlighted).forEach((node) => {
    node.classList.remove("custom-highlight");
  });
}

/**
 * When hovering over a match in the schedule, will highlight
 * the teams of the provided match argument in the standings table
 *
 */
function onHover(teams) {
  // First we clear all previous highlights
  clearHighlights();

  // Find the correct team nodes in the standings table
  const teamSpans = document.querySelectorAll("#standing-grid-team");
  const currentTeams = Object.values(teamSpans).filter((teamSpan) => {
    let name = teamSpan.querySelector("span:last-child").textContent;
    return teams.includes(name);
  });

  // For teams found, we traverse the span elements in the page
  // and currently the amount of spans is first + 6 elements.
  // Add custom-highlight class to each and remove background-color inline style
  // if one exists
  currentTeams.forEach((node) => {
    node.classList.add("custom-highlight");
    node.style.removeProperty("background-color");

    for (let i = 0; i < 6; i++) {
      node = node.nextSibling;

      node.classList.add("custom-highlight");
      node.style.removeProperty("background-color");
    }
  });
}

const matches = document.querySelectorAll(
  "#latest-games-card-container > div:not(:first-child)"
);

matches.forEach((match) => {
  match.addEventListener(
    "mouseover",
    (event) => {
      // I'm using a bit of a hack here to get the team names from alt texts.
      // It's the least likely part to break when they change something in the DOM.
      const teams = Object.values(match.querySelectorAll("img")).map(
        (img) => img.alt
      );
      onHover(teams);
    },
    {
      capture: true,
    }
  );
});

My accompanying CSS that I inject with Stylus extension:

#main-right-container {
    display: none !important;
}

._container .leftContainer {
    flex-direction: row;
    gap: 4em;
    justify-self: start;
}

#main-left-container > div {
    min-width: 400px;
}

#main-partners-banner-container {
 display: none;   
}

Opening blog posts to edit in VS Code

This is a feature I picked up from Lea Verou’s blog post Going Lean.

Since my blog posts URL path maps to my Eleventy project relatively well, I can use vscode:// protocol with a bookmarklet to open any blog post on my website into my website project in VS Code. Mind blown.

location.href = `vscode://file/[path/to/my/repository]/${(new URL(location.href)).pathname.slice(0, -1)}.njk`;

For this one, I pick up the current URL’s pathname, remove trailing slash and add .njk extension so it maps to my code. A great part of this is that it works both when I’m working locally in localhost, previewing in Netlify’s staging environment or browsing my site in production.

Other people's favorites

After sharing this blog post, I got a bunch of recommendations of what others use.

Marijke shared a Table of Contents bookmarklet originally created by her and further developed by Zoë Bijl. When run, it generates a table of contents using the headings on the page.

Jari shared two of his bookmarklets: one to invert colors and another to normalize fonts:

javascript:(function() { var v = document.getElementsByTagName("html"); v[0].style.background = "white"; v[0].style.filter = "invert(90%) sepia(60%) brightness(70%)"; v[0].style.backgroundColor = "black"; document.getElementsByTagName("body")[0].style.background = "white"; })();
javascript:(function () { const s = document.createElement("style");    s.textContent = "body * { font-family: 'Verdana' !important; font-size: 16px !important; line-height: 1.5 !important; font-weight: normal !important; letter-spacing: 0 !important; color: #000 !important; }";    document.head.append(s);})();

ichmoimeyo shared an article from The Tech Basket with 41 bookmarklets.

Syntax Error

Sign up for Syntax Error, a monthly newsletter that helps developers turn a stressful debugging situation into a joyful exploration.