Hacking the archive

GLAM Labs workshop, 24 June 2026

play along!

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)

reading urls

How many newspaper articles are in Trove?

  • go to Trove's newspapers
  • search for anything
  • delete your search term from the url
  • hit enter

web pages are never published

  • servers provide the blueprints, but your browser builds the page
  • you can inspect the blueprints to see how they work
  • you can intervene in the building process to change things

looking beneath the hood

  • View Page Source: the basic framework which might be overridden as resources load and scripts run
  • Developer Tools: live view of the current state of the page
  • right click on page
  • select Developer Tools > View Page Source
  • right click on page
  • select Developer Tools > Inspect

examine HTML code

check for errors

track what gets loaded

view styles

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

things to try

select an element to change

  • right click on the page element
  • select 'Developer tools > Inspect' to highlight the code
  • expand if necessary (click arrow)

change text

  • double click on the text to select it
  • edit the text
  • hit enter to update the page

much better!

change colors

  • select element
  • look for colours in the 'Styles' panel
  • click on the coloured squares to open colour picker
  • select a new colour

finding the source of page data

  • open the developer console
  • select the 'Network' tab
  • reload the page
  • look for data files
  • select a file to view details
  • click 'Response' to view contents

looks like metadata!

file url

look for data files

{"beaconO22":"531",
  "context" : "L",
  "@id" : "https://eu01.alma.exlibrisgroup.com/primaws/rest/pub/pnxs/L/99116987653104341",
  "adaptor" : "Local Search Engine",
  "pnx" : {
    "display" : {
      "source" : [ "Alma" ],
      "type" : [ "book" ],
      "language" : [ "eng" ],
      "title" : [ "\"Midland naturalist\" monthly metereological synopsis." ],
      "format" : [ "1 sheet ([2] pages) ; 39 x 24 cm ; folded 24 x 20 cm" ],
      "creationdate" : [ "1882" ],
      "publisher" : [ "no publisher identified" ],
      "mms" : [ "99116987653104341" ],
      "contributor" : [ "Carr, E. Donald, Rev., inscriber. Library Holding: StEdNL$$QCarr, E. Donald", "Wragge, Clement L. (Clement Lindley), 1852-1922, former owner. Library Holding: StEdNL$$QWragge, Clement L." ],
      "genre" : [ "Observations", "Sources" ],
      "place" : [ "Birmingham? :" ],
      "version" : [ "0" ],
      "lds01" : [ "A pre-printed record, folio, folding so as to form a letter with the printed addressee details of “Clement L. Wragge ... Ben Nevis Observatory, Fort William, N.B.”  The document was printed for completion by weather watchers from other parts of Britain, who provided meteorological observations for publication in \"The Midland Naturalist: The Journal of the Associated Natural History, Philosophical and Archaeology Societies and Field Clubs of the Midland Counties,\" which was published from 1878 to 1893. Wragge was a regular contributor to the journal.", "Clement Lindley Wragge (1852-1922), a British-born meteorologist, began his meteorological career in North Staffordshire. His work drew the attention of the Scottish Meteorological Society, which selected him to climb almost daily up to the summit of Ben Nevis to take meteorological observations. He won the Society’s Gold Medal for the continuous series of readings he made between 1 June and 14 October 1881. His wife, Leonora, made comparative observations at sea level at Fort William. In 1882 he had two assistants, William M. Whyte and Angus Rankin, who made the readings without him in 1883. During 1883 a special purpose-built observatory was constructed on the summit. The building was staffed full-time until 1904, when it was forced to close due to a lack of Government funding." ],
      "lds02" : [ "The printed folio has been completed in ink by the Rev. E. Donald Carr, Woolstaston (Wolstaston), Shropshire, for March 1882, and posted to Clement Wragge, with Shrewsbury and Fort Willliam postmarks. The Rev. Carr has recorded details from the local meteorological station including rainfall, temperature, notes, and remarks about changes in equipment (“as before”). <br>Library Holding: StEdNL" ],
      "lds18" : [ "Birmingham? " ],
      "subject" : [ "Meteorology -- History -- 19th century$$QMeteorology -- History -- 19th century -- Observations Sources" ]
    },
    "control" : {
      "sourcerecordid" : [ "99116987653104341" ],
      "recordid" : [ "alma99116987653104341" ],
      "sourceid" : "alma",
      "originalsourceid" : [ "99116987653104341" ],
      "sourcesystem" : [ "44NLS_INST" ],
      "sourceformat" : [ "MARC21" ],
      "score" : [ "1.0" ],
      "isDedup" : false
    },
    "addata" : {
      "originatingSystemIDContributor" : [ "no2002003249" ],
      "aulast" : [ "Carr", "Wragge" ],
      "aufirst" : [ "E. Donald", "Clement L." ],
      "auinit" : [ "E", "C" ],
      "addau" : [ "Carr, E. Donald", "Wragge, Clement L." ],
      "contributorfull" : [ "$$NCarr, E. Donald$$LCarr$$FE. Donald$$Rcontributor", "$$NWragge, Clement L.$$Rcontributor" ],
      "date" : [ "1882" ],
      "cop" : [ "Birmingham?" ],
      "pub" : [ "no publisher identified" ],
      "format" : [ "book" ],
      "genre" : [ "book" ],
      "ristype" : [ "BOOK" ],
      "btitle" : [ "\"Midland naturalist\" monthly metereological synopsis." ]
    },
    "sort" : {
      "title" : [ "Midland naturalist\" monthly metereological synopsis." ],
      "author" : [ "Carr, E. Donald, Rev., inscriber." ],
      "creationdate" : [ "1882" ]
    },
    "facets" : {
      "frbrtype" : [ "6" ]
    }
  },
  "delivery" : {
    "bestlocation" : {
      "isValidUser" : true,
      "organization" : "44NLS_INST",
      "libraryCode" : "SCRR",
      "availabilityStatus" : "available",
      "subLocation" : "Special Collections Reading Room, Edinburgh (request to consult)",
      "subLocationCode" : "GB06SC",
      "mainLocation" : "Special Collections Reading Room",
      "callNumber" : "AP.3.224.10",
      "callNumberType" : "8",
      "holdingURL" : "OVP",
      "adaptorid" : "ALMA_01",
      "ilsApiId" : "99116987653104341",
      "holdId" : "22712530560004341",
      "holKey" : "HoldingResultKey [mid=22712530560004341, libraryId=202601380004341, locationCode=GB06SC, callNumber=AP.3.224.10]",
      "matchForHoldings" : [ {
        "matchOn" : "MainLocation",
        "holdingRecord" : "852##b"
      } ],
      "stackMapUrl" : "",
      "relatedTitle" : null,
      "translateRelatedTitle" : null,
      "yearFilter" : null,
      "volumeFilter" : null,
      "singleUnavailableItemProcessType" : null,
      "boundWith" : false,
      "@id" : "_:0"
    },
    "holding" : [ {
      "isValidUser" : true,
      "organization" : "44NLS_INST",
      "libraryCode" : "SCRR",
      "availabilityStatus" : "available",
      "subLocation" : "Special Collections Reading Room, Edinburgh (request to consult)",
      "subLocationCode" : "GB06SC",
      "mainLocation" : "Special Collections Reading Room",
      "callNumber" : "AP.3.224.10",
      "callNumberType" : "8",
      "holdingURL" : "OVP",
      "adaptorid" : "ALMA_01",
      "ilsApiId" : "99116987653104341",
      "holdId" : "22712530560004341",
      "holKey" : "HoldingResultKey [mid=22712530560004341, libraryId=202601380004341, locationCode=GB06SC, callNumber=AP.3.224.10]",
      "matchForHoldings" : [ {
        "matchOn" : "MainLocation",
        "holdingRecord" : "852##b"
      } ],
      "stackMapUrl" : "",
      "relatedTitle" : null,
      "translateRelatedTitle" : null,
      "yearFilter" : null,
      "volumeFilter" : null,
      "singleUnavailableItemProcessType" : null,
      "boundWith" : false,
      "@id" : "_:0"
    } ],
    "electronicServices" : null,
    "additionalElectronicServices" : null,
    "filteredByGroupServices" : null,
    "quickAccessService" : null,
    "deliveryCategory" : [ "Alma-P" ],
    "serviceMode" : [ "ovp" ],
    "availability" : [ "available_in_library" ],
    "availabilityLinks" : [ "detailsgetit1" ],
    "availabilityLinksUrl" : [ ],
    "displayedAvailability" : null,
    "displayLocation" : true,
    "additionalLocations" : false,
    "titleRequestableAtItemLevel" : true,
    "physicalItemTextCodes" : null,
    "feDisplayOtherLocations" : false,
    "almaInstitutionsList" : [ ],
    "recordInstitutionCode" : null,
    "recordOwner" : "44NLS_INST",
    "hasFilteredServices" : null,
    "digitalAuxiliaryMode" : false,
    "digitalAuxiliaryThumbnail" : false,
    "hideResourceSharing" : false,
    "sharedDigitalCandidates" : null,
    "consolidatedCoverage" : null,
    "electronicContextObjectId" : null,
    "almaOpenurl" : null,
    "GetIt1" : [ {
      "category" : "Alma-P",
      "links" : [ {
        "isLinktoOnline" : false,
        "getItTabText" : "service_getit",
        "adaptorid" : "ALMA_01",
        "ilsApiId" : "99116987653104341",
        "link" : "OVP",
        "inst4opac" : "44NLS_INST",
        "displayText" : null,
        "@id" : "_:0"
      } ]
    } ],
    "physicalServiceId" : null,
    "link" : [ ],
    "hasD" : null
  },
  "enrichment" : {
    "virtualBrowseObject" : {
      "isVirtualBrowseEnabled" : true,
      "callNumber" : "AP.3.224.10",
      "callNumberBrowseField" : "8"
    }
  }
}
  • try opening urls
  • look for patterns in the url (eg identifiers)
