Hacking the library

SLV LAB Code Club, 2 October 2025

transforms the effectivities of socio-technical systems, making them work, or un-work, often in new and unexpected ways

hack (noun):

Mark Olson (2013), "Hacking the humanities: Twenty-first-century literacies and the ‘becoming other' of the humanities".

Hacking the systems that construct and control access to our cultural collections is at the core of humanities practice in the early 21st century. As online collections continue to expand, we need to carve out spaces that resist the weight of scale and foster alternative perspectives. As interfaces grow in sophistication and complexity, we need to stage playful and pointed interventions that reveal their limits and empower critique. We do not all have to be coders, but we do have to take code seriously. We have to take what we are given by collection databases and change it.

me:

GLAM hacking 101...

  • learn to read urls
  • look beneath the hood to see how things work
  • experiment (change things and see what happens)

looking under the hood

right click
→ Developer tools
→ Inspect

examine HTML code

check for errors

track what gets loaded

  • change some text
  • change some colours
  • swap an image

things to try

  • bookmarklets

  • userscripts (& userstyles)

  • extensions

changing websites

all ways of running javascript in your browser to change the content or behaviour of websites

bookmarklets

javascript:let%20handle=document.querySelector("input.handle");window.open("https://commons.wikimedia.org/w/index.php?title=Special:LinkSearch&target="+handle.value.replace("http://",""));
//Get the handle url from the input box
let handle = document.querySelector("input.handle");

// Construct a search url, then open it
window.open("https://commons.wikimedia.org/w/index.php?title=Special:LinkSearch&target=" + handle.value.replace("http://", ""));

create a bookmarklet

spaces and linebreaks removed or replaced

tells the browser to run this as code

javascript:let%20handle=document.querySelector("input.handle");window.open("https://commons.wikimedia.org/w/index.php?title=Special:LinkSearch&target="+handle.value.replace("http://",""));

add a bookmarklet

  1. right click on Bookmarks bar, select 'Add bookmark'
  2. give it a name
  3. copy and paste the code into the url field
  4. go here and try it!

userscripts

  • can change many aspects of a web page
  • need a userscript manager such as Tampermonkey or Violentmonkey
  • create/change userscripts using the built-in editor
  • install userscripts from sources such as GitHub Gists
  • share your userscripts!
  • be aware of security risks

added by a userscript

insert Commons links


// ==UserScript==
// @name        SLV add Wikimedia Commons links
// @namespace   wraggelabs.com/slv_add_wikimedia_commons_links
// @match       *://viewer.slv.vic.gov.au/?entity=*
// @connect     commons.wikimedia.org
// @grant       GM_xmlhttpRequest
// @version     1.0
// @author      -
// @description 29/09/2025, 10:37:18
// @require https://cdn.jsdelivr.net/gh/CoeJoder/waitForKeyElements.js@v1.3/waitForKeyElements.js
// ==/UserScript==
waitForKeyElements("#handleURL", (handleInput) => {
  // Get the handle url
  const handle = document.querySelector("input.handle");
  // Create a url to search for the handle in Wikimedia Commons
  const searchUrl = "https://commons.wikimedia.org/w/api.php?action=query&format=json&list=exturlusage&euquery=" + handle.value.replace("http://", "");
  const headers = {"User-Agent": "Userscript/SLV add Wikimedia Commons links (tim@timsherratt.au)"}
  // Query the Wikimedia API
  GM_xmlhttpRequest({
    method: "GET",
    url: searchUrl,
    responseType: "json",
    headers: headers,
    onload: function(response) {
      // Extract page ids from the results (if any)
      let pageIds = [];
      for (let page of response.response["query"]["exturlusage"]) {
        pageIds.push(page["pageid"]);
      }
      // If there are results, we'll get more info using the page ids
      if (pageIds.length > 0) {
        // Create a url using the page ids to get more info
        let infoUrl = "https://commons.wikimedia.org/w/api.php?action=query&pageids=" + pageIds.join("|") + "&prop=info&inprop=url&format=json";
        GM_xmlhttpRequest({
          method: "GET",
          url: infoUrl,
          responseType: "json",
          headers: headers,
          onload: function(response) {
            // Get the titles and urls from the results and put them in an HTML list
            let linkList = document.createElement("ul");
            for (const [key, value] of Object.entries(response.response["query"]["pages"])) {
              let pageUrl = value["canonicalurl"];
              let pageTitle = value["title"];
              let listItem = document.createElement("li");
              listItem.innerHTML = "<a href='" + pageUrl + "' style='color: #1779ba;'>" + pageTitle + "</a>";
              linkList.appendChild(listItem);
            }
            // Add a new row in the further details section of the page and add the list
            // New heading in dt
            let dt = document.createElement("dt");
            dt.className = "cell small-6 medium-2 large-2";
            dt.innerHTML = "Wikimedia Commons";
            // New dd to contain list
            let dd = document.createElement("dd");
            dd.className = "cell small-6 medium-10 large-10";
            dd.appendChild(linkList);
            // Get the current dl list
            let infoList = document.querySelector("dl#moreInfoList");
            // Add the new row
            infoList.appendChild(dt);
            infoList.appendChild(dd);
          }
        });
      }
    }
  });
});

