Automatically fetches and appends all pages of comments on AO3 — et carthago delenda est. Matches are AO3 and its TLDs that are not redirects directly to .org (no point in matching the TLDs that are simply redirects — cf. https://archiveofourown.org/faq/accessing-fanworks#archiveurl).
// ==UserScript==
// @name AO3: Load All Comments
// @namespace https://greasyfork.org/en/users/1555174-charles-rockafellor
// @version 1.5 on 07 May 2026
// @description Automatically fetches and appends all pages of comments on AO3 — et carthago delenda est. Matches are AO3 and its TLDs that are not redirects directly to .org (no point in matching the TLDs that are simply redirects — cf. https://archiveofourown.org/faq/accessing-fanworks#archiveurl).
// @author Charles Rockafellor
// @homepageURL https://archiveofourown.org/users/Charles_Rockafellor/collections
// @match https://archiveofourown.org/works/*
// @match https://archiveofourown.gay/works/*
// @match https://archive.transformativeworks.org/works/*
// @match https://insecure.archiveofourown.org/works/*
// @icon https://i.pinimg.com/1200x/98/af/04/98af04b09aedd599def9750ccf4c5ff9.jpg
// @grant none
// @run-at document-end
// @license MIT; https://opensource.org/license/mit
// @connect archiveofourown.org
// @history 1.5 — Included [successfully] the truncated threads' expansion without triggering too many requests at once. If you encounter a "429 Too Many Requests" error, just let the connection cool down for a few seconds or minutes, and then try again.
// @history 1.4 — Trying to retain thread structure, ignoring the thread-truncation (the "[n-many] more comments in this thread" message) for now in order to hopefully avoid the "Too many requests" AO3-limit.
// @history 1.3 — Stripped back selectors to be as broad as possible, removed observer and added immediate console log to check console pane for script having been loaded or not.
// @history 1.2 — Swapped scheme from wildcard to https, added a MutationObserver.
// @history 1.1 — Initial build.
// ==/UserScript==
(function() {
'use strict';
const DELAY_MS = 2000; // Increased to 2s for safety with deep expansion
const checkExist = setInterval(function() {
const thread = document.querySelector('ol.thread');
if (thread && !document.querySelector('#load-all-comments-btn')) {
initScript(thread);
}
}, 1000);
function initScript(thread) {
const loadAllBtn = document.createElement('button');
loadAllBtn.id = 'load-all-comments-btn';
loadAllBtn.innerHTML = "LOAD ALL & EXPAND DEEP THREADS";
loadAllBtn.style = "display: block; width: 100%; margin: 20px 0; padding: 15px; background: #900; color: #fff; border: 2px solid #333; cursor: pointer; font-weight: bold; font-family: sans-serif;";
thread.parentNode.insertBefore(loadAllBtn, thread);
loadAllBtn.addEventListener('click', (e) => {
e.preventDefault();
loadAllBtn.disabled = true;
loadAllBtn.style.background = "#444";
loadAllBtn.innerHTML = "PHASE 1: MERGING PAGES...";
fetchNextPage(document);
});
}
function fetchNextPage(doc) {
const nextLink = doc.querySelector('li.next a');
if (nextLink) {
setTimeout(() => {
fetch(nextLink.href).then(r => r.text()).then(html => {
const nextDoc = new DOMParser().parseFromString(html, 'text/html');
const remoteThread = nextDoc.querySelector('ol.thread');
if (remoteThread) document.querySelector('ol.thread').insertAdjacentHTML('beforeend', remoteThread.innerHTML);
fetchNextPage(nextDoc);
});
}, DELAY_MS);
} else {
console.log("AO3 Script: Starting Phase 2 (Expansion)...");
expandTruncatedThreads();
}
}
async function expandTruncatedThreads() {
const btn = document.querySelector('#load-all-comments-btn');
// Find all <li> that contain a "more comments in this thread" link
let expansionLinks = Array.from(document.querySelectorAll('ol.thread li.comment a')).filter(a => a.textContent.includes('more comments in this thread'));
if (expansionLinks.length === 0) {
finish();
return;
}
btn.innerHTML = `PHASE 2: EXPANDING ${expansionLinks.length} DEEP THREADS...`;
for (let link of expansionLinks) {
await new Promise(r => setTimeout(r, DELAY_MS)); // Safety delay
try {
const response = await fetch(link.href);
const html = await response.text();
const threadDoc = new DOMParser().parseFromString(html, 'text/html');
// Find the new comments in the fetched thread
const deepThread = threadDoc.querySelector('ol.thread');
if (deepThread) {
// Replace the "X more comments" <li> with the actual content
link.closest('li').insertAdjacentHTML('afterend', deepThread.innerHTML);
link.closest('li').remove();
}
} catch (err) {
console.error("Expansion failed for link:", link.href);
}
}
// Check again in case the new content has EVEN DEEPER links
const remaining = Array.from(document.querySelectorAll('ol.thread li.comment a')).filter(a => a.textContent.includes('more comments in this thread'));
if (remaining.length > 0) {
expandTruncatedThreads();
} else {
finish();
}
}
function finish() {
const btn = document.querySelector('#load-all-comments-btn');
btn.innerHTML = "ALL PAGES LOADED & EXPANDED";
document.querySelectorAll('ol.pagination').forEach(p => p.style.display = 'none');
}
})();