Skip to content
  • intro
  • inside belarus: soviet memory & national identity
  • inside transnistria: soviet memory & national identity
  • ukraine: sun, sea & sandbags
  • trans-siberian world cup
  • king charles III coronation
  • mourning queen elizabeth II
  • queen's platinum jubilee
  • the english summer season
  • boardmasters festival
  • sun, sea & covid-19
  • lockdown fanatical football fans
  • lockdown: tale of the tape
  • a1: britain on the verge
  • ford car fans
  • dench does dallas
  • football's hidden story
  • the british abroad
  • england uncensored
  • alcohol & england
  • advertising & commercial
  • portraits
  • singles
  • films
  • tearsheets
  • written articles
  • shop
  • abu dhabi corniche
  • arab london
  • day off in the lives of europe
  • dench does lockdown
  • fashion faces
  • fashionUK
  • female fitness uk
  • freshers
  • great british bed + breakfast
  • heatwave uk
  • household cavalry
  • italy in decline
  • laiba & robyn
  • last resort revisited
  • leicester city fc
  • liverpool fc training at melwood
  • match of the day at 50
  • miami
  • royalUK : william & kate
  • royalUK : harry & meghan
  • schooldisco.com
  • sport in osire refugee camp
  • trawlermen
  • whitby goth festival
  • zabaleen of garbage city
  • biography
  • contact
Loading Gallery...
`; container.appendChild(loadingIndicator); } catch (error) { console.error('EnhancedGallerySystem: Error preparing container:', error); } } setupGalleryParameters(gallery, galleryInstanceId) { // Set up gallery parameters with enhanced uniqueness window.Parameters = window.Parameters || {}; // Clone and extend existing Parameters to avoid losing important values const existingParams = { ...window.Parameters }; window.Parameters = { ...existingParams, SiteAlias: window.location.hostname, InitialPageUuid: gallery.pageId, InitialPageAlias: gallery.slug || this.slugify(gallery.title), // CRITICAL FIX: Set isInEditor to true if user is logged in as admin OR in edit mode // This ensures gallery files bypass cache for any logged-in admin, not just when edit mode is active isInEditor: window.isEditing || localStorage.getItem('hydra_is_admin') === 'true' || window.isAdmin === true || document.body.classList.contains('hydra-admin'), siteId: window.siteId || gallery.siteId || '', isHydra: true, // CRITICAL: These properties help the gallery script find the container galleryContainerId: 'neon-gallery-container', containerSelector: '#neon-gallery-container', // Additional properties galleryInstanceId: galleryInstanceId, galleryUniqueId: `gallery-container-${gallery.id}-${galleryInstanceId}`, loadTimestamp: galleryInstanceId, loadedGalleryId: gallery.id }; // Set up gallery config - CRITICAL: Include all permanent storage fields const galleryOptions = gallery.galleryOptions || {}; window.neonGalleryConfig = { useData: true, useCDN: true, version: 'live', manualCollectionName: galleryOptions.manualCollectionName || "mod", layoutType: galleryOptions.layoutType || "grid", ...galleryOptions, siteId: window.siteId || gallery.siteId || '', galleryId: gallery.id, galleryInstanceId: galleryInstanceId, // Add container selector here too containerSelector: '#neon-gallery-container', // CRITICAL: Explicitly include all permanent storage fields to ensure they're available // These are needed for proper gallery initialization and data loading isPerma: galleryOptions.isPerma, permaURL: galleryOptions.permaURL, loadTxId: galleryOptions.loadTxId, loadPermaURL: galleryOptions.loadPermaURL }; } /** * Show error message in container */ showErrorMessage(container, message) { // CRITICAL: Add null check for container if (!container) { console.error('EnhancedGallerySystem: Container is null in showErrorMessage'); return; } try { // Clear container container.innerHTML = ''; // Create error message element const errorElement = document.createElement('div'); errorElement.className = 'gallery-error'; errorElement.innerHTML = `

Error Loading Gallery

${message}

`; container.appendChild(errorElement); container.style.opacity = '1'; } catch (error) { console.error('EnhancedGallerySystem: Error showing error message:', error); } } /** * Utility function to convert title to slug */ slugify(text) { return text .toLowerCase() .replace(/s+/g, '-') .replace(/[^w-]+/g, '') .replace(/--+/g, '-') .trim(); } } // Create a singleton instance window.EnhancedGallerySystem = window.EnhancedGallerySystem || new EnhancedGallerySystem(); /** * Enhanced loadGallery function using the EnhancedGallerySystem * This replaces the existing loadGallery function */ const enhancedLoadGallery = async function(id, event, options) { const originalLoadGallery = window.originalLoadGallery; const forceGalleryReload = options?.forceReload === true || window._forceGalleryReload === true; stopAutoAdvanceTimer(); if (event) { event.preventDefault(); event.stopPropagation(); } let galleryContainer = document.querySelector('.gallery-container'); if (!galleryContainer) { galleryContainer = document.getElementById('galleryFrame')?.parentElement || document.getElementById('main-content') || document.getElementById('content'); if (!galleryContainer) { console.error('Gallery container not found, falling back to original function'); if (typeof originalLoadGallery === 'function') { return originalLoadGallery(id, event, options); } else { console.error('Gallery container not found and no fallback available'); return; } } } // Bail out before hiding/clearing when the same gallery is already loading or rendered. // Post-save passes { forceReload: true } to bypass this path without affecting navigation. const egEarly = window.EnhancedGallerySystem; if (egEarly && !forceGalleryReload) { if (egEarly._loadInProgress && egEarly._loadingGalleryId === id) { console.log('enhancedLoadGallery: awaiting in-flight load for gallery', id); await egEarly._loadInProgress; if (galleryContainer) galleryContainer.style.opacity = '1'; return; } const hasRenderedGallery = !!document.querySelector( '.gallery-wrapper .masonry-item, .gallery-wrapper .horizontal-scroller, .gallery-wrapper .masonry' ); if (hasRenderedGallery && egEarly.currentGalleryId === id) { console.log('enhancedLoadGallery: gallery already rendered, skipping reload'); if (galleryContainer) galleryContainer.style.opacity = '1'; if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { window.syncGalleryOptionsButtonVisibility(); } return; } } else if (forceGalleryReload) { console.log('enhancedLoadGallery: force reload requested, bypassing already-rendered skip'); if (egEarly?.lastInitializationTime?.delete) { egEarly.lastInitializationTime.delete(id); } if (egEarly) { egEarly._loadInProgress = null; egEarly._loadingGalleryId = null; } } if (galleryContainer) { galleryContainer.style.opacity = '0'; } console.log('Loading gallery with ID (enhancedLoadGallery):', id); const gallery = findGalleryById(galleries, id); if (!gallery) { console.warn('No gallery found with ID:', id); if (galleryContainer) { galleryContainer.innerHTML = `

