From e26aa103367a3409293e099b2cdc5b53e5f237b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Dec 2025 17:53:17 +0000 Subject: [PATCH] Fix pagination links broken on gistpreview.github.io The JavaScript that rewrites relative URLs for gistpreview.github.io now handles dynamic content and SPA-style navigation: - Skip already-rewritten links (starting with '?') to prevent double-rewriting when JS runs multiple times - Use MutationObserver to catch dynamically added links - Run on DOMContentLoaded as fallback for DOM timing issues - Add longer delay retry (2s) for slow-loading content Fixes #26 --- src/claude_code_transcripts/__init__.py | 67 +++++++++++++++++++++---- tests/test_generate_html.py | 37 ++++++++++++++ 2 files changed, 94 insertions(+), 10 deletions(-) diff --git a/src/claude_code_transcripts/__init__.py b/src/claude_code_transcripts/__init__.py index 8729fad..b2cffff 100644 --- a/src/claude_code_transcripts/__init__.py +++ b/src/claude_code_transcripts/__init__.py @@ -1048,6 +1048,7 @@ def render_message(log_type, message_json, timestamp): """ # JavaScript to fix relative URLs when served via gistpreview.github.io +# Fixes issue #26: Pagination links broken on gistpreview.github.io GIST_PREVIEW_JS = r""" (function() { if (window.location.hostname !== 'gistpreview.github.io') return; @@ -1055,17 +1056,63 @@ def render_message(log_type, message_json, timestamp): var match = window.location.search.match(/^\?([^/]+)/); if (!match) return; var gistId = match[1]; - document.querySelectorAll('a[href]').forEach(function(link) { - var href = link.getAttribute('href'); - // Skip external links and anchors - if (href.startsWith('http') || href.startsWith('#') || href.startsWith('//')) return; - // Handle anchor in relative URL (e.g., page-001.html#msg-123) - var parts = href.split('#'); - var filename = parts[0]; - var anchor = parts.length > 1 ? '#' + parts[1] : ''; - link.setAttribute('href', '?' + gistId + '/' + filename + anchor); + + function rewriteLinks(root) { + (root || document).querySelectorAll('a[href]').forEach(function(link) { + var href = link.getAttribute('href'); + // Skip already-rewritten links (issue #26 fix) + if (href.startsWith('?')) return; + // Skip external links and anchors + if (href.startsWith('http') || href.startsWith('#') || href.startsWith('//')) return; + // Handle anchor in relative URL (e.g., page-001.html#msg-123) + var parts = href.split('#'); + var filename = parts[0]; + var anchor = parts.length > 1 ? '#' + parts[1] : ''; + link.setAttribute('href', '?' + gistId + '/' + filename + anchor); + }); + } + + // Run immediately + rewriteLinks(); + + // Also run on DOMContentLoaded in case DOM isn't ready yet + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { rewriteLinks(); }); + } + + // Use MutationObserver to catch dynamically added content + // gistpreview.github.io may add content after initial load + var observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + mutation.addedNodes.forEach(function(node) { + if (node.nodeType === 1) { // Element node + rewriteLinks(node); + // Also check if the node itself is a link + if (node.tagName === 'A' && node.getAttribute('href')) { + var href = node.getAttribute('href'); + if (!href.startsWith('?') && !href.startsWith('http') && + !href.startsWith('#') && !href.startsWith('//')) { + var parts = href.split('#'); + var filename = parts[0]; + var anchor = parts.length > 1 ? '#' + parts[1] : ''; + node.setAttribute('href', '?' + gistId + '/' + filename + anchor); + } + } + } + }); + }); }); + // Start observing once body exists + function startObserving() { + if (document.body) { + observer.observe(document.body, { childList: true, subtree: true }); + } else { + setTimeout(startObserving, 10); + } + } + startObserving(); + // Handle fragment navigation after dynamic content loads // gistpreview.github.io loads content dynamically, so the browser's // native fragment navigation fails because the element doesn't exist yet @@ -1084,7 +1131,7 @@ def render_message(log_type, message_json, timestamp): // Try immediately in case content is already loaded if (!scrollToFragment()) { // Retry with increasing delays to handle dynamic content loading - var delays = [100, 300, 500, 1000]; + var delays = [100, 300, 500, 1000, 2000]; delays.forEach(function(delay) { setTimeout(scrollToFragment, delay); }); diff --git a/tests/test_generate_html.py b/tests/test_generate_html.py index b79542b..5e29bc8 100644 --- a/tests/test_generate_html.py +++ b/tests/test_generate_html.py @@ -466,6 +466,43 @@ def test_handles_empty_directory(self, output_dir): inject_gist_preview_js(output_dir) # Should complete without error + def test_gist_preview_js_skips_already_rewritten_links(self): + """Test that GIST_PREVIEW_JS skips links that have already been rewritten. + + When navigating between pages on gistpreview.github.io, the JS may run + multiple times. Links that have already been rewritten to the + ?GIST_ID/filename.html format should be skipped to avoid double-rewriting. + + This fixes issue #26 where pagination links break on later pages. + """ + # The JS should check if href already starts with '?' + assert "href.startsWith('?')" in GIST_PREVIEW_JS + + def test_gist_preview_js_uses_mutation_observer(self): + """Test that GIST_PREVIEW_JS uses MutationObserver for dynamic content. + + gistpreview.github.io loads content dynamically. When navigating between + pages via SPA-style navigation, new content is inserted without a full + page reload. The JS needs to use MutationObserver to detect and rewrite + links in dynamically added content. + + This fixes issue #26 where pagination links break on later pages. + """ + # The JS should use MutationObserver + assert "MutationObserver" in GIST_PREVIEW_JS + + def test_gist_preview_js_runs_on_dom_content_loaded(self): + """Test that GIST_PREVIEW_JS runs on DOMContentLoaded. + + The script is injected at the end of the body, but in some cases + (especially on gistpreview.github.io), the DOM might not be fully ready + when the script runs. We should also run on DOMContentLoaded as a fallback. + + This fixes issue #26 where pagination links break on later pages. + """ + # The JS should listen for DOMContentLoaded + assert "DOMContentLoaded" in GIST_PREVIEW_JS + class TestCreateGist: """Tests for the create_gist function."""