https://search.nls.uk/primaws/rest/pub/pnxs/L/alma99116987653104341?vid=44NLS_INST:44NLS_VU1&lang=en&search_scope=MainCatalogue&adaptor=Local%20Search%20Engine&lang=en

item id

if you know the item id you can get data!

  • bookmarklets

  • userscripts (& userstyles)

  • extensions

changing websites

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

bookmarklets

is this image in Wikimedia Commons?

javascript:const nlsLinks=document.evaluate("//td[contains(., 'digital.nls.uk')]",document,null,XPathResult.ANY_TYPE,null),nlsLink=nlsLinks.iterateNext();window.open("https://commons.wikimedia.org/w/index.php?title=Special:LinkSearch&target="+nlsLink.textContent.replace("https://",""));
// Get digital item identifier
const nlsLinks = document.evaluate("//td[contains(., 'digital.nls.uk')]", document, null, XPathResult.ANY_TYPE, null);
const nlsLink = nlsLinks.iterateNext();

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

create a bookmarklet to check

spaces and linebreaks removed or replaced

tells the browser to run this as code

javascript:const nlsLinks=document.evaluate("//td[contains(., 'digital.nls.uk')]",document,null,XPathResult.ANY_TYPE,null),nlsLink=nlsLinks.iterateNext();window.open("https://commons.wikimedia.org/w/index.php?title=Special:LinkSearch&target="+nlsLink.textContent.replace("https://",""));

