|
| 1 | +/** |
| 2 | + * Optimized Image Component |
| 3 | + * |
| 4 | + * This component provides: |
| 5 | + * - Lazy loading for better performance |
| 6 | + * - WebP support with fallbacks |
| 7 | + * - Proper alt text and metadata |
| 8 | + * - Structured data for rich snippets |
| 9 | + * - Responsive image loading |
| 10 | + */ |
| 11 | + |
| 12 | +class OptimizedImage { |
| 13 | + constructor(container, options = {}) { |
| 14 | + this.container = typeof container === 'string' ? document.querySelector(container) : container; |
| 15 | + this.options = { |
| 16 | + lazy: true, |
| 17 | + webp: true, |
| 18 | + responsive: true, |
| 19 | + structuredData: true, |
| 20 | + ...options |
| 21 | + }; |
| 22 | + |
| 23 | + this.init(); |
| 24 | + } |
| 25 | + |
| 26 | + init() { |
| 27 | + if (!this.container) { |
| 28 | + console.error('Container not found'); |
| 29 | + return; |
| 30 | + } |
| 31 | + |
| 32 | + this.loadImageManifest(); |
| 33 | + } |
| 34 | + |
| 35 | + async loadImageManifest() { |
| 36 | + try { |
| 37 | + const response = await fetch('/images/optimized/image-manifest.json'); |
| 38 | + this.manifest = await response.json(); |
| 39 | + this.renderImages(); |
| 40 | + } catch (error) { |
| 41 | + console.warn('Image manifest not found, using fallback rendering'); |
| 42 | + this.renderImagesFallback(); |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + renderImages() { |
| 47 | + const images = this.container.querySelectorAll('img[data-image-key]'); |
| 48 | + |
| 49 | + images.forEach(img => { |
| 50 | + const imageKey = img.getAttribute('data-image-key'); |
| 51 | + const imageData = this.manifest.images.find(img => img.filename === imageKey); |
| 52 | + |
| 53 | + if (imageData) { |
| 54 | + this.optimizeImage(img, imageData); |
| 55 | + } |
| 56 | + }); |
| 57 | + |
| 58 | + if (this.options.structuredData) { |
| 59 | + this.addStructuredData(); |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + renderImagesFallback() { |
| 64 | + const images = this.container.querySelectorAll('img[data-image-key]'); |
| 65 | + |
| 66 | + images.forEach(img => { |
| 67 | + const imageKey = img.getAttribute('data-image-key'); |
| 68 | + this.optimizeImageFallback(img, imageKey); |
| 69 | + }); |
| 70 | + } |
| 71 | + |
| 72 | + optimizeImage(img, imageData) { |
| 73 | + // Set basic attributes |
| 74 | + img.alt = imageData.alt; |
| 75 | + img.title = imageData.title; |
| 76 | + |
| 77 | + // Add loading attribute for lazy loading |
| 78 | + if (this.options.lazy) { |
| 79 | + img.loading = 'lazy'; |
| 80 | + } |
| 81 | + |
| 82 | + // Set src with WebP support |
| 83 | + if (this.options.webp && this.supportsWebP()) { |
| 84 | + img.src = imageData.webpUrl; |
| 85 | + // Fallback for older browsers |
| 86 | + img.onerror = () => { |
| 87 | + img.src = imageData.url; |
| 88 | + }; |
| 89 | + } else { |
| 90 | + img.src = imageData.url; |
| 91 | + } |
| 92 | + |
| 93 | + // Add responsive attributes |
| 94 | + if (this.options.responsive) { |
| 95 | + img.sizes = '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw'; |
| 96 | + img.srcset = this.generateSrcSet(imageData); |
| 97 | + } |
| 98 | + |
| 99 | + // Add data attributes for structured data |
| 100 | + img.setAttribute('data-description', imageData.description); |
| 101 | + img.setAttribute('data-keywords', imageData.keywords.join(', ')); |
| 102 | + img.setAttribute('data-metadata', imageData.metadata); |
| 103 | + |
| 104 | + // Add click tracking |
| 105 | + img.addEventListener('click', () => this.trackImageClick(imageData)); |
| 106 | + } |
| 107 | + |
| 108 | + optimizeImageFallback(img, imageKey) { |
| 109 | + // Basic fallback optimization |
| 110 | + img.loading = 'lazy'; |
| 111 | + img.alt = this.generateAltText(imageKey); |
| 112 | + img.title = this.generateTitle(imageKey); |
| 113 | + } |
| 114 | + |
| 115 | + generateSrcSet(imageData) { |
| 116 | + const sizes = [300, 600, 900, 1200]; |
| 117 | + return sizes.map(size => { |
| 118 | + const thumbnailUrl = imageData.thumbnailUrl.replace('-thumb', `-${size}`); |
| 119 | + return `${thumbnailUrl} ${size}w`; |
| 120 | + }).join(', '); |
| 121 | + } |
| 122 | + |
| 123 | + generateAltText(imageKey) { |
| 124 | + // Generate meaningful alt text based on filename |
| 125 | + return imageKey |
| 126 | + .replace(/[-_]/g, ' ') |
| 127 | + .replace(/\.(png|jpg|jpeg|gif|webp)$/i, '') |
| 128 | + .replace(/\b\w/g, l => l.toUpperCase()); |
| 129 | + } |
| 130 | + |
| 131 | + generateTitle(imageKey) { |
| 132 | + return this.generateAltText(imageKey); |
| 133 | + } |
| 134 | + |
| 135 | + supportsWebP() { |
| 136 | + const canvas = document.createElement('canvas'); |
| 137 | + canvas.width = 1; |
| 138 | + canvas.height = 1; |
| 139 | + return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0; |
| 140 | + } |
| 141 | + |
| 142 | + addStructuredData() { |
| 143 | + if (!this.manifest) return; |
| 144 | + |
| 145 | + const structuredData = { |
| 146 | + "@context": "https://schema.org", |
| 147 | + "@type": "ImageGallery", |
| 148 | + "name": "Telecom Security Research Images", |
| 149 | + "description": "Comprehensive collection of telecommunications security research images and diagrams", |
| 150 | + "url": "https://telcosec.github.io/Telecom-Security-Documents/images/", |
| 151 | + "image": this.manifest.images.map(img => ({ |
| 152 | + "@type": "ImageObject", |
| 153 | + "url": img.url, |
| 154 | + "name": img.title, |
| 155 | + "description": img.description, |
| 156 | + "thumbnailUrl": img.thumbnailUrl, |
| 157 | + "contentUrl": img.url, |
| 158 | + "encodingFormat": img.filename.split('.').pop().toUpperCase(), |
| 159 | + "keywords": img.keywords.join(', ') |
| 160 | + })), |
| 161 | + "author": { |
| 162 | + "@type": "Person", |
| 163 | + "name": "RFS", |
| 164 | + "description": "Telecommunications Security Researcher" |
| 165 | + }, |
| 166 | + "publisher": { |
| 167 | + "@type": "Organization", |
| 168 | + "name": "Telecom Security Documents", |
| 169 | + "url": "https://telcosec.github.io/Telecom-Security-Documents/" |
| 170 | + } |
| 171 | + }; |
| 172 | + |
| 173 | + // Add structured data to page |
| 174 | + const script = document.createElement('script'); |
| 175 | + script.type = 'application/ld+json'; |
| 176 | + script.textContent = JSON.stringify(structuredData); |
| 177 | + document.head.appendChild(script); |
| 178 | + } |
| 179 | + |
| 180 | + trackImageClick(imageData) { |
| 181 | + // Analytics tracking |
| 182 | + if (typeof gtag !== 'undefined') { |
| 183 | + gtag('event', 'image_click', { |
| 184 | + 'event_category': 'images', |
| 185 | + 'event_label': imageData.filename, |
| 186 | + 'value': 1 |
| 187 | + }); |
| 188 | + } |
| 189 | + |
| 190 | + // Custom tracking |
| 191 | + const event = new CustomEvent('imageClick', { |
| 192 | + detail: { |
| 193 | + image: imageData, |
| 194 | + timestamp: new Date().toISOString() |
| 195 | + } |
| 196 | + }); |
| 197 | + document.dispatchEvent(event); |
| 198 | + } |
| 199 | + |
| 200 | + // Static method for quick initialization |
| 201 | + static init(selector, options = {}) { |
| 202 | + return new OptimizedImage(selector, options); |
| 203 | + } |
| 204 | +} |
| 205 | + |
| 206 | +// Auto-initialize on DOM ready |
| 207 | +document.addEventListener('DOMContentLoaded', () => { |
| 208 | + // Initialize for all image containers |
| 209 | + const imageContainers = document.querySelectorAll('[data-image-container]'); |
| 210 | + imageContainers.forEach(container => { |
| 211 | + new OptimizedImage(container, { |
| 212 | + lazy: true, |
| 213 | + webp: true, |
| 214 | + responsive: true, |
| 215 | + structuredData: true |
| 216 | + }); |
| 217 | + }); |
| 218 | + |
| 219 | + // Initialize for main content area |
| 220 | + if (document.querySelector('main') || document.querySelector('.container')) { |
| 221 | + new OptimizedImage('main, .container', { |
| 222 | + lazy: true, |
| 223 | + webp: true, |
| 224 | + responsive: true, |
| 225 | + structuredData: false // Already added above |
| 226 | + }); |
| 227 | + } |
| 228 | +}); |
| 229 | + |
| 230 | +// Export for module systems |
| 231 | +if (typeof module !== 'undefined' && module.exports) { |
| 232 | + module.exports = OptimizedImage; |
| 233 | +} |
0 commit comments