To install:

more userscripts

seeing people in the archives

?!

your own nav bar

// ==UserScript==
// @name        SLV My Favs menu creator
// @namespace   Violentmonkey Scripts
// @match       *://www.slv.vic.gov.au/*
// @grant       none
// @version     1.0
// @author      -
// @description 28/09/2025, 11:19:01
// ==/UserScript==

// Edit this list to ad your own links
myLinks = [
  {
    "href": "https://find.slv.vic.gov.au/discovery/collectionDiscovery?vid=61SLV_INST:SLV&collectionId=81274079160007636",
    "text": "Melbourne and Metropolitan Board of Works plans"
  }
]

// Create a new top-level nav menu heading
let menuItem = document.createElement("li");
menuItem.className = "site-navigation__item site-navigation__item--get-involved";

let menuHead = document.createElement("a");
menuHead.className = "site-navigation__link js-site-navigation__link";
menuHead.href = "#";
menuHead.innerHTML = "My favourites"

// Display the megamenu when you hover over the heading
menuItem.addEventListener("mouseenter", (event) => {
  event.target.className = "site-navigation__item site-navigation__item--get-involved site-navigation__item--dropdown";
});
// Hide the megamenu when you move the mouse away
menuItem.addEventListener("mouseleave", (event) => {
  event.target.className = "site-navigation__item site-navigation__item--get-involved";
});

// Create the megamenu
// Lots of layers of divs in the MegaMenu!
let megaMenu = document.createElement("div");
megaMenu.className = "megamenu";

let megaMenuContainer = document.createElement("div");
megaMenuContainer.className = "megamenu__container";

let megaMenuNav = document.createElement("nav");
megaMenuNav.className ="megamenu-navigation";

let megaMenuSection = document.createElement("section");
megaMenuSection.className = "megamenu__section";

let megaMenuList = document.createElement("ul");
megaMenuList.className = "megamenu__list"

// Add links to the megamenu
for (link of myLinks) {
  let megaMenuItem = document.createElement("li");
  let megaMenuLink = document.createElement("a");
  megaMenuLink.className = "megamenu__link";
  megaMenuLink.href = link["href"];
  megaMenuLink.innerHTML = link["text"];
  megaMenuItem.appendChild(megaMenuLink);
  megaMenuList.appendChild(megaMenuItem);
}

// Get the nav menu container
let menuBar = document.getElementsByClassName("nav navbar-nav site-navigation__list")[0];

// Add everything in the hierarchy
megaMenuSection.appendChild(megaMenuList);
megaMenuNav.appendChild(megaMenuSection);
megaMenuContainer.appendChild(megaMenuNav);
megaMenu.appendChild(megaMenuContainer);

menuItem.appendChild(menuHead);
menuItem.appendChild(megaMenu);

menuBar.appendChild(menuItem);
myLinks = [
  {
    "href": "https://yourfav1.com",
    "text": "Title of YourFav1"
  },
  {
    "href": "https://yourfav2.com",
    "text": "Title of YourFav2"
  },
  {
    "href": "https://yourfav3.com",
    "text": "Title of YourFav3"
  }
]

add your own links

watch out for commas and curly brackets!

find the easter eggs!

create a simple game

// ==UserScript==
// @name        SLV easter egg hunt
// @namespace   wraggelabs.com/slv_easter_egg_hunt
// @match       *://viewer.slv.vic.gov.au/?entity=*
// @grant       none
// @version     1.0
// @author      -
// @description 28/09/2025, 14:41:35
// @require https://cdn.jsdelivr.net/gh/CoeJoder/waitForKeyElements.js@v1.3/waitForKeyElements.js
// ==/UserScript==

