Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save verfasor/971416bb9fc8f04bc947530dc1ac08be to your computer and use it in GitHub Desktop.

Select an option

Save verfasor/971416bb9fc8f04bc947530dc1ac08be to your computer and use it in GitHub Desktop.
bear blog bulk canonical URL updater script
// ==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