Created
November 8, 2025 17:29
-
-
Save verfasor/971416bb9fc8f04bc947530dc1ac08be to your computer and use it in GitHub Desktop.
bear blog bulk canonical URL updater script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Bear Blog Bulk Canonical URL Updater | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.0 | |
| // @description Bulk update canonical URLs for Bear Blog posts to https://yourdomain.com/{slug} | |
| // @author Mighil.com | |
| // @match https://bearblog.dev/* | |
| // @match https://*.bearblog.dev/*/dashboard/posts* | |
| // @grant none | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| const DOMAIN = 'https://yourdomain.com'; | |
| let postUrls = []; | |
| let currentIndex = 0; | |
| let isProcessing = false; | |
| // Function to extract post edit URLs from the posts list page | |
| function extractPostUrls() { | |
| const postLinks = document.querySelectorAll('.post-list a[href*="/dashboard/posts/"]'); | |
| const urls = Array.from(postLinks).map(link => { | |
| const href = link.getAttribute('href'); | |
| // Convert relative URLs to absolute | |
| if (href.startsWith('/')) { | |
| return window.location.origin + href; | |
| } | |
| return href; | |
| }); | |
| return urls.filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates | |
| } | |
| // Function to extract slug from header content | |
| function extractSlug(headerText) { | |
| const lines = headerText.split('\n'); | |
| for (const line of lines) { | |
| if (line.trim().toLowerCase().startsWith('link:')) { | |
| const slug = line.split(':')[1].trim(); | |
| return slug; | |
| } | |
| } | |
| return null; | |
| } | |
| // Function to update canonical URL in header content | |
| function updateCanonicalUrl(headerText, slug) { | |
| const lines = headerText.split('\n'); | |
| const newCanonicalUrl = `${DOMAIN}/${slug}`; | |
| let canonicalFound = false; | |
| let updatedLines = []; | |
| for (let i = 0; i < lines.length; i++) { | |
| const line = lines[i].trim(); | |
| if (line.toLowerCase().startsWith('canonical_url:')) { | |
| // Update existing canonical_url | |
| updatedLines.push(`canonical_url: ${newCanonicalUrl}`); | |
| canonicalFound = true; | |
| } else { | |
| updatedLines.push(lines[i]); | |
| } | |
| } | |
| // If canonical_url not found, add it after link: | |
| if (!canonicalFound) { | |
| const finalLines = []; | |
| for (let i = 0; i < updatedLines.length; i++) { | |
| finalLines.push(updatedLines[i]); | |
| // Insert canonical_url after link: line | |
| if (updatedLines[i].trim().toLowerCase().startsWith('link:')) { | |
| finalLines.push(`canonical_url: ${newCanonicalUrl}`); | |
| } | |
| } | |
| return finalLines.join('\n'); | |
| } | |
| return updatedLines.join('\n'); | |
| } | |
| // Function to process all posts | |
| async function processAllPosts() { | |
| if (isProcessing) { | |
| alert('Already processing posts. Please wait...'); | |
| return; | |
| } | |
| isProcessing = true; | |
| currentIndex = 0; | |
| // Check if we're on the posts list page | |
| if (window.location.pathname.includes('/dashboard/posts') && | |
| !window.location.pathname.match(/\/dashboard\/posts\/[^\/]+\/?$/)) { | |
| // Extract post URLs | |
| postUrls = extractPostUrls(); | |
| if (postUrls.length === 0) { | |
| alert('No posts found on this page.'); | |
| isProcessing = false; | |
| return; | |
| } | |
| const totalPosts = postUrls.length; | |
| const confirmed = confirm(`Found ${totalPosts} posts. This will update canonical URLs to ${DOMAIN}/{slug} for each post. Continue?`); | |
| if (!confirmed) { | |
| isProcessing = false; | |
| return; | |
| } | |
| // Store URLs in sessionStorage for navigation | |
| sessionStorage.setItem('bulkCanonicalProcessing', 'true'); | |
| sessionStorage.setItem('bulkCanonicalUrls', JSON.stringify(postUrls)); | |
| sessionStorage.setItem('bulkCanonicalCurrentIndex', '0'); | |
| // Start processing first post | |
| window.location.href = postUrls[0]; | |
| } else if (window.location.pathname.match(/\/dashboard\/posts\/[^\/]+\/?$/)) { | |
| // We're on an individual post edit page | |
| // Check if we should process this post | |
| if (sessionStorage.getItem('bulkCanonicalProcessing') === 'true') { | |
| const postUrls = JSON.parse(sessionStorage.getItem('bulkCanonicalUrls') || '[]'); | |
| const currentUrlIndex = parseInt(sessionStorage.getItem('bulkCanonicalCurrentIndex') || '0'); | |
| console.log(`🔍 On edit page. Processing post ${currentUrlIndex + 1}/${postUrls.length}`); | |
| // Wait for page to fully load, then process | |
| let attempts = 0; | |
| const maxAttempts = 25; // 5 seconds max | |
| const checkReady = setInterval(() => { | |
| attempts++; | |
| const headerContent = document.getElementById('header_content'); | |
| const form = document.querySelector('form.post-form'); | |
| const hiddenHeaderContent = document.getElementById('hidden_header_content'); | |
| if (headerContent && form && hiddenHeaderContent) { | |
| clearInterval(checkReady); | |
| console.log(`✓ Page ready. Starting processing...`); | |
| // Small additional delay to ensure everything is ready | |
| setTimeout(() => { | |
| processCurrentPost(postUrls, currentUrlIndex); | |
| }, 800); | |
| } else if (attempts >= maxAttempts) { | |
| clearInterval(checkReady); | |
| console.error('⚠ Timeout waiting for page elements'); | |
| // Try to continue anyway | |
| setTimeout(() => { | |
| processCurrentPost(postUrls, currentUrlIndex); | |
| }, 500); | |
| } | |
| }, 200); | |
| } | |
| } | |
| } | |
| // Function to process the current post on the edit page | |
| function processCurrentPost(postUrls, currentUrlIndex) { | |
| // Check if we just saved this post (page reloaded after save) | |
| const justSaved = sessionStorage.getItem('bulkCanonicalJustSaved') === 'true'; | |
| if (justSaved) { | |
| // We just saved, now move to next post | |
| sessionStorage.removeItem('bulkCanonicalJustSaved'); | |
| console.log(`✓ Post ${currentUrlIndex + 1} saved successfully. Moving to next...`); | |
| // Small delay before moving to next | |
| setTimeout(() => { | |
| moveToNextPost(postUrls, currentUrlIndex); | |
| }, 500); | |
| return; | |
| } | |
| const headerContent = document.getElementById('header_content'); | |
| const form = document.querySelector('form.post-form'); | |
| const hiddenHeaderContent = document.getElementById('hidden_header_content'); | |
| if (!headerContent || !form || !hiddenHeaderContent) { | |
| console.error('Could not find required elements. Retrying...'); | |
| // Retry after a short delay | |
| setTimeout(() => { | |
| processCurrentPost(postUrls, currentUrlIndex); | |
| }, 1000); | |
| return; | |
| } | |
| // Extract slug | |
| const headerText = headerContent.innerText; | |
| const slug = extractSlug(headerText); | |
| if (!slug) { | |
| console.warn(`⚠ Could not extract slug from post. Skipping...`); | |
| moveToNextPost(postUrls, currentUrlIndex); | |
| return; | |
| } | |
| // Update canonical URL (always override existing canonical_url) | |
| const updatedHeader = updateCanonicalUrl(headerText, slug); | |
| const expectedCanonical = `${DOMAIN}/${slug}`; | |
| console.log(`📝 Processing post ${currentUrlIndex + 1}/${postUrls.length}: ${slug}`); | |
| console.log(` Setting canonical_url to: ${expectedCanonical}`); | |
| // Update the header content in the contenteditable div | |
| headerContent.innerText = updatedHeader; | |
| // Update hidden input (this is what Bear Blog uses for form submission) | |
| hiddenHeaderContent.value = updatedHeader; | |
| // Mark that we're about to save | |
| sessionStorage.setItem('bulkCanonicalJustSaved', 'true'); | |
| // Trigger form submission - Bear Blog's form handler will intercept and submit | |
| // We need to dispatch a submit event to trigger Bear Blog's handler | |
| const submitEvent = new Event('submit', { bubbles: true, cancelable: true }); | |
| form.dispatchEvent(submitEvent); | |
| // Fallback: if the event doesn't work, try direct submit after a small delay | |
| setTimeout(() => { | |
| if (sessionStorage.getItem('bulkCanonicalJustSaved') === 'true') { | |
| // Still marked as "just saved" means form didn't submit, try direct submit | |
| console.log('Direct form submit fallback...'); | |
| form.submit(); | |
| } | |
| }, 1000); | |
| } | |
| // Function to move to the next post | |
| function moveToNextPost(postUrls, currentUrlIndex) { | |
| const nextIndex = currentUrlIndex + 1; | |
| if (nextIndex >= postUrls.length) { | |
| // Finished processing all posts | |
| sessionStorage.removeItem('bulkCanonicalProcessing'); | |
| sessionStorage.removeItem('bulkCanonicalUrls'); | |
| sessionStorage.removeItem('bulkCanonicalCurrentIndex'); | |
| alert(`Finished processing ${postUrls.length} posts!`); | |
| // Redirect back to posts list | |
| const pathParts = window.location.pathname.split('/').filter(p => p); | |
| // Remove the last two parts (uid and empty string) to get back to posts list | |
| const postsListPath = '/' + pathParts.slice(0, -1).join('/') + '/'; | |
| window.location.href = window.location.origin + postsListPath; | |
| return; | |
| } | |
| // Update index and navigate to next post | |
| sessionStorage.setItem('bulkCanonicalCurrentIndex', nextIndex.toString()); | |
| window.location.href = postUrls[nextIndex]; | |
| } | |
| // Add button to posts list page | |
| function addControlButton() { | |
| // Check if we're on the posts list page | |
| if (window.location.pathname.includes('/dashboard/posts') && | |
| !window.location.pathname.match(/\/dashboard\/posts\/[^\/]+$/)) { | |
| // Check if button already exists | |
| if (document.getElementById('bulk-canonical-btn')) { | |
| return; | |
| } | |
| const button = document.createElement('button'); | |
| button.id = 'bulk-canonical-btn'; | |
| button.textContent = 'Bulk Update Canonical URLs'; | |
| button.style.cssText = ` | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| z-index: 10000; | |
| padding: 10px 20px; | |
| background-color: #4CAF50; | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.2); | |
| `; | |
| button.onclick = (e) => { | |
| e.preventDefault(); | |
| processAllPosts(); | |
| }; | |
| document.body.appendChild(button); | |
| } | |
| } | |
| // Initialize when page loads | |
| function initialize() { | |
| addControlButton(); | |
| // Also check if we should continue processing | |
| if (sessionStorage.getItem('bulkCanonicalProcessing') === 'true') { | |
| processAllPosts(); | |
| } | |
| } | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', initialize); | |
| } else { | |
| initialize(); | |
| } | |
| // Also check after navigation (for SPA-like behavior) | |
| const observer = new MutationObserver(() => { | |
| addControlButton(); | |
| }); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| // Run processAllPosts on every page load to continue processing | |
| window.addEventListener('load', () => { | |
| if (sessionStorage.getItem('bulkCanonicalProcessing') === 'true') { | |
| setTimeout(() => { | |
| processAllPosts(); | |
| }, 1000); | |
| } | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment