From e6afccdf79e15140a7a1cfacbe549dd6f5189b5c Mon Sep 17 00:00:00 2001 From: yaroslav Date: Wed, 25 Jun 2025 20:41:58 +0300 Subject: [PATCH 1/2] [image-retry] --- Gemfile | 1 + Gemfile.lock | 3 + _layouts/base.html | 1 + _layouts/post.html | 14 +- _plugins/image_url_processor.rb | 36 +++++ _sass/_archive.scss | 29 ++-- _sass/_base.scss | 3 + _sass/_images.scss | 66 +++++++++ _sass/_layouts.scss | 3 + _sass/_mixins.scss | 2 + _sass/_posts.scss | 24 ++-- _sass/_travel-home.scss | 25 ++-- assets/js/image-retry.js | 230 ++++++++++++++++++++++++++++++++ css/main.scss | 22 +-- 14 files changed, 401 insertions(+), 58 deletions(-) create mode 100644 _plugins/image_url_processor.rb create mode 100644 assets/js/image-retry.js diff --git a/Gemfile b/Gemfile index 1e664f2..157e681 100644 --- a/Gemfile +++ b/Gemfile @@ -21,6 +21,7 @@ end gem "sassc", "~> 2.4" gem "image_optim", "~> 0.31" gem "image_optim_pack", "~> 0.10" +gem "faraday-retry", "~> 2.2" # Development and deployment gem "jgd", "~> 1.14" diff --git a/Gemfile.lock b/Gemfile.lock index 8dc114a..9dc85df 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -21,6 +21,8 @@ GEM logger faraday-net_http (3.4.0) net-http (>= 0.5.0) + faraday-retry (2.3.2) + faraday (~> 2.0) ffi (1.17.2) ffi (1.17.2-x64-mingw-ucrt) ffi (1.17.2-x86_64-linux-gnu) @@ -195,6 +197,7 @@ PLATFORMS DEPENDENCIES csv + faraday-retry (~> 2.2) http_parser.rb (~> 0.6.0) image_optim (~> 0.31) image_optim_pack (~> 0.10) diff --git a/_layouts/base.html b/_layouts/base.html index d9eda7b..830c8ce 100644 --- a/_layouts/base.html +++ b/_layouts/base.html @@ -19,6 +19,7 @@ + diff --git a/_layouts/post.html b/_layouts/post.html index 701a9bd..bc2261c 100644 --- a/_layouts/post.html +++ b/_layouts/post.html @@ -2,6 +2,7 @@ layout: base --- +

{{ page.title }}

@@ -33,16 +34,3 @@

{{ page.title }}