add the bookmarklet to your browser

  • right click on Bookmarks bar, select 'Add bookmark'
  • give it a name
  • copy and paste the code into the url field
  • go here and click the bookmarklet to run it!
// Get digital item identifier
const nlsLinks = document.evaluate("//td[contains(., 'digital.nls.uk')]", document, null, XPathResult.ANY_TYPE, null);
const nlsLink = nlsLinks.iterateNext();

a common bookmarklet pattern

// Construct a search url, then open it
window.open("https://commons.wikimedia.org/w/index.php?title=Special:LinkSearch&target=" + nlsLink.textContent.replace("https://", ""));
  • extract a value from the page (eg an idenitifer)
  • construct a new url using the value and open it

userscripts

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

Tampermonkey is available as an extension or add on for most browsers, to install it:

  • go to the Tampermonkey home page
  • select your browser (Chrome / Firefox / Safari / Edge)
  • follow the link to your browser's app store
  • click on the button to install Tampermonkey

Note that if you're using Tampermonkey with Chrome-based browsers (including Edge and Vivaldi), you have to explicitly give it permission to run userscripts after installation.

is this image in Wikimedia Commons?

added by a userscript

insert Wikimedia links

// ==UserScript==
// @name        NLS add Wikimedia links
// @namespace   wraggelabs.com/nls-wiki-links
// @match       *://digital.nls.uk/*
// @connect     commons.wikimedia.org
// @grant       GM_xmlhttpRequest
// @version     1.0
// @author      Tim Sherratt (timsherratt.au)
// @description 23/06/2026, 10:37:18
// ==/UserScript==
(function() {
    'use strict';
    // Make a new row for the metadata table and insert the list of links
    function makeRow(label, list) {
        let tr = document.createElement("tr");
        let th = document.createElement("th");
        th.textContent = label;
        let td = document.createElement("td");
        td.appendChild(list);
        tr.appendChild(th);
        tr.appendChild(td);
        return tr;
    }
    // Get the digital identifier
    const nlsLinks = document.evaluate("//td[contains(., 'digital.nls.uk')]", document, null, XPathResult.ANY_TYPE, null);
    const nlsLink = nlsLinks.iterateNext();
    // Only proceed if there is a digital id on this page
    if (nlsLink) {
        console.log(nlsLink.textContent);
        // Create a url to search for the pages with the id in Wikimedia Commons
        const searchUrl = "https://commons.wikimedia.org/w/api.php?action=query&format=json&list=exturlusage&euquery=" + nlsLink.textContent.replace("https://", "");
        const headers = {"User-Agent": "Userscript/NLS add Wikimedia Commons links (tim@timsherratt.au)"}
        // Query the Wikimedia API for JSON results
        GM_xmlhttpRequest({
            method: "GET",
            url: searchUrl,
            responseType: "json",
            headers: headers,
            onload: function(response) {
                console.log(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|globalusage|iwlinks|categories&inprop=url&format=json";
                    console.log(infoUrl);
                    GM_xmlhttpRequest({
                        method: "GET",
                        url: infoUrl,
                        responseType: "json",
                        headers: headers,
                        onload: function(response) {
                            // The results contain info on Commons pages, other WM pages that
                            // use the image, links from the Commons page, and categories that 
                            // the image is part of.
                            // We'll loop through each of these sets, creating arrays of HTML links.
                            let commonsList = document.createElement("ul");
                            let usageList = document.createElement("ul");
                            let linkList = document.createElement("ul");
                            let catList = document.createElement("ul");
                            // Commons pages
                            for (const [key, value] of Object.entries(response.response.query.pages)) {
                                let listItem = document.createElement("li");
                                listItem.innerHTML = "<a href='" + value.canonicalurl + "'>" + value.title + "</a>";
                                commonsList.appendChild(listItem);
                                // Usage on WP pages
                                for (let usage of value.globalusage) {
                                    let usageItem = document.createElement("li");
                                    usageItem.innerHTML = "<a href='" + usage.url + "'>" + usage.title + "</a> (" + usage.wiki + ")";
                                    usageList.append(usageItem);
                                }
                                // Links from Commons page
                                for (let link of value.iwlinks) {
                                    let linkItem = document.createElement("li");
                                    linkItem.innerHTML = "<a href='" + link.url + "'>" + link["*"] + "</a> (" + link.prefix + ")";
                                    linkList.append(linkItem);
                                }
                                // Commons categories
                                for (let cat of value.categories) {
                                    let catItem = document.createElement("li");
                                    catItem.innerHTML = "<a href='https://commons.wikimedia.org/wiki/" + cat.title + "'>" + cat.title + "</a>";
                                    catList.append(catItem);
                                }
                            }
                            // Create a new table
                            let table = document.createElement("table");
                            table.className = "metadata";
                            let caption = document.createElement("caption");
                            caption.textContent = "Wikimedia Links";
                            table.appendChild(caption);
                            let tbody = document.createElement("tbody");
                            // Add new rows for each of the link lists
                            let commonsRow = makeRow("Commons pages", commonsList);
                            tbody.appendChild(commonsRow);
                            let usageRow = makeRow("Used on pages", usageList);
                            tbody.appendChild(usageRow);
                            let linkRow = makeRow("Related links", linkList);
                            tbody.appendChild(linkRow);
                            let catRow = makeRow("Commons categories", catList);
                            tbody.appendChild(catRow);
                            table.appendChild(tbody);
                            // Get the metdata section of the page
                            let metadataList = document.querySelector("div#maincontent");
                            // Add the new table to the end of the metadata section of the page
                            metadataList.appendChild(table);
                        }
                    });
                }
            }
        });
    }
})();

