Feature request: Custom JavaScript injection for replayed pages

Hi Webrecorder team,

I use ArchiveWeb.page to archive forum sites built on Invision Community (IPS). During replay, pagination within a topic doesn’t work — clicking “Page 2” leaves the page frozen on Page 1. The URL in the sidebar changes but the content doesn’t.

**Why this happens:**

IPS builds its pagination on AJAX. When you click “Page 2” on the live site, the site’s JavaScript fetches the next page’s content in the background and swaps it into the DOM. My archiver forces full-page navigations during the crawl, so ArchiveWeb.page records clean HTML pages — but the *original* IPS JavaScript is still saved with them. When I click “Page 2” in replay, the archived JavaScript tries to make the AJAX request that was never recorded. The request fails silently and the DOM never updates.

**Why I can’t work around it:**

I’ve tried injecting a userscript via Tampermonkey to intercept pagination clicks and force a `window.location.assign()`. This works fine on the live site. It does not work during replay because:

1. ArchiveWeb.page serves replayed pages inside an `iframe` whose URL is `chrome-extension:///…`

2. Tampermonkey’s `@match` rules are evaluated against that `chrome-extension://` URL

3. Tampermonkey refuses to inject userscripts into extension-served frames (security policy)

4. I confirmed this by checking `window.location.href` inside the replay frame — it returns the `chrome-extension://` URL, and Tampermonkey never fires

**The feature request:**

Please add a “Custom JavaScript” field to ArchiveWeb.page’s settings, similar to what other web-archiving tools offer. Content pasted there would be injected into every replayed page’s document, before the page’s own scripts run. The field could be:

- Global (applies to all collections) or

- Per-collection (applies to one archive)

**How it would help my use case:**

With ~15 lines of JavaScript injected into every replayed page, I can intercept pagination clicks in the capture phase, stop the broken AJAX handler, and force a full `window.location.assign()` to the recorded Page 2 URL. ArchiveWeb.page’s Service Worker then serves the recorded page from the archive — exactly the same way clicking the sidebar works today. This is the only thing preventing multi-page topics from being navigable during replay.

**What I’ve verified on my end:**

- The replayed page’s HTML is intact and correct

- The sidebar can open any recorded page successfully

- The URL of the inner frame is confirmed to be `chrome-extension://…`

- The pagination click handler in the archived IPS JavaScript can be intercepted with a capture-phase listener, but only from inside the frame itself — Tampermonkey cannot reach it

This is not specific to Invision Community. Any archived site with client-side routing (AJAX pagination, SPA navigation, infinite scroll) has the same problem in replay, and the same feature would fix all of them.

**Alternative implementations** if a script field is too broad:

- A “Pagination Fix” toggle that automatically intercepts `a[href*=“page=”]` clicks in replay

- A per-collection “inject script” field accessible from the collection settings dialog

Thank you for the work on this tool. Let me know if you’d like a minimal reproduction case.

Are the pagination controls being clicked during the archiving process?

Hi, I’ve encountered this a lot. Many e.g. server-rendered applications that work on the client-side too, have this issue.

My approach was to inject custom Javascript into ReplayWeb.page using this Docker image, and e.g. an inject.js like:

(function() {
  // disable click handlers of links, so we can follow links server-side instead of client-side
  // probably will need some tweaking on some sites!
  function disable_js_links({ dynamic = false, removeAttrs = [] }) {
    function removeHandlers(node) {
      const clone = node.cloneNode(true);
      (removeAttrs || []).forEach((attr) => {
        clone.removeAttribute(attr);
      });
      node.replaceWith(clone);
    }

    window.addEventListener('DOMContentLoaded', (event) => {
      console.debug('[inject.js] Removing event handlers from <a> elements...');
      // optionally do this on newly added elements
      if (dynamic) {
        const observer = new MutationObserver((mutationList, observer) => {
          for (mutation in mutationList) {
            if (mutation.type === 'childList') {
	          mutation.addedNodes.forEach(removeHandlers);
            }
          }
        });
        observer.observe(document.body, { childList: true, subtree: true });
      }
      // Remove event handlers from all existing a elements
      document.querySelectorAll('a').forEach(removeHandlers);
    });
  }

 function remove_element_by_id(id) {
    window.addEventListener('DOMContentLoaded', (event) => {
      const el = document.getElementById(id);
      if (el) el.remove();
    });
  }

  function remove_element_by_tag_name(tag) {
    window.addEventListener('DOMContentLoaded', (event) => {
      document.getElementsByTagName(tag).forEach((element) => {
        element.remove();
      });
    });
  }

  function create_inline_css(css) {
    const stylesheet = document.createElement('style');
    stylesheet.innerText = css;
    document.head.appendChild(stylesheet);
  }4

  // Site-specific tweaks based on hostname
  const re = new RegExp('^.+https?:/+(www\.)?([^/?]+)', 'i');
  const match = document.location.pathname.match(re);
  const domain = match && match[2];
  console.log('[inject.js] Activating tweaks for', domain);
  switch(domain) {
  case 'example.com':
    disable_js_links({});
    remove_element_by_id('cookie-banner');
    create_inline_css('body { overflow: auto !important; }');
    break;
  default:
    console.log('[inject.js] No domain-specific Javascript injections for', domain);
  }
})();

disable_js_links just removes click handlers, and makes links work as if no Javascript was present.

But the feature request you have here, is something I think would be useful to have: some way to adapt replaying so that an archive can be shown properly, with links working. It could either be inside the WACZ (portable, but changing it would require changing the WACZ - and what about crawls with multiple WACZs), or it could be part of Browsertrix’s metadata about a crawl.


Subject: Re: Feature request: Custom JavaScript injection for replayed pages

Hi,

Thanks for getting back to me. To answer your question: no, the pagination controls are not clicked in the normal AJAX way during archiving.

During the crawl, I use a userscript that intercepts clicks on pagination links and forces a full-page navigation (window.location.assign()). This is deliberate — it ensures ArchiveWeb.page records clean, fully-rendered HTML pages for each page of a topic, rather than relying on the site’s client-side AJAX to swap content into the DOM.

The downside is exactly what I described: the archived JavaScript still contains the original IPS AJAX pagination handlers. When I click “Page 2” during replay, that JavaScript tries to make the same background request it would on the live site. Since that request was never recorded (because we bypassed it during capture), it fails silently and the DOM never updates. The sidebar works because it triggers a full page load, which the Service Worker can serve from the archive.

So the problem is a mismatch between how the page was captured (full-page navigations) and how the archived JS expects to work (AJAX). I could try to click every pagination link during recording, but that would mean the archiver captures the AJAX responses — however, it would also mean the page is recorded in a partially-updated state, and the navigation between pages would still be broken in replay because the JS would try to re-fetch.

The Docker image mentioned by [other person] is a clever workaround, and I appreciate them sharing it. But it requires self-hosting, a reverse proxy, and manual script maintenance. It’s not something a typical ArchiveWeb.page user can easily adopt.

That’s why I’m asking for a built-in way to inject custom JavaScript into replayed pages — either globally or per-collection. Even a simple “Pagination Fix” toggle that strips click handlers from a[href*="page="] during replay would solve this for a huge class of sites (forums, blogs, e-commerce, any client-side routing).

I’m happy to provide a minimal WACZ or a screen recording if that would help. Thanks again for looking into this.

Best,
[Jeon]