{% endif %}
- diff --git a/_plugins/image_url_processor.rb b/_plugins/image_url_processor.rb new file mode 100644 index 0000000..6e1a4af --- /dev/null +++ b/_plugins/image_url_processor.rb @@ -0,0 +1,36 @@ +Jekyll::Hooks.register :posts, :post_render do |post| + # Only process travel posts that have storage_prefix + if post.url.start_with?('/travel/') && post.data['storage_prefix'] + + storage_prefix = post.data['storage_prefix'] + site_data = post.site.data['site'] + + if site_data && site_data['image_storage'] && site_data['image_storage']['prefixes'] + prefixes = site_data['image_storage']['prefixes'] + + if prefixes[storage_prefix] && !prefixes[storage_prefix].empty? + prefix = prefixes[storage_prefix].gsub(':slug', post.data['slug'] || '') + + # Process HTML content + post.output = post.output.gsub(/]*\s+)?src="([^"]+)"([^>]*)>/i) do |match| + before = $1 || '' + img_url = $2 + after = $3 || '' + + # Skip if already absolute URL or assets + if img_url.match?(/^https?:\/\//) || img_url.start_with?('/assets/') + match + else + # Keep the full path, don't strip subdirectories + # This preserves paths like "08/DSC_0972.JPG" + new_url = prefix + img_url + + puts "Processing image in #{post.url}: #{img_url} -> #{new_url}" + + "" + end + end + end + end + end +end \ No newline at end of file diff --git a/_sass/_archive.scss b/_sass/_archive.scss index 4fb87ce..7ca8d2a 100644 --- a/_sass/_archive.scss +++ b/_sass/_archive.scss @@ -1,3 +1,6 @@ +@use 'sass:color'; +@use 'variables' as *; +@use 'mixins' as *; // Archive pages styles .archive-page { @@ -20,20 +23,20 @@ font-size: 1.8rem; margin-bottom: $spacing-base; padding-bottom: 8px; - border-bottom: 2px solid lighten($primary-color, 70%); + border-bottom: 2px solid color.adjust($primary-color, $lightness: 70%); a { color: $text-color; text-decoration: none; &:hover { - color: lighten($text-color, 20%); + color: color.adjust($text-color, $lightness: 20%); } } .post-count { font-size: 0.8em; - color: lighten($text-color, 40%); + color: color.adjust($text-color, $lightness: 40%); font-weight: normal; } } @@ -45,9 +48,9 @@ .post-item { padding: 15px; - border: 1px solid lighten($primary-color, 80%); + border: 1px solid color.adjust($primary-color, $lightness: 80%); border-radius: 6px; - background: lighten($background-color, 2%); + background: color.adjust($background-color, $lightness: 2%); transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; &:hover { @@ -57,7 +60,7 @@ .post-date { font-size: 0.9rem; - color: lighten($text-color, 30%); + color: color.adjust($text-color, $lightness: 30%); font-weight: 500; } @@ -70,13 +73,13 @@ text-decoration: none; &:hover { - color: lighten($text-color, 20%); + color: color.adjust($text-color, $lightness: 20%); } } } .post-description { - color: lighten($text-color, 15%); + color: color.adjust($text-color, $lightness: 15%); line-height: 1.4; margin: 8px 0; } @@ -99,20 +102,20 @@ } .tag { - background: lighten($primary-color, 85%); - color: darken($primary-color, 10%); + background: color.adjust($primary-color, $lightness: 85%); + color: color.adjust($primary-color, $lightness: -10%); &:hover { - background: lighten($primary-color, 75%); + background: color.adjust($primary-color, $lightness: 75%); } } .category { - background: lighten($primary-color, 90%); + background: color.adjust($primary-color, $lightness: 90%); color: $primary-color; &:hover { - background: lighten($primary-color, 80%); + background: color.adjust($primary-color, $lightness: 80%); } } } \ No newline at end of file diff --git a/_sass/_base.scss b/_sass/_base.scss index d290f81..15ef42d 100644 --- a/_sass/_base.scss +++ b/_sass/_base.scss @@ -1,4 +1,7 @@ +@use 'variables' as *; +@use 'mixins' as *; + // Base HTML elements html { background-color: $background-color; diff --git a/_sass/_images.scss b/_sass/_images.scss index 36476e9..1c0e018 100644 --- a/_sass/_images.scss +++ b/_sass/_images.scss @@ -1,4 +1,7 @@ +@use 'sass:color'; +@use 'variables' as *; + // Image handling and gallery styles img { position: relative; @@ -63,4 +66,67 @@ img[data-src] { &.loaded { filter: blur(0); } +} + +// Image loading states +img { + &.loading { + opacity: 0.7; + background: color.adjust($background-color, $lightness: -5%); + } + + &.error { + opacity: 0.5; + background: color.adjust($primary-color, $lightness: 90%); + position: relative; + } + + &.retry { + animation: pulse 1.5s infinite; + } +} + +@keyframes pulse { + 0% { opacity: 0.5; } + 50% { opacity: 0.8; } + 100% { opacity: 0.5; } +} + +// Error placeholder +.image-error-placeholder { + display: inline-block; + background: color.adjust($primary-color, $lightness: 95%); + border: 2px dashed color.adjust($primary-color, $lightness: 70%); + border-radius: 8px; + padding: $spacing-base; + text-align: center; + color: color.adjust($text-color, $lightness: 30%); + font-size: 0.9rem; + min-height: 100px; + width: 100%; + max-width: 400px; + + &::before { + content: "📷"; + display: block; + font-size: 2rem; + margin-bottom: 8px; + } + + .retry-button { + display: inline-block; + margin-top: 8px; + padding: 4px 12px; + background: $primary-color; + color: $background-color; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.8rem; + transition: background-color 0.2s ease; + + &:hover { + background: color.adjust($primary-color, $lightness: -10%); + } + } } \ No newline at end of file diff --git a/_sass/_layouts.scss b/_sass/_layouts.scss index be9f4ca..f0d7ee5 100644 --- a/_sass/_layouts.scss +++ b/_sass/_layouts.scss @@ -1,4 +1,7 @@ +@use 'variables' as *; +@use 'mixins' as *; + // Default layout .default-layout { @include centered-content; diff --git a/_sass/_mixins.scss b/_sass/_mixins.scss index 8379e17..808c211 100644 --- a/_sass/_mixins.scss +++ b/_sass/_mixins.scss @@ -1,4 +1,6 @@ +@use 'variables' as *; + // Typography mixins @mixin font-smoothing { -webkit-font-smoothing: antialiased; diff --git a/_sass/_posts.scss b/_sass/_posts.scss index 13ef931..8e8ca82 100644 --- a/_sass/_posts.scss +++ b/_sass/_posts.scss @@ -1,14 +1,18 @@ +@use 'sass:color'; +@use 'variables' as *; +@use 'mixins' as *; + // Post metadata styles .post-meta { text-align: center; margin-bottom: $spacing-base * 1.5; padding-bottom: $spacing-base; - border-bottom: 1px solid lighten($primary-color, 85%); + border-bottom: 1px solid color.adjust($primary-color, $lightness: 85%); time { display: block; font-size: 0.9rem; - color: lighten($text-color, 30%); + color: color.adjust($text-color, $lightness: 30%); margin-bottom: 10px; } @@ -27,20 +31,20 @@ } .category { - background: lighten($primary-color, 90%); + background: color.adjust($primary-color, $lightness: 90%); color: $primary-color; &:hover { - background: lighten($primary-color, 80%); + background: color.adjust($primary-color, $lightness: 80%); } } .tag { - background: lighten($primary-color, 85%); - color: darken($primary-color, 10%); + background: color.adjust($primary-color, $lightness: 85%); + color: color.adjust($primary-color, $lightness: -10%); &:hover { - background: lighten($primary-color, 75%); + background: color.adjust($primary-color, $lightness: 75%); } } } @@ -51,7 +55,7 @@ text-decoration: none; margin: 0 auto; padding: $spacing-base; - border-bottom: 1px solid lighten($primary-color, 85%); + border-bottom: 1px solid color.adjust($primary-color, $lightness: 85%); display: block; --document-column-size: #{$max-content-width}; @@ -184,8 +188,8 @@ blockquote { font-size: 1.2rem; margin-left: 40px; padding-left: $spacing-base; - border-left: 3px solid lighten($primary-color, 70%); - color: lighten($text-color, 10%); + border-left: 3px solid color.adjust($primary-color, $lightness: 70%); + color: color.adjust($text-color, $lightness: 10%); @include mobile { font-size: 1.1rem; diff --git a/_sass/_travel-home.scss b/_sass/_travel-home.scss index c556eb1..abba1ba 100644 --- a/_sass/_travel-home.scss +++ b/_sass/_travel-home.scss @@ -1,3 +1,6 @@ +@use 'sass:color'; +@use 'variables' as *; +@use 'mixins' as *; // Travel home page container .travel-home-container { @@ -110,7 +113,7 @@ } .post-card-description { - color: lighten($text-color, 15%); + color: color.adjust($text-color, $lightness: 15%); font-size: 0.95rem; line-height: 1.4; margin: 0 0 16px 0; @@ -132,7 +135,7 @@ .post-card-date { font-size: 0.85rem; - color: lighten($text-color, 40%); + color: color.adjust($text-color, $lightness: 40%); font-weight: 500; } @@ -145,7 +148,7 @@ .post-card-category { font-size: 0.75rem; padding: 4px 8px; - background: lighten($primary-color, 90%); + background: color.adjust($primary-color, $lightness: 90%); color: $primary-color; border-radius: 12px; font-weight: 500; @@ -180,7 +183,7 @@ align-items: center; justify-content: center; padding: 10px 16px; - border: 1px solid lighten($primary-color, 80%); + border: 1px solid color.adjust($primary-color, $lightness: 80%); border-radius: 6px; text-decoration: none; color: $text-color; @@ -195,9 +198,9 @@ } &:hover { - background: lighten($primary-color, 95%); - border-color: lighten($primary-color, 70%); - color: darken($text-color, 10%); + background: color.adjust($primary-color, $lightness: 95%); + border-color: color.adjust($primary-color, $lightness: 70%); + color: color.adjust($text-color, $lightness: -10%); } } @@ -207,8 +210,8 @@ border-color: $primary-color; &:hover { - background: darken($primary-color, 5%); - border-color: darken($primary-color, 5%); + background: color.adjust($primary-color, $lightness: -5%); + border-color: color.adjust($primary-color, $lightness: -5%); color: $background-color; } } @@ -252,7 +255,7 @@ align-items: center; justify-content: center; padding: 10px 8px; - color: lighten($text-color, 40%); + color: color.adjust($text-color, $lightness: 40%); @include mobile { padding: 8px 6px; @@ -262,7 +265,7 @@ .pagination-info { margin-top: $spacing-base; font-size: 0.9rem; - color: lighten($text-color, 30%); + color: color.adjust($text-color, $lightness: 30%); @include mobile { font-size: 0.85rem; diff --git a/assets/js/image-retry.js b/assets/js/image-retry.js new file mode 100644 index 0000000..8e92b84 --- /dev/null +++ b/assets/js/image-retry.js @@ -0,0 +1,230 @@ +/** + * Image Auto-Retry System + * Automatically retries failed image loads with exponential backoff + */ + +class ImageRetrySystem { + constructor(options = {}) { + this.maxRetries = options.maxRetries || 3; + this.baseDelay = options.baseDelay || 1000; // 1 second + this.maxDelay = options.maxDelay || 10000; // 10 seconds + this.retryAttempts = new Map(); + + this.init(); + } + + init() { + // Handle existing images + this.setupImageErrorHandlers(); + + // Handle dynamically added images + this.observeNewImages(); + + console.log('Image retry system initialized'); + } + + setupImageErrorHandlers() { + const images = document.querySelectorAll('img'); + images.forEach(img => this.attachErrorHandler(img)); + } + + attachErrorHandler(img) { + // Skip if already handled + if (img.dataset.retryHandled) return; + + // Mark as handled + img.dataset.retryHandled = 'true'; + + // Only add loading class if image hasn't loaded yet + if (!img.complete) { + img.classList.add('loading'); + } + + // Handle successful load + img.addEventListener('load', () => { + img.classList.remove('loading', 'error', 'retry'); + this.retryAttempts.delete(img.src); + }); + + // Handle error + img.addEventListener('error', (e) => { + this.handleImageError(img); + }); + + // If image is already in error state, handle it + if (img.complete && img.naturalWidth === 0) { + this.handleImageError(img); + } + } + + handleImageError(img) { + const src = img.src; + const currentAttempts = this.retryAttempts.get(src) || 0; + + console.log(`Image load failed: ${src} (attempt ${currentAttempts + 1})`); + + img.classList.remove('loading'); + img.classList.add('error'); + + if (currentAttempts < this.maxRetries) { + this.scheduleRetry(img, currentAttempts); + } else { + this.handlePermanentFailure(img); + } + } + + scheduleRetry(img, attemptCount) { + const delay = Math.min( + this.baseDelay * Math.pow(2, attemptCount), + this.maxDelay + ); + + console.log(`Scheduling retry for ${img.src} in ${delay}ms`); + + // Update retry count + this.retryAttempts.set(img.src, attemptCount + 1); + + // Add retry animation + img.classList.add('retry'); + + setTimeout(() => { + this.retryImage(img); + }, delay); + } + + retryImage(img) { + console.log(`Retrying image: ${img.src}`); + + // Get the base URL without query parameters + let baseUrl = img.src.split('?')[0]; + + // If this was already a retry attempt, make sure we have the correct base URL + if (img.dataset.originalSrc && window._imagePrefix) { + // Reconstruct the correct URL from the original filename + baseUrl = window._imagePrefix + img.dataset.originalSrc; + } + + const cacheBuster = Date.now(); + const newSrc = `${baseUrl}?retry=${cacheBuster}`; + + console.log(`Retry URL: ${newSrc}`); + + img.classList.remove('error', 'retry'); + img.classList.add('loading'); + + // Force reload with cache buster + img.src = newSrc; + } + + handlePermanentFailure(img) { + console.warn(`Image permanently failed after ${this.maxRetries} attempts: ${img.src}`); + + img.classList.remove('retry'); + img.classList.add('error'); + + // Create fallback placeholder + this.createErrorPlaceholder(img); + } + + createErrorPlaceholder(img) { + const placeholder = document.createElement('div'); + placeholder.className = 'image-error-placeholder'; + + // Store original filename if available + if (img.dataset.originalSrc) { + placeholder.dataset.originalSrc = img.dataset.originalSrc; + } + + placeholder.innerHTML = ` +
Image failed to load
+ + `; + + // Replace image with placeholder + img.parentNode.replaceChild(placeholder, img); + } + + manualRetry(originalSrc, button) { + console.log(`Manual retry requested for: ${originalSrc}`); + + // Extract the base URL without retry parameters + const baseUrl = originalSrc.split('?')[0]; + + // Reset retry counter for the base URL + this.retryAttempts.delete(baseUrl); + this.retryAttempts.delete(originalSrc); + + // Create new image element + const img = document.createElement('img'); + img.classList.add('loading'); + + // If we have the original filename stored, use it + const placeholder = button.parentNode; + const originalFilename = placeholder.dataset.originalSrc; + if (originalFilename && window._imagePrefix) { + img.dataset.originalSrc = originalFilename; + img.src = window._imagePrefix + originalFilename; + } else { + img.src = baseUrl; + } + + console.log(`Manual retry with URL: ${img.src}`); + + // Attach error handler + this.attachErrorHandler(img); + + // Replace placeholder with new image + placeholder.parentNode.replaceChild(img, placeholder); + } + + observeNewImages() { + // Watch for dynamically added images + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === 1) { // Element node + if (node.tagName === 'IMG') { + this.attachErrorHandler(node); + } else { + // Check for images within added nodes + const images = node.querySelectorAll?.('img') || []; + images.forEach(img => this.attachErrorHandler(img)); + } + } + }); + }); + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + } +} + +// Initialize the retry system when DOM is ready +let imageRetrySystem; + +function initRetrySystem() { + if (!imageRetrySystem) { + imageRetrySystem = new ImageRetrySystem({ + maxRetries: 3, + baseDelay: 1500, + maxDelay: 8000 + }); + + // Make it globally available + window.imageRetrySystem = imageRetrySystem; + } +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initRetrySystem); +} else { + initRetrySystem(); +} + +// Export for global access +window.imageRetrySystem = imageRetrySystem; \ No newline at end of file diff --git a/css/main.scss b/css/main.scss index 0f48d78..bf430c3 100644 --- a/css/main.scss +++ b/css/main.scss @@ -1,14 +1,14 @@ --- --- -@import 'variables'; -@import 'mixins'; -@import 'title-fonts'; -@import 'home-button'; -@import 'country-flags'; -@import 'base'; -@import 'posts'; -@import 'travel-home'; -@import 'images'; -@import 'archive'; -@import 'layouts'; \ No newline at end of file +@use 'variables' as *; +@use 'mixins' as *; +@use 'title-fonts'; +@use 'home-button'; +@use 'country-flags'; +@use 'base'; +@use 'posts'; +@use 'travel-home'; +@use 'images'; +@use 'archive'; +@use 'layouts'; \ No newline at end of file From f71340c5469bf91c1d4ce7887c3bcd6e856e50d5 Mon Sep 17 00:00:00 2001 From: yaroslav Date: Wed, 25 Jun 2025 21:00:34 +0300 Subject: [PATCH 2/2] [image-retry] fix local image loading in travel home --- _layouts/travel-home.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/_layouts/travel-home.html b/_layouts/travel-home.html index 1aa28a8..e1f85f7 100644 --- a/_layouts/travel-home.html +++ b/_layouts/travel-home.html @@ -22,8 +22,15 @@

{{ page.title }}