To install:

a common userscript pattern

  • extract information from the current page
  • use this information to query an API
  • insert data from the API results in the page

click icon to open options

toggle userscript on/off

open dashboard

managing userscripts

Tampermonkey dashboard

click on a script to edit it

create a basic userscript

  • open the Tampermonkey dashboard
  • click on the '+' tab to create a new script
  • delete the template code
  • copy and paste the code here into the empty script
  • use Ctrl-S to save the script
  • open the NLS home page
// ==UserScript==
// @name         My First Userscript
// @namespace    http://tampermonkey.net/
// @version      2026-06-24
// @author       You
// @match        https://www.nls.uk/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=nls.uk
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    const button = document.querySelector("a.page-header__cta");
    button.textContent = "Explore our cool stuff!";
})();

metadata

code

more userscripts

seeing people in the archives

more examples

browse collection images!

You can customise by

  • adding IIIF manifest urls
  • setting the thumbnail size
  • setting the number of images to display
// ==UserScript==
// @name         NLS add images to homepage
// @namespace    wraggelabs.com/nls-home-thumbnails
// @version      2026-06-23
// @description  Adds a random selection of thumbnail images to the NLS home page
// @author       Tim Sherratt (timsherratt.au)
// @match        https://www.nls.uk/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=nls.uk
// @grant        GM_xmlhttpRequest
// @grant        GM_addElement
// ==/UserScript==