Gallery Not Found

The requested gallery could not be found.

`; galleryContainer.style.opacity = '1'; } return; } if (isPageMenuItem(gallery)) { return typeof window.loadPage === 'function' ? window.loadPage(id, event) : loadPage(id, event); } window._activeContentType = 'gallery'; if (gallery.isSubmenu) { console.log('Gallery is submenu, skipping URL update'); toggleSubmenu(id, event || { stopPropagation: () => {} }); if (galleryContainer){ galleryContainer.style.opacity = '1'; } return; } console.log('Found gallery:', gallery.title); // Check for gallery-level password protection (only for visitors, not in edit mode) if (!isInEditMode() && gallery.passwordProtected) { // Use PageManager's gallery password protection methods if (window.PageManager && typeof window.PageManager.checkGalleryPasswordAccess === 'function' && typeof window.PageManager.createGalleryPasswordOverlay === 'function') { if (!window.PageManager.checkGalleryPasswordAccess(gallery)) { console.log('Gallery is password protected, showing overlay'); window.PageManager.createGalleryPasswordOverlay(gallery, (accessGranted) => { if (accessGranted) { // Reload the gallery now that access is granted enhancedLoadGallery(id, event); } }); return; // Exit early, will reload after password is entered } } } window.activeGalleryId = id; activeGalleryId = id; // CRITICAL: Clean up page-embedded gallery scripts/DOM before loading a standalone gallery. // loadPage() calls removeGalleryScriptsWithPause which can interrupt EnhancedGallerySystem.initialize(), // leaving isLoading:true with a never-resolving initPromise. Re-run cleanup here (matching original // loadGallery) and clear the container so page content does not block gallery rendering. if (typeof window.removeGalleryScriptsWithPause === 'function') { await window.removeGalleryScriptsWithPause(); } if (galleryContainer) { galleryContainer.innerHTML = ''; } const bodyEl = document.body; if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { bodyEl.classList.add('menu-hidden-on-page'); } else { bodyEl.classList.remove('menu-hidden-on-page'); } try { if (typeof updateActiveStates === 'function') updateActiveStates(); if (typeof updateMobileTitle === 'function') updateMobileTitle(); if (typeof closeMobileMenu === 'function') closeMobileMenu(); // Close the gallery options panel when navigating to a different gallery if (typeof window.closeOptionsPanel === 'function' && !forceGalleryReload) { window.closeOptionsPanel(); } const staleOptionsOverlay = document.getElementById('optionsOverlay'); if (staleOptionsOverlay) { staleOptionsOverlay.remove(); } window.optionsVisible = false; if (typeof updateURLWithGallerySlug === 'function') { updateURLWithGallerySlug(gallery); } } catch (uiError) { console.error('Error updating UI state:', uiError); } try { // Add detailed logging about EnhancedGallerySystem availability console.log('🔍 Checking EnhancedGallerySystem availability:', { exists: !!window.EnhancedGallerySystem, hasLoadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function'), initialized: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.initialized), isLoading: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.isLoading) }); if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function') { console.log('✅ EnhancedGallerySystem available, calling loadGallery'); const result = await window.EnhancedGallerySystem.loadGallery( gallery, galleryContainer, forceGalleryReload ? { forceReload: true } : options ); console.log('📊 EnhancedGallerySystem.loadGallery result:', result); if (!result && typeof originalLoadGallery === 'function') { console.log('⚠️ Enhanced gallery loading failed, falling back to original function'); return originalLoadGallery(id, event, options); } if (galleryContainer) { galleryContainer.style.opacity = '1'; } if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { window.syncGalleryOptionsButtonVisibility(); setTimeout(window.syncGalleryOptionsButtonVisibility, 350); } } else { console.error('❌ EnhancedGallerySystem or its loadGallery method is not available.', { EnhancedGallerySystem: !!window.EnhancedGallerySystem, loadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function'), useCdnProxy: window.useCdnProxy || false }); // Fallback: Try using createGalleryContext directly console.log('🔄 Attempting fallback: createGalleryContext'); if (typeof createGalleryContext === 'function') { createGalleryContext(gallery, galleryContainer); if (galleryContainer) { galleryContainer.style.opacity = '1'; } return; } if (galleryContainer) { galleryContainer.innerHTML = `
Gallery system not available. EnhancedGallerySystem: ${!!window.EnhancedGallerySystem}, loadGallery: ${!!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function')}
`; galleryContainer.style.opacity = '1'; } return; } } catch (error) { console.error('Error loading gallery:', error); if (galleryContainer) { try { galleryContainer.innerHTML = `

Error Loading Gallery

There was a problem loading the gallery. Please try again later.

${error.message}

`; galleryContainer.style.opacity = '1'; } catch (containerError) { console.error('Error showing error message in container:', containerError); } } if (typeof originalLoadGallery === 'function') { console.log('Falling back to original loadGallery after error'); return originalLoadGallery(id, event, options); } } }; /** * Initialization function to set up the gallery system */ function initializeGallerySystem() { console.log('Initializing enhanced gallery system'); // Store reference to original function before replacing if (typeof window.loadGallery === 'function') { window.originalLoadGallery = window.loadGallery; } // Replace the existing loadGallery function with our optimized version window.loadGallery = enhancedLoadGallery; // Add this CSS for loading/error states addGallerySystemStyles(); // Preload styles only at boot — scripts load when first gallery opens (with container ready) window.EnhancedGallerySystem.preloadStylesOnly().catch(error => { console.warn('Gallery styles preloading failed:', error); }); } /** * Add CSS styles for loading indicators and error messages */ function addGallerySystemStyles() { const styleId = 'enhanced-gallery-system-styles'; // Skip if already added if (document.getElementById(styleId)) { return; } const style = document.createElement('style'); style.id = styleId; style.textContent = ` /* Loading indicator */ .gallery-loading { display: flex; flex-direction: column; justify-content: center; align-items: center; width: 100%; height: 100%; min-height: 200px; opacity: 0 !important; } .gallery-loading .spinner { width: 50px; height: 50px; border: 3px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-top-color: #4682B4; animation: spin 1s ease-in-out infinite; margin-bottom: 20px; opacity: 0 !important; } @keyframes spin { to { transform: rotate(360deg); } } .gallery-loading .loading-text { font-family: Arial, sans-serif; font-size: 16px; color: #555; opacity: 0; } /* Error message */ .gallery-error { display: flex; flex-direction: column; justify-content: center; align-items: center; width: 100%; height: 100%; min-height: 200px; padding: 30px; text-align: center; } .gallery-error h3 { font-family: Arial, sans-serif; font-size: 24px; color:rgba(211, 47, 47, 0.25); margin-bottom: 20px; } .gallery-error p { font-family: Arial, sans-serif; font-size: 16px; color: #555; max-width: 600px; margin-bottom: 10px; } .gallery-error .error-details { font-size: 14px; color: #888; font-style: italic; } `; document.head.appendChild(style); } // Initialize gallery system immediately if possible initializeGallerySystem(); })(); /** * Enhanced Gallery Link Interceptor * Handles clicks on gallery links to prevent full page reloads */ (function() { // Store any loaded gallery data for quick reference const GALLERY_DATA_CACHE = { initialized: false, galleries: [], lastUpdate: 0 }; // Initialize when DOM is ready to catch early clicks document.addEventListener('DOMContentLoaded', initGalleryLinkInterceptor); // Also initialize when window loads to ensure complete gallery data window.addEventListener('load', function() { // Use a small delay to ensure galleries have fully initialized setTimeout(initGalleryLinkInterceptor, 200); }); // Additional init on first user interaction document.addEventListener('click', function initOnFirstClick() { initGalleryLinkInterceptor(); // Remove this listener after first click document.removeEventListener('click', initOnFirstClick); }, { once: true }); // Main initialization function function initGalleryLinkInterceptor() { // Prevent multiple initializations if (window._galleryLinkInterceptorInitialized) { // If already initialized, just refresh the gallery data refreshGalleryCache(); return; } window._galleryLinkInterceptorInitialized = true; console.log('Initializing enhanced gallery link interceptor'); // Update our cache with current galleries data refreshGalleryCache(); // Get primary containers that might contain links const galleryContainer = document.querySelector('.gallery-container'); const mainContainer = document.querySelector('.container') || document.body; if (!galleryContainer && !mainContainer) { console.warn('No gallery containers found, link interceptor partially initialized'); } // Add global click handler to catch all link clicks document.addEventListener('click', handleGlobalLinkClick, true); // Try to intercept links from iframes - critical for gallery content interceptAllIframeLinks(); // Set up a mutation observer to handle dynamically added content setupMutationObserver(); // Log initialization with site details const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; console.log(`Gallery link interceptor initialized. Preview mode: ${isPreviewMode}`); // Create global helper for debugging window.debugGalleryLinks = function() { return { inPreviewMode: isPreviewMode, interceptorInitialized: window._galleryLinkInterceptorInitialized, cachedGalleries: GALLERY_DATA_CACHE.galleries.length, urlPath: window.location.pathname, siteGuid: getSiteGuidFromPath() }; }; } // Update the gallery cache when gallery data changes function refreshGalleryCache() { // Initialize GALLERY_DATA_CACHE if it doesn't exist if (!window.GALLERY_DATA_CACHE) { window.GALLERY_DATA_CACHE = { initialized: false, galleries: [], lastUpdate: 0 }; } // Check for global galleries variable first if (typeof galleries !== 'undefined' && Array.isArray(galleries)) { // Use the global galleries array window.GALLERY_DATA_CACHE.galleries = galleries; window.GALLERY_DATA_CACHE.initialized = true; window.GALLERY_DATA_CACHE.lastUpdate = Date.now(); window.galleries = galleries; console.log(`Gallery cache refreshed with ${galleries.length} galleries from global variable`); return; } // Debug log console.log(`Gallery cache refreshed with ${GALLERY_DATA_CACHE.galleries.length} galleries`); } // Global click handler - catches all link clicks function handleGlobalLinkClick(e) { // Find if a link was clicked const link = e.target.closest('a'); if (!link || !link.href) return; // Skip if the link uses special targets or modifiers if (link.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return; // Skip for hash links or javascript: links if (link.href.includes('#') || link.href.startsWith('javascript:')) return; try { // Parse the URL to work with it const url = new URL(link.href); // Only intercept links to the same domain if (url.hostname !== window.location.hostname) return; // Skip API and resource URLs if (url.pathname.includes('/api/') || isResourceUrl(url.pathname)) return; console.log('Link intercepted:', url.pathname); // For preview URLs, we need to handle the path specially const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; const siteGuid = getSiteGuidFromPath(); console.log('Link interceptor - isPreviewMode:', isPreviewMode, 'siteGuid:', siteGuid); // Site Graph card links carry data-sg-target-id — use SPA navigation (not preview iframe fallback) if (link.classList.contains('sg-card-link') && link.closest('.site-graph-element') && link.dataset.sgTargetId) { let galleriesData = (typeof galleries !== 'undefined' && Array.isArray(galleries)) ? galleries : window.galleries; const sgItem = Array.isArray(galleriesData) ? findGalleryById(galleriesData, link.dataset.sgTargetId) : null; if (sgItem) { e.preventDefault(); e.stopPropagation(); if (sgItem.isPage && window.loadPage) { window.loadPage(sgItem.id); } else if (window.loadGallery) { window.loadGallery(sgItem.id); } let targetUrl = url.href; if (isPreviewMode && siteGuid) { const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? url.pathname : `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`; const newUrl = new URL(url); newUrl.pathname = pathname; targetUrl = newUrl.href; } window.history.pushState( { galleryId: sgItem.id }, sgItem.title || document.title, targetUrl ); document.title = formatGalleryDocumentTitle(sgItem.title); window.scrollTo(0, 0); return; } } // Check if this is internal navigation first let matchingItem = findGalleryByPath(url.pathname, isPreviewMode, siteGuid); console.log('Link interceptor - matchingItem found:', !!matchingItem, matchingItem ? matchingItem.title : 'none'); if (matchingItem) { // Prevent default navigation (full page reload) e.preventDefault(); e.stopPropagation(); console.log('Found matching gallery for link:', matchingItem.title); const nestedLinkModeActive = typeof window.isNestedGalleryLinkModeEnabled === 'function' ? window.isNestedGalleryLinkModeEnabled() : !!(window.neonGalleryConfig?.useLinks && window.neonGalleryConfig?.openMultipleLightboxes); const isGridGalleryLink = !!(link.closest('.masonry-item') || (link.closest('.gallery-wrapper, #neon-gallery-container, .gallery-direct-content, .masonry') && link.querySelector('img') && !link.closest('#neon-lightbox, .neon-lightbox'))); if (!matchingItem.isPage && nestedLinkModeActive && isGridGalleryLink && typeof window.openNestedGalleryLightbox === 'function') { void window.openNestedGalleryLightbox(matchingItem, window.activeGalleryId).then((opened) => { if (!opened) { console.warn('Nested gallery lightbox failed — staying on parent gallery (no loadGallery fallback)'); } }); return; } // Use the existing navigation functions if (matchingItem.isPage && window.loadPage) { window.loadPage(matchingItem.id); } else if (window.loadGallery) { window.loadGallery(matchingItem.id); } // Construct proper URL for preview mode let targetUrl = url.href; if (isPreviewMode && siteGuid) { // Ensure we keep the GUID in the URL path const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? url.pathname : `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`; const newUrl = new URL(url); newUrl.pathname = pathname; targetUrl = newUrl.href; } // Update URL without reload window.history.pushState( { galleryId: matchingItem.id }, matchingItem.title || document.title, targetUrl ); // Update page title document.title = formatGalleryDocumentTitle(matchingItem.title); // Scroll to top window.scrollTo(0, 0); return; } // Special case for preview mode - even if no matching gallery found else if (isPreviewMode && siteGuid) { // We're in preview mode with a GUID - intercept the navigation // to maintain the GUID in the URL. Only do this when #galleryFrame // can load content; otherwise allow SPA onclick handlers to run. console.log('No direct match but in preview mode - handling navigation'); const frame = document.getElementById('galleryFrame'); if (!frame) { console.warn('Gallery frame not found - allowing default/SPA handlers'); } else { // Prevent default navigation e.preventDefault(); e.stopPropagation(); // Construct a URL that preserves the GUID let newPathname = url.pathname; if (!newPathname.startsWith(`/${siteGuid}/`)) { // Add the GUID to the path if not already there newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`; } // Create the preview-friendly URL for browser history const browserUrl = new URL(window.location.origin); browserUrl.pathname = newPathname; console.log('Constructing preview URL with GUID:', browserUrl.href); // IMPORTANT: For gallery navigation, we need to create a special URL // that tells the gallery system to load the content directly console.log('Loading preview path in iframe:', newPathname); // First update browser URL to show the correct path window.history.pushState( { customPath: newPathname }, document.title, browserUrl.href ); // Create direct path to the gallery content const cleanPath = newPathname.replace(`/${siteGuid}/`, '/'); // For direct gallery paths, we need to load the gallery with special parameters // This tells the gallery system to load content from this path const galleryUrl = `https://storage.neonsky.app/sites:site_${siteGuid}.json`; // Two approaches to loading the content: // 1. Use the gallery wrapper approach const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + `?siteId=${siteGuid}` + `&path=${encodeURIComponent(cleanPath)}` + `&directPath=true` + `&t=${Date.now()}`; // Cache buster console.log('Loading gallery via wrapper URL:', wrapperUrl); frame.src = wrapperUrl; // Clear any active gallery ID since we're navigating directly if (window.activeGalleryId) { console.log('Clearing active gallery ID during direct navigation'); window.activeGalleryId = null; } return; // Critical: prevent further navigation } } console.log('No matching gallery or special handling - allow normal navigation'); } catch (error) { console.error('Error processing link click:', error); } } // Handle all iframes on the page function interceptAllIframeLinks() { // Look for any iframe that might contain gallery content const iframes = document.querySelectorAll('iframe'); iframes.forEach(iframe => { if (iframe.id === 'galleryFrame' || iframe.classList.contains('gallery-iframe')) { // Primary gallery iframe - high priority setupIframeIntercept(iframe, true); } else { // Any other iframe - still try to intercept setupIframeIntercept(iframe, false); } }); console.log(`Attempted to intercept links in ${iframes.length} iframes`); } // Setup link interception for a specific iframe function setupIframeIntercept(iframe, isPrimary) { try { // Skip if iframe is not fully loaded yet if (!iframe.contentWindow || !iframe.contentDocument) { // Add load event to try again when loaded iframe.addEventListener('load', function() { setupIframeIntercept(iframe, isPrimary); }); return; } // Try to access the iframe content - might fail due to same-origin policy try { const iframeDoc = iframe.contentDocument; const iframeWin = iframe.contentWindow; // Add event listener for all link clicks in the iframe iframeDoc.addEventListener('click', function(e) { const link = e.target.closest('a'); if (!link || !link.href) return; // Skip special cases if (link.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return; if (link.href.includes('#') || link.href.startsWith('javascript:')) return; try { const url = new URL(link.href); // Check if same domain or relative URL const isSameDomain = url.hostname === window.location.hostname || url.hostname === iframe.src.hostname || !url.hostname; if (isSameDomain) { const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; const siteGuid = getSiteGuidFromPath(); // First try to find a matching gallery/page const matchingItem = parent.findGalleryByPath ? parent.findGalleryByPath(url.pathname, isPreviewMode, siteGuid) : findGalleryByPath(url.pathname, isPreviewMode, siteGuid); if (matchingItem) { // We found a match - prevent default and navigate e.preventDefault(); e.stopPropagation(); console.log('Iframe: Found matching content for link:', matchingItem.title); // Access parent window functions if (matchingItem.isPage && parent.loadPage) { parent.loadPage(matchingItem.id); } else if (parent.loadGallery) { parent.loadGallery(matchingItem.id); } // Update URL in parent window let targetUrl = url.href; if (isPreviewMode && siteGuid) { // Ensure GUID is preserved in URL const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? url.pathname : `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`; const newUrl = new URL(window.location.origin + pathname); targetUrl = newUrl.href; } // Update browser URL parent.history.pushState( { galleryId: matchingItem.id }, matchingItem.title || document.title, targetUrl ); // Update parent window title parent.document.title = formatGalleryDocumentTitle(matchingItem.title, parent); // Scroll parent to top parent.scrollTo(0, 0); return; } // Special handling for preview mode - critical fix here! else if (isPreviewMode && siteGuid) { // Special handling for preview URLs e.preventDefault(); e.stopPropagation(); console.log('Iframe: No direct match but in preview mode - handling navigation'); // Construct a URL that preserves the GUID let newPathname = url.pathname; if (!newPathname.startsWith(`/${siteGuid}/`)) { newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`; } // Create a URL for the browser history const browserUrl = new URL(window.location.origin); browserUrl.pathname = newPathname; console.log('Iframe: Constructing preview URL with GUID:', browserUrl.href); // Update browser URL in parent parent.history.pushState( { customPath: newPathname }, document.title, browserUrl.href ); // Handle the navigation in the parent window if (isPrimary && parent.document.getElementById('galleryFrame')) { const galleryFrame = parent.document.getElementById('galleryFrame'); // Create clean path for the gallery system const cleanPath = newPathname.replace(`/${siteGuid}/`, '/'); // Create wrapper URL for gallery content const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + `?siteId=${siteGuid}` + `&path=${encodeURIComponent(cleanPath)}` + `&directPath=true` + `&t=${Date.now()}`; // Cache buster console.log('Iframe: Loading gallery via wrapper URL:', wrapperUrl); galleryFrame.src = wrapperUrl; // Clear any active gallery ID since we're navigating directly if (parent.activeGalleryId) { console.log('Iframe: Clearing active gallery ID during direct navigation'); parent.activeGalleryId = null; } } else { // Fallback - navigate normally in case we can't handle it console.log('Iframe: Gallery frame not found or not primary, navigating normally'); window.location.href = browserUrl.href; } return; } } } catch (iframeError) { console.error('Error handling iframe link:', iframeError); } }); if (isPrimary) { console.log('Successfully intercepted links in primary gallery iframe'); } } catch (sameOriginError) { // This is expected for cross-origin iframes if (isPrimary) { console.warn('Cannot access iframe content due to same-origin policy. Using fallback method.'); // Apply a fallback method - add postMessage listener setupPostMessageListener(iframe); } } } catch (error) { console.warn('Error setting up iframe link interception:', error); } } // Fallback: use postMessage for cross-origin iframes function setupPostMessageListener(iframe) { // Listen for messages from the iframe window.addEventListener('message', function(event) { try { // Verify the message is from our iframe if (event.source !== iframe.contentWindow) return; const data = event.data; // Check if this is a link click event if (data && data.type === 'linkClick' && data.href) { console.log('Received link click message from iframe:', data.href); // Create a URL object to work with the link const url = new URL(data.href); // Check if it's a local navigation if (url.hostname === window.location.hostname) { const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; const siteGuid = getSiteGuidFromPath(); // Try to find a matching gallery const matchingItem = findGalleryByPath(url.pathname, isPreviewMode, siteGuid); if (matchingItem) { console.log('Found matching gallery for postMessage link:', matchingItem.title); // Navigate using the appropriate function if (matchingItem.isPage && window.loadPage) { window.loadPage(matchingItem.id); } else if (window.loadGallery) { window.loadGallery(matchingItem.id); } // Update URL without reload let targetUrl = url.href; if (isPreviewMode && siteGuid) { // Ensure GUID is preserved const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? url.pathname : `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`; const newUrl = new URL(window.location.origin + pathname); targetUrl = newUrl.href; } window.history.pushState( { galleryId: matchingItem.id }, matchingItem.title || document.title, targetUrl ); // Update page title document.title = formatGalleryDocumentTitle(matchingItem.title); return; } // Special handling for preview mode - Fixed! else if (isPreviewMode && siteGuid) { console.log('No direct match but in preview mode - handling navigation via postMessage'); // Construct URL that preserves the GUID let newPathname = url.pathname; if (!newPathname.startsWith(`/${siteGuid}/`)) { newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`; } // Create browser URL const browserUrl = new URL(window.location.origin); browserUrl.pathname = newPathname; // Update browser URL window.history.pushState( { customPath: newPathname }, document.title, browserUrl.href ); // Get the main gallery frame - NOT the iframe that sent the message const galleryFrame = document.getElementById('galleryFrame'); if (galleryFrame) { // Create clean path without GUID const cleanPath = newPathname.replace(`/${siteGuid}/`, '/'); // Create wrapper URL const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + `?siteId=${siteGuid}` + `&path=${encodeURIComponent(cleanPath)}` + `&directPath=true` + `&t=${Date.now()}`; // Cache buster console.log('Loading gallery via wrapper URL (postMessage):', wrapperUrl); galleryFrame.src = wrapperUrl; // Clear any active gallery ID if (window.activeGalleryId) { window.activeGalleryId = null; } return; } else { console.warn('Gallery frame not found - cannot load content via postMessage'); } } } } } catch (error) { console.error('Error processing postMessage from iframe:', error); } }); // Inject a script into the iframe if possible to send messages back try { // We might be able to inject a script tag even in cross-origin scenarios // if the iframe is not yet loaded or using document.write iframe.addEventListener('load', function() { try { const script = iframe.contentDocument.createElement('script'); script.textContent = ` // Link interception script injected from parent document.addEventListener('click', function(e) { const link = e.target.closest('a'); if (link && link.href && !link.target && !e.ctrlKey && !e.metaKey && !e.shiftKey) { // Send message to parent window window.parent.postMessage({ type: 'linkClick', href: link.href, pathname: link.pathname }, '*'); // Prevent default navigation e.preventDefault(); } }); console.log('Gallery link interceptor injected from parent'); `; iframe.contentDocument.head.appendChild(script); } catch (e) { // This will likely fail for cross-origin iframes console.log('Could not inject script into iframe - expected for cross-origin frames'); } }); } catch (error) { console.warn('Error trying to inject script into iframe:', error); } } // Observer for new content/iframes function setupMutationObserver() { if (!window.MutationObserver) return; const observer = new MutationObserver(function(mutations) { let newIframesFound = false; mutations.forEach(function(mutation) { if (mutation.type === 'childList' && mutation.addedNodes.length) { // Check if any new iframes were added mutation.addedNodes.forEach(function(node) { // Check if the node is an iframe or contains iframes if (node.nodeName === 'IFRAME') { newIframesFound = true; setupIframeIntercept(node, node.id === 'galleryFrame'); } else if (node.querySelectorAll) { const iframes = node.querySelectorAll('iframe'); if (iframes.length > 0) { newIframesFound = true; iframes.forEach(function(iframe) { setupIframeIntercept(iframe, iframe.id === 'galleryFrame'); }); } } }); } }); // If new iframes were found, update our cache if (newIframesFound) { refreshGalleryCache(); } }); // Observe the entire document observer.observe(document.body, { childList: true, subtree: true }); } // Helper to check if a URL is a resource rather than a page function isResourceUrl(path) { const resourceExtensions = [ '.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.css', '.js', '.pdf', '.ico', '.ttf', '.woff', '.woff2' ]; return resourceExtensions.some(ext => path.toLowerCase().endsWith(ext)); } // Get the site GUID from the path in preview mode function getSiteGuidFromPath() { if (window.location.hostname === 'preview.neonsky.app') { const pathParts = window.location.pathname.split('/').filter(Boolean); if (pathParts.length > 0) { return pathParts[0]; } } return null; } /** * Enhanced gallery path matching with improved accuracy: * * This function finds the appropriate gallery based on URL path with better * prioritization and more precise matching criteria. * * @param {string} path - The URL path to match * @param {boolean} isPreviewMode - Whether we're in preview mode * @param {string} siteGuid - The site GUID (for preview mode) * @returns {Object|null} The matching gallery or null if none found */ function findGalleryByPath(path, isPreviewMode, siteGuid) { // Ensure path is a string if (typeof path !== 'string') { console.warn('Invalid path provided to findGalleryByPath:', path); return null; } // Determine which galleries array to use - check global variable first let galleriesData; let galleryCount = 0; if (typeof galleries !== 'undefined' && Array.isArray(galleries)) { galleriesData = galleries; galleryCount = galleries.length; } else if (typeof window.galleries !== 'undefined' && Array.isArray(window.galleries)) { galleriesData = window.galleries; galleryCount = window.galleries.length; } else if (window.GALLERY_DATA_CACHE && window.GALLERY_DATA_CACHE.initialized && Array.isArray(window.GALLERY_DATA_CACHE.galleries)) { galleriesData = window.GALLERY_DATA_CACHE.galleries; galleryCount = window.GALLERY_DATA_CACHE.galleries.length; } else { console.warn('No galleries array available'); return null; } console.log(`Searching for match among ${galleryCount} galleries`); // Strip leading/trailing slashes without regex — template-literal embedding mangles / escapes // (e.g. /^/|/$/g becomes /^/|/$/g → 0), which left "/" unnormalized and broke home matching. function stripEdgeSlashes(value) { return String(value || '').split('/').filter(Boolean).join('/'); } // Normalize the path for comparison (handle trailing/leading slashes) let normalizedPath = stripEdgeSlashes(path).toLowerCase(); const originalNormalizedPath = normalizedPath; // Keep original for logging // If this is a preview URL, we need to handle the path differently if (isPreviewMode && siteGuid) { // Check if the path starts with the GUID if (normalizedPath.startsWith(siteGuid.toLowerCase() + '/')) { // Remove the GUID prefix normalizedPath = normalizedPath.substring(siteGuid.length + 1); } else if (normalizedPath === siteGuid.toLowerCase()) { // This is just the GUID with no additional path normalizedPath = ''; } } // Create a dash-normalized version (all sequences of dashes become single dashes) // This handles both double-dash patterns and any other dash inconsistencies const normalizedPathWithSingleDashes = normalizedPath.replace(/-+/g, '-'); console.log(`Normalized path: "${normalizedPath}" (dash-normalized: "${normalizedPathWithSingleDashes}")`); // For empty paths, try to find the homepage if (!normalizedPath) { // Look for a gallery marked as home page const homePage = galleriesData.find(g => g.isHomePage === true); if (homePage) { console.log('Empty path - returning homepage:', homePage.title); return homePage; } } // Array to store potential matches with their confidence levels const potentialMatches = []; // Process each gallery to find potential matches with confidence levels galleriesData.forEach(gallery => { // Skip invalid galleries if (!gallery || !gallery.id) return; // Store matching details for debugging const matchInfo = { gallery, confidence: 0, matchType: null, usedNormalizedDashes: false }; // 1. EXACT SLUG MATCH (highest confidence) if (gallery.slug) { const gallerySlug = stripEdgeSlashes(gallery.slug).toLowerCase(); // Try original path if (gallerySlug === normalizedPath) { matchInfo.confidence = 100; // Max confidence matchInfo.matchType = 'exact_slug'; potentialMatches.push({...matchInfo}); return; // Continue to next gallery } // Try dash-normalized path const gallerySlugWithSingleDashes = gallerySlug.replace(/-+/g, '-'); if (normalizedPathWithSingleDashes === gallerySlugWithSingleDashes) { matchInfo.confidence = 95; // Very high confidence matchInfo.matchType = 'exact_slug_normalized_dashes'; matchInfo.usedNormalizedDashes = true; potentialMatches.push({...matchInfo}); return; // Continue to next gallery } } // 2. EXACT URL PATH MATCH (very high confidence) if (gallery.url) { let galleryPath = gallery.url.toLowerCase(); if (galleryPath.startsWith('http')) { try { const urlObj = new URL(galleryPath); galleryPath = urlObj.pathname; } catch (e) { // If URL parsing fails, use the string as is } } galleryPath = stripEdgeSlashes(galleryPath); // Try original path if (galleryPath === normalizedPath) { matchInfo.confidence = 90; // Very high confidence matchInfo.matchType = 'exact_url_path'; potentialMatches.push({...matchInfo}); return; // Continue to next gallery } // Try dash-normalized path const galleryPathWithSingleDashes = galleryPath.replace(/-+/g, '-'); if (normalizedPathWithSingleDashes === galleryPathWithSingleDashes) { matchInfo.confidence = 85; // High confidence matchInfo.matchType = 'exact_url_path_normalized_dashes'; matchInfo.usedNormalizedDashes = true; potentialMatches.push({...matchInfo}); return; // Continue to next gallery } } // 3. TITLE SLUG MATCH (high confidence) if (gallery.title) { const titleSlug = gallery.title.toLowerCase() .replace(/s+/g, '-') .replace(/[^w-]+/g, '') .replace(/--+/g, '-') .trim(); // Try original path if (titleSlug === normalizedPath) { matchInfo.confidence = 80; // High confidence matchInfo.matchType = 'title_slug_match'; potentialMatches.push({...matchInfo}); return; // Continue to next gallery } // Try dash-normalized path const titleSlugWithSingleDashes = titleSlug.replace(/-+/g, '-'); if (normalizedPathWithSingleDashes === titleSlugWithSingleDashes) { matchInfo.confidence = 75; // Good confidence matchInfo.matchType = 'title_slug_match_normalized_dashes'; matchInfo.usedNormalizedDashes = true; potentialMatches.push({...matchInfo}); return; // Continue to next gallery } } // 4. PARTIAL SLUG MATCH - BUT WITH MUCH STRICTER CRITERIA if (gallery.slug && normalizedPath.length > 5) { const gallerySlug = stripEdgeSlashes(gallery.slug).toLowerCase(); const gallerySlugWithSingleDashes = gallerySlug.replace(/-+/g, '-'); // Only if paths are substantial (to avoid false positives) if (normalizedPath.length >= 5 && gallerySlug.length >= 5) { // Try matching significant portions with dash normalization // If the normalized paths contain each other with substantial overlap if (normalizedPathWithSingleDashes.includes(gallerySlugWithSingleDashes) || gallerySlugWithSingleDashes.includes(normalizedPathWithSingleDashes)) { // Calculate overlap ratio based on the shorter string const overlapRatio = Math.min(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length) / Math.max(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length); // Only consider matches with substantial overlap (over 70%) if (overlapRatio >= 0.7) { const score = Math.round(60 + (overlapRatio * 20)); // 60-80 based on overlap matchInfo.confidence = score; matchInfo.matchType = 'partial_match_normalized'; matchInfo.usedNormalizedDashes = true; matchInfo.overlapRatio = overlapRatio; potentialMatches.push({...matchInfo}); } } } } }); // No potential matches found if (potentialMatches.length === 0) { console.log('No gallery match found for path:', path); return null; } // Sort potential matches by confidence (highest first) potentialMatches.sort((a, b) => b.confidence - a.confidence); // Get the best match const bestMatch = potentialMatches[0]; // Log detailed debugging info about the match console.log(`Best match for "${normalizedPath}":`, { title: bestMatch.gallery.title, id: bestMatch.gallery.id, matchType: bestMatch.matchType, confidence: bestMatch.confidence, usedNormalizedDashes: bestMatch.usedNormalizedDashes, totalMatches: potentialMatches.length }); // If we have multiple close matches, log them for debugging if (potentialMatches.length > 1 && potentialMatches[1].confidence >= bestMatch.confidence - 10) { console.log('Close alternative matches:', potentialMatches.slice(1, 3).map(m => ({ title: m.gallery.title, id: m.gallery.id, matchType: m.matchType, confidence: m.confidence }))); } // Return the best matching gallery return bestMatch.gallery; } // Make helpful functions available globally window.findGalleryByPath = findGalleryByPath; window.findGalleryById = findGalleryById; window.refreshGalleryCache = refreshGalleryCache; window.getSiteGuidFromPath = getSiteGuidFromPath; if (typeof getSiteGalleriesArray === 'function') { window.getSiteGalleriesArray = getSiteGalleriesArray; } })();

Add sidebar element

Select the type of element to add:

Add to menu

Site Metadata

Recommended length: 50-60 characters
Recommended length: 150-160 characters
Recommended about 1200×630px. Optional: when you upload an image here, it is used for link previews site-wide. If you leave this empty, the first image on each page or gallery (including the home page) is used instead.
Example: G-XXXXXXXXXX or UA-XXXXXXXX-X
Enter the Pixel ID from Meta Events Manager. A full Meta Pixel snippet can also be pasted.
Set the primary language of your site content. Chrome will offer to translate if visitor's language is different.
Use this if the site is under development or shouldn't appear in search results

Schema / Identity

Helps Google understand what this site represents — used in Knowledge Panels and local search results.

Select "Person" for individual photographers, artists, consultants. Select "Organization" for studios, agencies, companies.
Used in Google Knowledge Panels and local search (e.g. "photojournalist Minneapolis").
Improves visibility in location-based searches.
Embedded in structured data only — not displayed on the page.
When enabled, the copyright text shows at the bottom of the sidebar.
This text appears in the sidebar footer and inside the copyright overlay.
Upload an image file for your site favicon (recommended: 32x32px or 16x16px, PNG or ICO format)

Custom HTML site (full bypass)

Upload one self-contained HTML file (e.g. from an AI tool). When enabled, visitors to the site root see only that file — not the Hydra menu or gallery. To log in and edit, open /_manage on your domain, then press + (numpad + or Shift+= on US keyboards) to log in or toggle edit mode, then open Site Metadata from the edit bar. To log out in this browser, visit /_manage?logout=1 or /_manage/logout (also works as ?hydra_logout=1 on any page).

No HTML file uploaded yet.

Site-wide password (browser login)

When enabled, visitors must enter this username and password in the browser’s dialog before any page or asset loads (same credentials on your live domain and on preview.neonsky.app/<your-site-id>/). Editors still use Magic login to edit. Turn off to make the site fully public again.

Stored in your site configuration. Use a strong password; anyone with it can view the site.
Label shown in the browser login dialog.
' + '
Preview (32x32px)
' + '' + '' + ''; } // Function to remove the favicon function removeFavicon() { const faviconInput = document.getElementById('faviconUrl'); if (faviconInput) { faviconInput.value = ''; } const container = document.getElementById('faviconPreviewContainer'); if (container) { container.innerHTML = ''; } const btnText = document.getElementById('uploadFaviconBtnText'); if (btnText) { btnText.textContent = 'Upload Favicon'; } console.log('Favicon removed'); } function uploadSiteOgImage() { if (!window.ImageUploader || typeof window.ImageUploader.open !== 'function') { alert('Image uploader not available. Refresh the page and try again.'); return; } try { window.ImageUploader.open({ title: 'Upload social preview image', containerType: 'page', quality: 0.92 }, function(imageResult, imageInfo) { const inp = document.getElementById('ogImageUrl'); const setUrl = function(url) { if (inp) inp.value = url; const btnText = document.getElementById('uploadOgImageBtnText'); if (btnText) btnText.textContent = 'Change image'; showOgImagePreview(url); window.siteMetadata = window.siteMetadata || {}; window.siteMetadata.ogImageUrl = url; }; if (imageInfo && imageInfo.url) { setUrl(imageInfo.url); } else if (imageResult && typeof imageResult === 'string' && imageResult.indexOf('data:') === 0) { setUrl(imageResult); } else { alert('Upload failed. Please try again.'); } }); } catch (error) { console.error('Error opening OG image uploader:', error); alert('Error opening uploader: ' + (error.message || 'Unknown error')); } } function showOgImagePreview(imageUrl) { const container = document.getElementById('ogImagePreviewContainer'); if (!container) return; container.innerHTML = '
' + 'Social preview' + '
' + '
Current social preview image
' + '
This image overrides the first page/gallery image for social previews
' + '
' + '' + '
'; } function removeOgImage() { const inp = document.getElementById('ogImageUrl'); if (inp) inp.value = ''; if (window.siteMetadata) window.siteMetadata.ogImageUrl = ''; const container = document.getElementById('ogImagePreviewContainer'); if (container) container.innerHTML = ''; const btnText = document.getElementById('uploadOgImageBtnText'); if (btnText) btnText.textContent = 'Upload image'; }

Import Galleries from JSON or Classic GUID

Paste the full site JSON into the text area below, OR enter the GUID of a classic gallery collection.

OR
Use a GUID to fetch from classic site, or use import_ format to fetch from R2 storage.
If checked, the current menu will be deleted and replaced with the imported items. If unchecked, imported items will be added to the current menu.
Processing...