☰

Pawchive Next/Previous Post Navigator

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

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Violentmonkey installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Violentmonkey installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Userscripts installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey installieren.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

(Ich habe bereits einen Benutzerstil Verwaltung, ich möchte ihn installieren!)

// ==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();
    }
})();