(function() {
    'use strict';
    // CONFIGURATION SECTION
    // Add/remove manifest urls
    const manifests = [
        "https://view.nls.uk/manifest/2438/7517/243875173/manifest.json",
        "https://view.nls.uk/manifest/1897/4224/189742241/manifest.json",
        "https://view.nls.uk/manifest/7549/75496599/manifest.json",
        "https://view.nls.uk/manifest/7445/74457611/manifest.json"
    ];
    // Max number of images to display
    const numImages = 57;
    // Size of thumbnails
    const thumbSize = 50;
    // END CONFIG SECTION

    const header = document.querySelector("header.page-header > div");
    const imageDiv = document.createElement("div");
    imageDiv.style.display = "flex";
    imageDiv.style.marginTop = "20px";
    imageDiv.style.width = "100%";
    imageDiv.style.flexWrap = "wrap";
    const allImages = [];
    var manifestsProcessed = 0;

    function shuffleArray(array) {
        for (let i = array.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [array[i], array[j]] = [array[j], array[i]];
        }
    }
    for (let manifestUrl of manifests) {
        GM_xmlhttpRequest({
            method: "GET",
            url: manifestUrl,
            responseType: "json",
            onload: function(response) {
                var link, title;
                const manifest = response.response;

                for (let md of manifest.metadata) {
                    if (md.value.includes("digital.nls.uk")) {
                        link = md.value.match(/(https?:\/\/digital.nls.uk\/\d+)/)[0];
                    } else if (md.label == "Title") {
                        title = md.value;
                    }
                }
                for (let canvas of manifest.sequences[0].canvases) {
                    for (let img of canvas.images) {
                        let imageId = img.resource.service["@id"];
                        allImages.push({"src": `${imageId}/square/${thumbSize},/0/default.jpg`, "link": link, "title": title});
                    }
                }
                manifestsProcessed++;
                displayImages();
            }
        });
    }
    function displayImages() {
        if (manifests.length == manifestsProcessed) {
            shuffleArray(allImages);
            for (let image of allImages.slice(0, numImages)) {
                let imageLink = document.createElement("a");
                imageLink.href = image.link;
                GM_addElement(imageLink, 'img', {
                    src: image.src,
                    title: image.title,
                    style: "margin: 4px"
                });
                imageDiv.appendChild(imageLink);
            }
            header.appendChild(imageDiv);
        }
    }
})();
const manifests = [
        "https://view.nls.uk/manifest/2438/7517/243875173/manifest.json",
        "https://view.nls.uk/manifest/1897/4224/189742241/manifest.json",
        "https://view.nls.uk/manifest/7549/75496599/manifest.json",
        "https://view.nls.uk/manifest/7445/74457611/manifest.json"
];

add your own manifests

watch out for commas and quotes!

finding manifest urls

  • start in the digital gallery
  • click through to images
  • look for a IIIF Manifest link
  • if there's no manifest link look for a Universal Viewer link and open that
  • copy IIIF Manifest url

add info to Primo catalogue

// ==UserScript==
// @name        NLS add catalogue info
// @namespace   wraggelabs.com/nls-add-catalogue-info
// @match       *://search.nls.uk/discovery*
// @grant       none
// @version     1.0
// @author      -
// @description 23/06/2026, 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

your ideas!

  • what would you like to fix, improve, add?
  • plan with pseudo-code
  • share and maybe build

resources