// These are the pages on which the eggs will appear
const pages = [
  "https://viewer.slv.vic.gov.au/?entity=IE741728&mode=browse",
  "https://viewer.slv.vic.gov.au/?entity=IE4152304&mode=browse",
  "https://viewer.slv.vic.gov.au/?entity=IE223183&mode=browse"
];

// The message on page 1 gives clues to page 2 etc
const messages = [
  "WELL DONE!\n\nYou've found the first egg\n\nHere's some clues to help you find the next one:\n\n\t• Part of the anti-conscription campaign in WWI\n\t• 'The Death ______'\n\n",
  "GREAT JOB!\n\nYou've found the second egg\n\nHere's some clues to help you find the next one:\n\n\t• Copyright registration\n\t• full of 'vegetable bitters'\n\n",
  "SMASHED IT!\n\nYou've found all the eggs, but why not keep exploring the Library's digitised collections?\n\n",
]

// Check to see if the current page needs an egg
function getOrder() {
  for (let i=0; i<pages.length; i++) {
    if (window.location.href == pages[i]) {
      console.log("found", i);
      return i;
    }

  }
  return false;
}

function addEgg(order) {
  let footerList = document.querySelector("nav.footer-navigation ul");
  let egg = document.createElement("li");
  egg.className = "footer-navigation__item";
  egg.innerHTML = "🥚";
  egg.onclick = function() { alert(messages[order]);}
  egg.style.cursor = "pointer";
  footerList.appendChild(egg);
}

waitForKeyElements("#handleURL", (handleInput) => {
  const order = getOrder();
  if (order !== false) {
    addEgg(order);
  }
});
// These are the pages on which the eggs will appear
const pages = [
  "https://viewer.slv.vic.gov.au/?entity=IE741728&mode=browse",
  "https://viewer.slv.vic.gov.au/?entity=IE4152304&mode=browse",
  "https://viewer.slv.vic.gov.au/?entity=IE223183&mode=browse"
];

// The message on page 1 gives clues to page 2 etc
const messages = [
  "WELL DONE!\n\nYou've found the first egg\n\nHere's some clues to help you find the next one:\n\n\t• Part of the anti-conscription campaign in WWI\n\t• 'The Death ______'\n\n",
  "GREAT JOB!\n\nYou've found the second egg\n\nHere's some clues to help you find the next one:\n\n\t• Copyright registration\n\t• full of 'vegetable bitters'\n\n",
  "SMASHED IT!\n\nYou've found all the eggs, but why not keep exploring the Library's digitised collections?\n\n",
]

pages the eggs appear on

clues when you click on the egg

add your own pages and clues

add info to Primo

// ==UserScript==
// @name        SLV add catalogue info
// @namespace   Violentmonkey Scripts
// @match       *://find.slv.vic.gov.au/discovery/*
// @grant       none
// @version     1.0
// @author      -
// @description 28/09/2025, 14:41:35
// @require https://cdn.jsdelivr.net/gh/CoeJoder/waitForKeyElements.js@v1.3/waitForKeyElements.js
// ==/UserScript==


function changeDisplay() {
  if (document.location.href.includes("fulldisplay")) {
    waitForKeyElements("#full-view-container", (container) => {
      let details = document.querySelector("#item-details div.spaced-rows");

      let newRow = document.createElement("div");

      newRow.className="layout-block-xs layout-xs-column layout-row";
      let newLabel = document.createElement("div");
      newLabel.className = "flex-gt-xs-25 flex-gt-sm-20 flex bold-text";
      newLabel.innerHTML = "New info";
      let newValue = document.createElement("div");
      newValue.className = "flex";
      newValue.innerHTML = "Some additional piece of information";
      newRow.appendChild(newLabel);
      newRow.appendChild(newValue);
      details.appendChild(newRow);
    });
  }
}

// This function looks for clicks in search results that show full item details in an overlay.
// These clicks don't actually open a new page, even though the url changes.
function watchHistoryEvents() {
    const { pushState, replaceState } = window.history;

    window.history.pushState = function (...args) {
        pushState.apply(window.history, args);
        window.dispatchEvent(new Event('pushState'));
    };
    window.addEventListener('pushState', changeDisplay);
}

watchHistoryEvents();

changeDisplay();
  • waits until specific parts of the page are loaded
  • works with full display in overlay or new window

resources