☰

Pawchive Next/Previous Post Navigator

Displays preview cards for previous and next posts on Pawchive post pages with instant navigation.

スクリプトをインストールするには、Tampermonkey, Greasemonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、 Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Userscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら、通報はこちらへお寄せください
// ==UserScript==
// @name         Pawchive Next/Previous Post Navigator
// @namespace    pawchive-next-previous-card
// @version      1.0
// @description  Displays preview cards for previous and next posts on Pawchive post pages with instant navigation.
// @author       Mekokung
// @tag          utilities
// @icon         https://pawchive.pw/static/favicon.png
// @supportURL   https://linktr.ee/Mekokung
// @match        https://pawchive.pw/patreon/user/*
// @match        https://pawchive.pw/fanbox/user/*
// @match        https://pawchive.pw/discord/user/*
// @match        https://pawchive.pw/discord/server/*
// @match        https://pawchive.pw/*
// @run-at       document-start
// @noframes
// @grant        none
// @license      GPL-3.0
// ==/UserScript==

(function () {
    'use strict';

    const CONFIG = {
        debug: false,
        cacheStorageKey: 'pawchive_post_navigator_cache_v1',
        maxCacheEntries: 300,
        requestTimeoutMs: 8000,
        sidebarWidthRem: 12,
        cardWidthPx: 270,
        cardHeightPx: 92,
        thumbWidthPx: 90
    };

    const PREFIX = '[PawchiveNav]';

    function log(...args) {
        if (CONFIG.debug) {
            console.log(PREFIX, ...args);
        }
    }

    function logError(...args) {
        console.error(PREFIX, ...args);
    }

    class PostCache {
        constructor() {
            this.memoryCache = new Map();
            this.loadFromStorage();
        }

        loadFromStorage() {
            try {
                const raw = sessionStorage.getItem(CONFIG.cacheStorageKey);
                if (raw) {
                    const parsed = JSON.parse(raw);
                    if (parsed && typeof parsed === 'object') {
                        for (const [key, value] of Object.entries(parsed)) {
                            this.memoryCache.set(key, value);
                        }
                    }
                }
            } catch (err) {
                log('Failed to load cache from sessionStorage:', err);
            }
        }

        saveToStorage() {
            try {
                if (this.memoryCache.size > CONFIG.maxCacheEntries) {
                    const keys = Array.from(this.memoryCache.keys());
                    const toDelete = keys.slice(0, keys.length - CONFIG.maxCacheEntries);
                    for (const k of toDelete) {
                        this.memoryCache.delete(k);
                    }
                }
                const obj = Object.fromEntries(this.memoryCache.entries());
                sessionStorage.setItem(CONFIG.cacheStorageKey, JSON.stringify(obj));
            } catch (err) {
                log('Failed to save cache to sessionStorage:', err);
            }
        }

        normalizeKey(urlOrId) {
            if (!urlOrId) return '';
            try {
                const parsed = new URL(urlOrId, window.location.origin);
                return parsed.pathname.replace(/\/+$/, '');
            } catch {
                return String(urlOrId).trim();
            }
        }

        get(urlOrId) {
            const key = this.normalizeKey(urlOrId);
            return this.memoryCache.get(key) || null;
        }

        set(urlOrId, postData) {
            if (!urlOrId || !postData) return;
            const key = this.normalizeKey(urlOrId);
            this.memoryCache.set(key, postData);
            this.saveToStorage();
        }
    }

    const postCache = new PostCache();

    const SVG_ICONS = {
        arrowLeft: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>`,
        arrowRight: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>`,
        clock: `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>`,
        noPreview: `<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>`
    };

    function isPostPage() {
        const path = window.location.pathname;
        const matchesPostPath = /\/post\/[^/?#]+/.test(path);
        const hasPostElement = Boolean(document.querySelector('.site-section--post, #page[data-service]'));
        return matchesPostPath || hasPostElement;
    }

    function isPostListPage() {
        const path = window.location.pathname;
        return /\/[^/]+\/user\/[^/]+(?:\/)?$/.test(path) && !/\/post\//.test(path);
    }

    function formatPublishedDate(rawDate) {
        if (!rawDate) return '';
        const trimmed = rawDate.replace(/Published:\s*/i, '').trim();
        const isoMatch = trimmed.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})(?::\d{2})?/);
        if (isoMatch) {
            return `${isoMatch[1]} ${isoMatch[2]}`;
        }
        return trimmed;
    }

    function sanitizeText(text) {
        if (!text) return '';
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }

    function isSidebarOpen() {
        const sidebar = document.querySelector('.global-sidebar');
        const header = document.querySelector('.header');
        const wrapper = document.querySelector('.content-wrapper');

        const isRetracted = sidebar?.classList.contains('retracted') ||
                            header?.classList.contains('sidebar-retracted');
        if (isRetracted) return false;

        const isShifted = wrapper?.classList.contains('shifted');
        if (isShifted) return true;

        if (sidebar) {
            const rect = sidebar.getBoundingClientRect();
            if (rect.right > 50 && rect.left >= -10) return true;
        }

        return false;
    }

    function updateSidebarPosition() {
        const prevCard = document.getElementById('pawchive-card-prev');
        if (!prevCard) return;

        const open = isSidebarOpen();
        const targetLeft = open ? `calc(${CONFIG.sidebarWidthRem}rem + 12px)` : '12px';
        prevCard.style.left = targetLeft;
        log('Sidebar position updated. Open:', open, 'Left:', targetLeft);
    }

    function indexPostListIfPresent() {
        const postCards = document.querySelectorAll('article.post-card');
        if (!postCards.length) return;

        log(`Indexing ${postCards.length} posts from current list page...`);
        postCards.forEach(card => {
            const id = card.getAttribute('data-id');
            const link = card.querySelector('a.image-link, a[href*="/post/"]');
            if (!link) return;

            const url = link.href;
            const titleEl = card.querySelector('.post-card__header');
            const imgEl = card.querySelector('.post-card__image');
            const timeEl = card.querySelector('time.timestamp');

            const title = titleEl ? titleEl.textContent.trim() : '';
            const thumbnail = imgEl ? (imgEl.getAttribute('data-src') || imgEl.src || '') : '';
            const published = timeEl ? (timeEl.getAttribute('datetime') || timeEl.textContent.trim()) : '';

            postCache.set(url, {
                id: id || '',
                title: title || 'Untitled Post',
                thumbnail,
                published: formatPublishedDate(published),
                url
            });
        });
    }

    function indexCurrentPost() {
        if (!isPostPage()) return;

        const postSection = document.querySelector('.site-section--post, #page');
        const id = postSection?.getAttribute('data-id') || window.location.pathname.split('/').filter(Boolean).pop();
        const titleEl = document.querySelector('.post__title span') || document.querySelector('.post__title');
        const pubMeta = document.querySelector('meta[name="published"]');
        const pubEl = document.querySelector('.post__published');
        const imgEl = document.querySelector('.post__thumbnail img, .post__files img, .post__content img');

        let title = titleEl ? titleEl.textContent.trim() : '';
        if (!title) {
            title = document.title.replace(/\s*\|\s*Pawchive$/i, '').trim();
        }

        const published = pubMeta?.content || (pubEl ? pubEl.textContent : '');
        const thumbnail = imgEl ? (imgEl.getAttribute('data-src') || imgEl.src || '') : '';

        postCache.set(window.location.href, {
            id,
            title: title || 'Untitled Post',
            thumbnail,
            published: formatPublishedDate(published),
            url: window.location.href
        });
    }

    function getAdjacentNavLinks() {
        const prevLink = document.querySelector('a.post__nav-link.prev');
        const nextLink = document.querySelector('a.post__nav-link.next');

        return {
            prevUrl: prevLink && prevLink.href ? prevLink.href : null,
            nextUrl: nextLink && nextLink.href ? nextLink.href : null
        };
    }

    let activeAbortController = null;

    async function fetchPostMetadata(url, signal) {
        if (!url) return null;

        const cached = postCache.get(url);
        if (cached) {
            log('Cache hit for:', url);
            return cached;
        }

        log('Fetching metadata for:', url);
        try {
            const timeoutSignal = AbortSignal.timeout(CONFIG.requestTimeoutMs);
            const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;

            const response = await fetch(url, {
                method: 'GET',
                headers: {
                    'Accept': 'text/html,application/xhtml+xml,application/xml'
                },
                credentials: 'same-origin',
                signal: combinedSignal
            });

            if (!response.ok) {
                throw new Error(`HTTP error ${response.status}`);
            }

            const html = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');

            const id = doc.querySelector('meta[name="id"]')?.content ||
                       doc.querySelector('.site-section--post')?.getAttribute('data-id') ||
                       url.split('/').filter(Boolean).pop();

            let title = doc.querySelector('.post__title span')?.textContent?.trim() ||
                        doc.querySelector('.post__title')?.textContent?.trim();
            if (!title) {
                const ogTitle = doc.querySelector('meta[property="og:title"]')?.content;
                if (ogTitle) {
                    const m = ogTitle.match(/^"([^"]+)"/);
                    title = m ? m[1] : ogTitle;
                } else {
                    title = doc.title.replace(/\s*\|\s*Pawchive$/i, '').trim();
                }
            }

            const pubMeta = doc.querySelector('meta[name="published"]')?.content;
            const pubDiv = doc.querySelector('.post__published')?.textContent;
            const timeTag = doc.querySelector('time.timestamp')?.getAttribute('datetime') ||
                            doc.querySelector('time.timestamp')?.textContent;
            const published = pubMeta || pubDiv || timeTag || '';

            let thumbnail = '';
            const thumbImg = doc.querySelector('.post__thumbnail img, .post__files img, .post__attachment img, .fileThumb img, .post__content img');
            if (thumbImg) {
                thumbnail = thumbImg.getAttribute('data-src') || thumbImg.getAttribute('src') || '';
            }

            if (thumbnail) {
                try {
                    thumbnail = new URL(thumbnail, window.location.origin).href;
                } catch {
                }
            }

            const postData = {
                id,
                title: title || 'Untitled Post',
                thumbnail,
                published: formatPublishedDate(published),
                url
            };

            postCache.set(url, postData);
            return postData;
        } catch (err) {
            if (err.name === 'AbortError') {
                log('Fetch aborted for', url);
                return null;
            }
            logError('Failed to fetch post metadata for', url, err);
            return {
                id: url.split('/').filter(Boolean).pop(),
                title: 'View Post',
                thumbnail: '',
                published: '',
                url
            };
        }
    }

    function injectStyles() {
        if (document.getElementById('pawchive-navigator-styles')) return;

        const style = document.createElement('style');
        style.id = 'pawchive-navigator-styles';
        style.textContent = `
            .pawchive-nav-card {
                position: fixed;
                bottom: 110px;
                width: ${CONFIG.cardWidthPx}px;
                height: ${CONFIG.cardHeightPx}px;
                background: rgba(28, 30, 32, 0.94);
                border: 1px solid rgba(255, 255, 255, 0.12);
                border-radius: 8px;
                box-shadow: 0 4px 18px rgba(0, 0, 0, 0.6);
                backdrop-filter: blur(8px);
                -webkit-backdrop-filter: blur(8px);
                display: flex;
                flex-direction: column;
                text-decoration: none;
                color: #e0e0e0;
                z-index: 900;
                user-select: none;
                cursor: pointer;
                box-sizing: border-box;
                transition: left 0.25s ease-in-out, transform 0.2s cubic-bezier(0.16, 1, 0.3, 1),
                            border-color 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
            }

            .pawchive-nav-card:hover {
                transform: translateY(-2px);
                border-color: #ffd05b;
                box-shadow: 0 8px 24px rgba(0, 0, 0, 0.75), 0 0 10px rgba(255, 208, 91, 0.25);
            }

            .pawchive-nav-card.pawchive-prev {
                left: 12px;
            }

            body:has(.content-wrapper.shifted):not(:has(.header.sidebar-retracted)):not(:has(.global-sidebar.retracted)) .pawchive-nav-card.pawchive-prev {
                left: calc(${CONFIG.sidebarWidthRem}rem + 12px);
            }

            body:has(.header.sidebar-retracted) .pawchive-nav-card.pawchive-prev,
            body:has(.global-sidebar.retracted) .pawchive-nav-card.pawchive-prev,
            body:not(:has(.content-wrapper.shifted)) .pawchive-nav-card.pawchive-prev {
                left: 12px;
            }

            .pawchive-nav-card.pawchive-next {
                right: 12px;
            }

            .pawchive-nav-tooltip {
                position: absolute;
                top: -30px;
                display: inline-flex;
                align-items: center;
                gap: 5px;
                padding: 4px 10px;
                background: #181a1b;
                color: #ffd05b;
                border: 1px solid rgba(255, 208, 91, 0.38);
                border-radius: 6px;
                font-size: 11px;
                font-weight: 600;
                letter-spacing: 0.2px;
                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
                pointer-events: none;
                opacity: 0;
                transform: translateY(4px);
                transition: opacity 0.2s ease, transform 0.2s ease;
                white-space: nowrap;
                z-index: 910;
            }

            .pawchive-prev .pawchive-nav-tooltip {
                left: 0;
            }

            .pawchive-next .pawchive-nav-tooltip {
                right: 0;
            }

            .pawchive-nav-card:hover .pawchive-nav-tooltip {
                opacity: 1;
                transform: translateY(0);
            }

            .pawchive-nav-body {
                display: flex;
                flex-direction: row;
                align-items: stretch;
                width: 100%;
                height: 100%;
                overflow: hidden;
                border-radius: 7px;
            }

            .pawchive-nav-text-col {
                flex: 1;
                min-width: 0;
                padding: 8px 10px;
                display: flex;
                flex-direction: column;
                justify-content: space-between;
                box-sizing: border-box;
                background: transparent;
            }

            .pawchive-nav-title {
                font-size: 12px;
                font-weight: 600;
                line-height: 1.35;
                color: #f0f2f5;
                margin: 0;
                display: -webkit-box;
                -webkit-line-clamp: 2;
                -webkit-box-orient: vertical;
                overflow: hidden;
                text-overflow: ellipsis;
                word-break: break-word;
            }

            .pawchive-nav-card:hover .pawchive-nav-title {
                color: #ffffff;
            }

            .pawchive-nav-date {
                font-size: 10.5px;
                color: #8f959c;
                margin-top: 4px;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
                display: flex;
                align-items: center;
                gap: 4px;
            }

            .pawchive-nav-date svg {
                opacity: 0.7;
                flex-shrink: 0;
            }

            .pawchive-nav-thumb-col {
                width: ${CONFIG.thumbWidthPx}px;
                height: 100%;
                flex-shrink: 0;
                overflow: hidden;
                position: relative;
                background: #141617;
                display: flex;
                align-items: center;
                justify-content: center;
            }

            .pawchive-nav-thumb {
                width: 100%;
                height: 100%;
                object-fit: cover;
                display: block;
                transition: transform 0.3s ease;
            }

            .pawchive-nav-card:hover .pawchive-nav-thumb {
                transform: scale(1.05);
            }

            .pawchive-no-preview {
                width: 100%;
                height: 100%;
                display: flex;
                flex-direction: column;
                align-items: center;
                justify-content: center;
                gap: 4px;
                background: #151718;
                color: #71767c;
                font-size: 9.5px;
                font-weight: 600;
                text-transform: uppercase;
                letter-spacing: 0.4px;
                user-select: none;
                padding: 4px;
                box-sizing: border-box;
                text-align: center;
            }

            .pawchive-no-preview svg {
                opacity: 0.55;
            }

            .pawchive-nav-skeleton .pawchive-nav-title {
                background: linear-gradient(90deg, #25282a 25%, #34383c 50%, #25282a 75%);
                background-size: 200% 100%;
                animation: pawNavShimmer 1.5s infinite;
                height: 24px;
                border-radius: 4px;
                color: transparent;
            }

            .pawchive-nav-skeleton .pawchive-nav-date {
                background: linear-gradient(90deg, #25282a 25%, #34383c 50%, #25282a 75%);
                background-size: 200% 100%;
                animation: pawNavShimmer 1.5s infinite;
                height: 12px;
                width: 65%;
                border-radius: 3px;
                color: transparent;
            }

            @keyframes pawNavShimmer {
                0% { background-position: 200% 0; }
                100% { background-position: -200% 0; }
            }

            @media (max-width: 900px) {
                .pawchive-nav-card {
                    width: 220px;
                    height: 80px;
                    bottom: 80px;
                }
                .pawchive-nav-thumb-col {
                    width: 72px;
                }
                .pawchive-nav-title {
                    font-size: 11px;
                }
                .pawchive-nav-date {
                    font-size: 9.5px;
                }
            }

            @media (max-width: 520px) {
                .pawchive-nav-card {
                    display: none !important;
                }
            }
        `;
        document.head.appendChild(style);
    }

    function createThumbnailElement(thumbnailUrl, altText) {
        const thumbCol = document.createElement('div');
        thumbCol.className = 'pawchive-nav-thumb-col';

        if (thumbnailUrl) {
            const img = document.createElement('img');
            img.className = 'pawchive-nav-thumb';
            img.src = thumbnailUrl;
            img.alt = altText || 'Post thumbnail';
            img.loading = 'lazy';
            img.onerror = () => {
                thumbCol.innerHTML = `
                    <div class="pawchive-no-preview">
                        ${SVG_ICONS.noPreview}
                        <span>No Preview</span>
                    </div>
                `;
            };
            thumbCol.appendChild(img);
        } else {
            thumbCol.innerHTML = `
                <div class="pawchive-no-preview">
                    ${SVG_ICONS.noPreview}
                    <span>No Preview</span>
                </div>
            `;
        }

        return thumbCol;
    }

    function createTextElement(title, published) {
        const textCol = document.createElement('div');
        textCol.className = 'pawchive-nav-text-col';

        const titleDiv = document.createElement('div');
        titleDiv.className = 'pawchive-nav-title';
        titleDiv.title = title || '';
        titleDiv.textContent = title || 'Loading...';

        const dateDiv = document.createElement('div');
        dateDiv.className = 'pawchive-nav-date';
        if (published) {
            dateDiv.innerHTML = `${SVG_ICONS.clock} <span>${sanitizeText(published)}</span>`;
        } else {
            dateDiv.innerHTML = `${SVG_ICONS.clock} <span>Date unavailable</span>`;
        }

        textCol.appendChild(titleDiv);
        textCol.appendChild(dateDiv);
        return textCol;
    }

    function createCardElement(type, url, postData) {
        const isPrev = type === 'prev';
        const card = document.createElement('a');
        card.id = isPrev ? 'pawchive-card-prev' : 'pawchive-card-next';
        card.className = `pawchive-nav-card ${isPrev ? 'pawchive-prev' : 'pawchive-next'}`;
        card.href = url;
        card.title = isPrev ? 'Go to Previous Post' : 'Go to Next post';

        const tooltip = document.createElement('div');
        tooltip.className = 'pawchive-nav-tooltip';
        tooltip.innerHTML = isPrev
            ? `${SVG_ICONS.arrowLeft} <span>Go to Previous Post</span>`
            : `<span>Go to Next post</span> ${SVG_ICONS.arrowRight}`;
        card.appendChild(tooltip);

        const body = document.createElement('div');
        body.className = 'pawchive-nav-body';

        if (!postData) {
            card.classList.add('pawchive-nav-skeleton');
            const textCol = createTextElement('Loading post...', '');
            const thumbCol = createThumbnailElement('', '');

            if (isPrev) {
                body.appendChild(textCol);
                body.appendChild(thumbCol);
            } else {
                body.appendChild(thumbCol);
                body.appendChild(textCol);
            }
        } else {
            const textCol = createTextElement(postData.title, postData.published);
            const thumbCol = createThumbnailElement(postData.thumbnail, postData.title);

            if (isPrev) {
                body.appendChild(textCol);
                body.appendChild(thumbCol);
            } else {
                body.appendChild(thumbCol);
                body.appendChild(textCol);
            }
        }

        card.appendChild(body);
        return card;
    }

    function updateCardContent(card, type, postData) {
        if (!card || !postData) return;
        const isPrev = type === 'prev';

        card.classList.remove('pawchive-nav-skeleton');
        card.href = postData.url;

        const body = card.querySelector('.pawchive-nav-body');
        if (!body) return;
        body.innerHTML = '';

        const textCol = createTextElement(postData.title, postData.published);
        const thumbCol = createThumbnailElement(postData.thumbnail, postData.title);

        if (isPrev) {
            body.appendChild(textCol);
            body.appendChild(thumbCol);
        } else {
            body.appendChild(thumbCol);
            body.appendChild(textCol);
        }
    }

    function removeCards() {
        document.getElementById('pawchive-card-prev')?.remove();
        document.getElementById('pawchive-card-next')?.remove();
    }

    async function updateNavigator() {
        if (!isPostPage()) {
            removeCards();
            if (isPostListPage()) {
                indexPostListIfPresent();
            }
            return;
        }

        injectStyles();
        indexCurrentPost();

        const { prevUrl, nextUrl } = getAdjacentNavLinks();
        log('Adjacent links detected:', { prevUrl, nextUrl });

        if (!prevUrl && !nextUrl) {
            removeCards();
            return;
        }

        if (activeAbortController) {
            activeAbortController.abort();
        }
        activeAbortController = new AbortController();
        const { signal } = activeAbortController;

        const container = document.body;

        let prevCard = document.getElementById('pawchive-card-prev');
        if (prevUrl) {
            const cachedPrev = postCache.get(prevUrl);
            if (!prevCard) {
                prevCard = createCardElement('prev', prevUrl, cachedPrev);
                container.appendChild(prevCard);
            } else {
                prevCard.href = prevUrl;
                if (cachedPrev) updateCardContent(prevCard, 'prev', cachedPrev);
            }

            updateSidebarPosition();

            if (!cachedPrev) {
                fetchPostMetadata(prevUrl, signal).then(data => {
                    if (data && prevCard && prevCard.href === data.url) {
                        updateCardContent(prevCard, 'prev', data);
                        updateSidebarPosition();
                    }
                });
            }
        } else if (prevCard) {
            prevCard.remove();
        }

        let nextCard = document.getElementById('pawchive-card-next');
        if (nextUrl) {
            const cachedNext = postCache.get(nextUrl);
            if (!nextCard) {
                nextCard = createCardElement('next', nextUrl, cachedNext);
                container.appendChild(nextCard);
            } else {
                nextCard.href = nextUrl;
                if (cachedNext) updateCardContent(nextCard, 'next', cachedNext);
            }

            if (!cachedNext) {
                fetchPostMetadata(nextUrl, signal).then(data => {
                    if (data && nextCard && nextCard.href === data.url) {
                        updateCardContent(nextCard, 'next', data);
                    }
                });
            }
        } else if (nextCard) {
            nextCard.remove();
        }
    }

    let debounceTimer = null;
    function scheduleUpdate() {
        if (debounceTimer) clearTimeout(debounceTimer);
        debounceTimer = setTimeout(() => {
            updateNavigator();
        }, 50);
    }

    function initObservers() {
        document.body.addEventListener('htmx:afterSettle', () => {
            scheduleUpdate();
            updateSidebarPosition();
        });
        document.body.addEventListener('htmx:afterSwap', () => {
            scheduleUpdate();
            updateSidebarPosition();
        });
        document.body.addEventListener('htmx:historyRestore', () => {
            scheduleUpdate();
            updateSidebarPosition();
        });

        window.addEventListener('popstate', scheduleUpdate);

        const originalPushState = history.pushState;
        if (originalPushState) {
            history.pushState = function (...args) {
                const result = originalPushState.apply(this, args);
                scheduleUpdate();
                return result;
            };
        }

        const originalReplaceState = history.replaceState;
        if (originalReplaceState) {
            history.replaceState = function (...args) {
                const result = originalReplaceState.apply(this, args);
                scheduleUpdate();
                return result;
            };
        }

        document.addEventListener('click', (e) => {
            const target = e.target;
            if (target && target.closest && target.closest('#burgor, .close-sidebar, .backdrop, .global-sidebar-entry-item')) {
                requestAnimationFrame(updateSidebarPosition);
                setTimeout(updateSidebarPosition, 60);
                setTimeout(updateSidebarPosition, 270);
            }
        }, { passive: true });

        window.addEventListener('resize', updateSidebarPosition, { passive: true });

        const observer = new MutationObserver(mutations => {
            let shouldUpdateCards = false;
            let shouldUpdateSidebar = false;

            for (const mutation of mutations) {
                if (mutation.type === 'childList') {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            if (node.id === 'main' || node.classList?.contains('site-section--post') || node.querySelector?.('.site-section--post')) {
                                shouldUpdateCards = true;
                                break;
                            }
                        }
                    }
                } else if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
                    const target = mutation.target;
                    if (target.classList?.contains('global-sidebar') ||
                        target.classList?.contains('content-wrapper') ||
                        target.classList?.contains('header')) {
                        shouldUpdateSidebar = true;
                    }
                }
            }

            if (shouldUpdateCards) scheduleUpdate();
            if (shouldUpdateSidebar) updateSidebarPosition();
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['class']
        });
    }

    function init() {
        log('Initializing Pawchive Post Navigator...');
        initObservers();
        scheduleUpdate();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init, { once: true });
    } else {
        init();
    }
})();