sexyjekyll-theme 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +82 -0
  3. data/LICENSE +21 -0
  4. data/README.de.md +276 -0
  5. data/README.es.md +276 -0
  6. data/README.fr.md +276 -0
  7. data/README.it.md +219 -0
  8. data/README.md +276 -0
  9. data/_includes/critical-css.html +4 -0
  10. data/_includes/footer.html +81 -0
  11. data/_includes/head.html +88 -0
  12. data/_includes/nav.html +21 -0
  13. data/_includes/related-posts.html +75 -0
  14. data/_includes/social-icon.html +98 -0
  15. data/_includes/structured-data-article.html +55 -0
  16. data/_includes/structured-data-breadcrumb.html +28 -0
  17. data/_includes/structured-data-person.html +82 -0
  18. data/_includes/structured-data-website.html +23 -0
  19. data/_layouts/blog.html +101 -0
  20. data/_layouts/category.html +66 -0
  21. data/_layouts/contact.html +26 -0
  22. data/_layouts/default.html +13 -0
  23. data/_layouts/home.html +30 -0
  24. data/_layouts/llms.txt +34 -0
  25. data/_layouts/post.html +99 -0
  26. data/_plugins/auto_related_posts.rb +153 -0
  27. data/_plugins/category_generator.rb +27 -0
  28. data/_plugins/llms_txt_generator.rb +45 -0
  29. data/_plugins/localized_date.rb +52 -0
  30. data/assets/bg.jpeg +0 -0
  31. data/assets/bg.webp +0 -0
  32. data/assets/debug/blocco.png +0 -0
  33. data/assets/debug/categorie.jpeg +0 -0
  34. data/assets/debug/categorie.png +0 -0
  35. data/assets/debug/codice.png +0 -0
  36. data/assets/debug/contrasto.jpeg +0 -0
  37. data/assets/debug/dipendenze.png +0 -0
  38. data/assets/debug/h1.png +0 -0
  39. data/assets/debug/pagespeed.png +0 -0
  40. data/assets/debug/ricerca.png +0 -0
  41. data/assets/debug/richieste.png +0 -0
  42. data/assets/favicon/android-icon-144x144.png +0 -0
  43. data/assets/favicon/android-icon-192x192.png +0 -0
  44. data/assets/favicon/android-icon-36x36.png +0 -0
  45. data/assets/favicon/android-icon-48x48.png +0 -0
  46. data/assets/favicon/android-icon-72x72.png +0 -0
  47. data/assets/favicon/android-icon-96x96.png +0 -0
  48. data/assets/favicon/apple-icon-114x114.png +0 -0
  49. data/assets/favicon/apple-icon-120x120.png +0 -0
  50. data/assets/favicon/apple-icon-144x144.png +0 -0
  51. data/assets/favicon/apple-icon-152x152.png +0 -0
  52. data/assets/favicon/apple-icon-180x180.png +0 -0
  53. data/assets/favicon/apple-icon-57x57.png +0 -0
  54. data/assets/favicon/apple-icon-60x60.png +0 -0
  55. data/assets/favicon/apple-icon-72x72.png +0 -0
  56. data/assets/favicon/apple-icon-76x76.png +0 -0
  57. data/assets/favicon/apple-icon-precomposed.png +0 -0
  58. data/assets/favicon/apple-icon.png +0 -0
  59. data/assets/favicon/favicon-16x16.png +0 -0
  60. data/assets/favicon/favicon-32x32.png +0 -0
  61. data/assets/favicon/favicon-96x96.png +0 -0
  62. data/assets/favicon/favicon.ico +0 -0
  63. data/assets/favicon/ms-icon-144x144.png +0 -0
  64. data/assets/favicon/ms-icon-150x150.png +0 -0
  65. data/assets/favicon/ms-icon-310x310.png +0 -0
  66. data/assets/favicon/ms-icon-70x70.png +0 -0
  67. data/assets/images/aiact.jpeg +0 -0
  68. data/assets/images/aiethics.jpeg +0 -0
  69. data/assets/images/green.jpeg +0 -0
  70. data/assets/images/jekyll.webp +0 -0
  71. data/assets/images/parenting.jpeg +0 -0
  72. data/assets/images/seo-generativo.jpeg +0 -0
  73. data/assets/images/upskilling-ai.jpeg +0 -0
  74. data/assets/pic.jpeg +0 -0
  75. data/assets/screens/screen.jpeg +0 -0
  76. data/assets/screens/screen2.jpeg +0 -0
  77. data/css/animations.css +404 -0
  78. data/css/style.css +2250 -0
  79. data/css/syntax-dark.css +157 -0
  80. data/css/syntax-light.css +157 -0
  81. data/js/main.js +706 -0
  82. data/js/simple-jekyll-search.min.js +6 -0
  83. metadata +254 -0
data/js/main.js ADDED
@@ -0,0 +1,706 @@
1
+ // =======================
2
+ // Main JavaScript
3
+ // =======================
4
+
5
+ // Wait for DOM to be fully loaded
6
+ document.addEventListener('DOMContentLoaded', () => {
7
+ initDarkMode();
8
+ initSmoothScroll();
9
+ initNavigation();
10
+ initAnimatedBackground();
11
+ initSearch();
12
+ initReadingProgress();
13
+ initExternalLinks();
14
+ initPageAnimations();
15
+ });
16
+
17
+ // =======================
18
+ // Dark Mode (Always Active)
19
+ // =======================
20
+
21
+ function initDarkMode() {
22
+ // Set dark mode permanently
23
+ document.documentElement.setAttribute('data-theme', 'dark');
24
+
25
+ // Force dark syntax highlighting
26
+ const darkLink = document.querySelector('link[href*="syntax-dark"]');
27
+ const lightLink = document.querySelector('link[href*="syntax-light"]');
28
+
29
+ if (darkLink && lightLink) {
30
+ darkLink.removeAttribute('disabled');
31
+ darkLink.media = 'all';
32
+ lightLink.setAttribute('disabled', 'disabled');
33
+ lightLink.media = 'not all';
34
+ }
35
+ }
36
+
37
+ // =======================
38
+ // Smooth Scroll
39
+ // =======================
40
+
41
+ function initSmoothScroll() {
42
+ // Handle smooth scrolling for anchor links
43
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
44
+ anchor.addEventListener('click', function (e) {
45
+ const href = this.getAttribute('href');
46
+
47
+ // Don't prevent default if href is just "#"
48
+ if (href === '#') return;
49
+
50
+ e.preventDefault();
51
+
52
+ const target = document.querySelector(href);
53
+ if (target) {
54
+ const headerOffset = 80; // Account for fixed header
55
+ const elementPosition = target.getBoundingClientRect().top;
56
+ const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
57
+
58
+ window.scrollTo({
59
+ top: offsetPosition,
60
+ behavior: 'smooth'
61
+ });
62
+ }
63
+ });
64
+ });
65
+ }
66
+
67
+ // =======================
68
+ // Navigation (Always Visible)
69
+ // =======================
70
+
71
+ function initNavigation() {
72
+ // Active nav link highlighting
73
+ highlightActiveSection();
74
+ }
75
+
76
+ // =======================
77
+ // Active Section Highlighting
78
+ // =======================
79
+
80
+ function highlightActiveSection() {
81
+ const sections = document.querySelectorAll('section[id]');
82
+ const navLinks = document.querySelectorAll('.nav-link');
83
+
84
+ if (!('IntersectionObserver' in window)) return;
85
+
86
+ const observerOptions = {
87
+ threshold: 0.3,
88
+ rootMargin: '-80px 0px -70% 0px'
89
+ };
90
+
91
+ const observer = new IntersectionObserver((entries) => {
92
+ entries.forEach(entry => {
93
+ if (entry.isIntersecting) {
94
+ const id = entry.target.getAttribute('id');
95
+
96
+ // Remove active class from all links
97
+ navLinks.forEach(link => {
98
+ link.classList.remove('active');
99
+ if (link.getAttribute('href') === `#${id}`) {
100
+ link.classList.add('active');
101
+ }
102
+ });
103
+ }
104
+ });
105
+ }, observerOptions);
106
+
107
+ sections.forEach(section => {
108
+ observer.observe(section);
109
+ });
110
+ }
111
+
112
+ // =======================
113
+ // Utility Functions
114
+ // =======================
115
+
116
+ // Debounce function for performance optimization
117
+ function debounce(func, wait) {
118
+ let timeout;
119
+ return function executedFunction(...args) {
120
+ const later = () => {
121
+ clearTimeout(timeout);
122
+ func(...args);
123
+ };
124
+ clearTimeout(timeout);
125
+ timeout = setTimeout(later, wait);
126
+ };
127
+ }
128
+
129
+ // Throttle function for scroll events
130
+ function throttle(func, limit) {
131
+ let inThrottle;
132
+ return function(...args) {
133
+ if (!inThrottle) {
134
+ func.apply(this, args);
135
+ inThrottle = true;
136
+ setTimeout(() => inThrottle = false, limit);
137
+ }
138
+ };
139
+ }
140
+
141
+ // =======================
142
+ // Performance Monitoring
143
+ // =======================
144
+
145
+ // Log performance metrics (optional, for development)
146
+ if (window.performance && console.debug) {
147
+ window.addEventListener('load', () => {
148
+ const perfData = window.performance.timing;
149
+ const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;
150
+ const connectTime = perfData.responseEnd - perfData.requestStart;
151
+ const renderTime = perfData.domComplete - perfData.domLoading;
152
+
153
+ console.debug('Page Load Time:', pageLoadTime + 'ms');
154
+ console.debug('Connect Time:', connectTime + 'ms');
155
+ console.debug('Render Time:', renderTime + 'ms');
156
+ });
157
+ }
158
+
159
+ // =======================
160
+ // Error Handling
161
+ // =======================
162
+
163
+ // Global error handler
164
+ window.addEventListener('error', (event) => {
165
+ console.error('Global error:', event.error);
166
+ // You can add error reporting service here (e.g., Sentry)
167
+ });
168
+
169
+ // Handle unhandled promise rejections
170
+ window.addEventListener('unhandledrejection', (event) => {
171
+ console.error('Unhandled promise rejection:', event.reason);
172
+ // You can add error reporting service here
173
+ });
174
+
175
+ // =======================
176
+ // Animated Background
177
+ // =======================
178
+
179
+ function initAnimatedBackground() {
180
+ const blobs = document.querySelectorAll('.gradient-blob');
181
+
182
+ // Add mouse move parallax effect
183
+ document.addEventListener('mousemove', (e) => {
184
+ const mouseX = e.clientX / window.innerWidth;
185
+ const mouseY = e.clientY / window.innerHeight;
186
+
187
+ blobs.forEach((blob, index) => {
188
+ const speed = (index + 1) * 0.5;
189
+ const x = (mouseX - 0.5) * speed * 50;
190
+ const y = (mouseY - 0.5) * speed * 50;
191
+
192
+ blob.style.transform = `translate(${x}px, ${y}px)`;
193
+ });
194
+ });
195
+
196
+ // Add random color shift over time
197
+ function shiftColors() {
198
+ const colors = [
199
+ { name: 'purple', value: '#a855f7' },
200
+ { name: 'pink', value: '#ec4899' },
201
+ { name: 'blue', value: '#3b82f6' },
202
+ { name: 'cyan', value: '#06b6d4' },
203
+ { name: 'orange', value: '#f97316' },
204
+ { name: 'yellow', value: '#facc15' }
205
+ ];
206
+
207
+ blobs.forEach((blob, index) => {
208
+ const randomColor = colors[Math.floor(Math.random() * colors.length)];
209
+ const currentBg = blob.style.background || '';
210
+
211
+ // Subtle color changes without jarring transitions
212
+ if (Math.random() > 0.7) {
213
+ blob.style.background = `radial-gradient(circle, ${randomColor.value} 0%, transparent 70%)`;
214
+ }
215
+ });
216
+ }
217
+
218
+ // Shift colors every 10 seconds
219
+ setInterval(shiftColors, 10000);
220
+ }
221
+
222
+ // =======================
223
+ // Search Functionality
224
+ // =======================
225
+
226
+ function initSearch() {
227
+ const searchInput = document.getElementById('search-input');
228
+ const searchClear = document.getElementById('search-clear');
229
+ const searchResults = document.getElementById('search-results');
230
+ const searchResultsCount = document.getElementById('search-results-count');
231
+ const regularPosts = document.getElementById('regular-posts');
232
+ const pagination = document.getElementById('pagination');
233
+
234
+ // Only initialize if we're on a page with search
235
+ if (!searchInput) return;
236
+
237
+ // Load Simple Jekyll Search library
238
+ const script = document.createElement('script');
239
+ script.src = '/js/simple-jekyll-search.min.js';
240
+ script.onload = () => {
241
+ // Initialize Simple Jekyll Search
242
+ window.simpleJekyllSearch = SimpleJekyllSearch({
243
+ searchInput: searchInput,
244
+ resultsContainer: searchResults,
245
+ json: '/search.json',
246
+ searchResultTemplate: `
247
+ <article class="blog-post-card">
248
+ <div class="post-card-content">
249
+ <time class="post-date" datetime="{date}">{date}</time>
250
+ <h2 class="post-card-title">
251
+ <a href="{url}">{title}</a>
252
+ </h2>
253
+ <p class="post-card-excerpt">{excerpt}</p>
254
+ <div class="post-card-footer">
255
+ <div class="post-categories">
256
+ <span class="post-category">{category}</span>
257
+ </div>
258
+ <a href="{url}" class="read-more">Leggi →</a>
259
+ </div>
260
+ </div>
261
+ </article>
262
+ `,
263
+ noResultsText: `
264
+ <div class="search-no-results">
265
+ <h3>Nessun risultato trovato</h3>
266
+ <p>Prova con parole chiave diverse o sfoglia tutti gli articoli qui sotto.</p>
267
+ </div>
268
+ `,
269
+ limit: 20,
270
+ fuzzy: false,
271
+ exclude: []
272
+ });
273
+ };
274
+ document.head.appendChild(script);
275
+
276
+ // Handle search input
277
+ searchInput.addEventListener('input', debounce((e) => {
278
+ const query = e.target.value.trim();
279
+
280
+ if (query.length > 0) {
281
+ // Show clear button
282
+ searchClear.style.display = 'flex';
283
+
284
+ // Show search results, hide regular posts and pagination
285
+ searchResults.style.display = 'grid';
286
+ if (regularPosts) regularPosts.style.display = 'none';
287
+ if (pagination) pagination.style.display = 'none';
288
+
289
+ // Count results after a short delay to let Simple Jekyll Search finish
290
+ setTimeout(() => {
291
+ const resultCount = searchResults.children.length;
292
+ if (resultCount > 0 && !searchResults.querySelector('.search-no-results')) {
293
+ searchResultsCount.style.display = 'block';
294
+ searchResultsCount.innerHTML = `Trovat${resultCount !== 1 ? 'i' : 'o'} <strong>${resultCount}</strong> risultat${resultCount !== 1 ? 'i' : 'o'} per "<strong>${escapeHtml(query)}</strong>"`;
295
+ } else {
296
+ searchResultsCount.style.display = 'none';
297
+ }
298
+ }, 100);
299
+ } else {
300
+ clearSearch();
301
+ }
302
+ }, 300));
303
+
304
+ // Handle clear button
305
+ searchClear.addEventListener('click', () => {
306
+ searchInput.value = '';
307
+ searchInput.focus();
308
+ clearSearch();
309
+ });
310
+
311
+ // Clear search on Escape key
312
+ searchInput.addEventListener('keydown', (e) => {
313
+ if (e.key === 'Escape') {
314
+ searchInput.value = '';
315
+ clearSearch();
316
+ }
317
+ });
318
+
319
+ function clearSearch() {
320
+ searchClear.style.display = 'none';
321
+ searchResults.style.display = 'none';
322
+ searchResultsCount.style.display = 'none';
323
+ if (regularPosts) regularPosts.style.display = 'grid';
324
+ if (pagination) pagination.style.display = 'flex';
325
+ }
326
+
327
+ // Escape HTML to prevent XSS
328
+ function escapeHtml(text) {
329
+ const map = {
330
+ '&': '&amp;',
331
+ '<': '&lt;',
332
+ '>': '&gt;',
333
+ '"': '&quot;',
334
+ "'": '&#039;'
335
+ };
336
+ return text.replace(/[&<>"']/g, (m) => map[m]);
337
+ }
338
+ }
339
+
340
+ // =======================
341
+ // Reading Progress Bar
342
+ // =======================
343
+
344
+ function initReadingProgress() {
345
+ const progressBar = document.getElementById('reading-progress-bar');
346
+ const progressContainer = progressBar?.parentElement;
347
+
348
+ // Only initialize on blog post pages
349
+ if (!progressBar || !progressContainer) return;
350
+
351
+ // Get the main content element
352
+ const mainContent = document.querySelector('.post-content');
353
+ if (!mainContent) return;
354
+
355
+ // Function to calculate and update progress
356
+ function updateProgress() {
357
+ // Get the bounding rectangles
358
+ const contentRect = mainContent.getBoundingClientRect();
359
+ const windowHeight = window.innerHeight;
360
+
361
+ // Calculate the start and end points for reading
362
+ // Start when content top reaches viewport top
363
+ const startPoint = contentRect.top + window.scrollY - windowHeight * 0.2;
364
+ // End when content bottom reaches viewport bottom
365
+ const endPoint = contentRect.bottom + window.scrollY - windowHeight * 0.8;
366
+
367
+ // Calculate total scrollable distance
368
+ const totalDistance = endPoint - startPoint;
369
+
370
+ // Current scroll position relative to start
371
+ const currentScroll = window.scrollY - startPoint;
372
+
373
+ // Calculate percentage (0-100)
374
+ let percentage = (currentScroll / totalDistance) * 100;
375
+
376
+ // Clamp between 0 and 100
377
+ percentage = Math.max(0, Math.min(100, percentage));
378
+
379
+ // Update the progress bar width
380
+ progressBar.style.width = `${percentage}%`;
381
+
382
+ // Show progress bar when user starts scrolling
383
+ if (window.scrollY > 100) {
384
+ progressContainer.classList.add('visible');
385
+ } else {
386
+ progressContainer.classList.remove('visible');
387
+ }
388
+ }
389
+
390
+ // Throttled scroll handler for better performance
391
+ let ticking = false;
392
+ function handleScroll() {
393
+ if (!ticking) {
394
+ window.requestAnimationFrame(() => {
395
+ updateProgress();
396
+ ticking = false;
397
+ });
398
+ ticking = true;
399
+ }
400
+ }
401
+
402
+ // Initial calculation
403
+ updateProgress();
404
+
405
+ // Update on scroll with throttling
406
+ window.addEventListener('scroll', handleScroll, { passive: true });
407
+
408
+ // Update on resize (in case content reflows)
409
+ window.addEventListener('resize', debounce(() => {
410
+ updateProgress();
411
+ }, 250));
412
+ }
413
+
414
+ // =======================
415
+ // Table of Contents
416
+ // =======================
417
+
418
+ function initTableOfContents() {
419
+ const tocContainer = document.getElementById('toc-container');
420
+ const tocList = document.getElementById('toc-list');
421
+ const tocEmpty = document.getElementById('toc-empty');
422
+ const tocToggle = document.getElementById('toc-toggle');
423
+
424
+ // Only initialize if TOC container exists
425
+ if (!tocContainer || !tocList) return;
426
+
427
+ // Get all H2 and H3 headings from post content
428
+ const content = document.querySelector('.post-content');
429
+ if (!content) return;
430
+
431
+ const headings = content.querySelectorAll('h2, h3');
432
+
433
+ // If no headings found, show empty message
434
+ if (headings.length === 0) {
435
+ tocEmpty.style.display = 'block';
436
+ document.getElementById('toc-nav').style.display = 'none';
437
+ return;
438
+ }
439
+
440
+ // Generate TOC items
441
+ headings.forEach((heading, index) => {
442
+ // Create unique ID if heading doesn't have one
443
+ if (!heading.id) {
444
+ heading.id = `heading-${index}-${slugify(heading.textContent)}`;
445
+ }
446
+
447
+ // Create TOC item
448
+ const tocItem = document.createElement('li');
449
+ tocItem.className = `toc-item toc-${heading.tagName.toLowerCase()}`;
450
+
451
+ const tocLink = document.createElement('a');
452
+ tocLink.className = 'toc-link';
453
+ tocLink.href = `#${heading.id}`;
454
+ tocLink.textContent = heading.textContent;
455
+ tocLink.setAttribute('data-heading-id', heading.id);
456
+
457
+ // Smooth scroll on click
458
+ tocLink.addEventListener('click', (e) => {
459
+ e.preventDefault();
460
+ const target = document.getElementById(heading.id);
461
+ if (target) {
462
+ const headerOffset = 100;
463
+ const elementPosition = target.getBoundingClientRect().top;
464
+ const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
465
+
466
+ window.scrollTo({
467
+ top: offsetPosition,
468
+ behavior: 'smooth'
469
+ });
470
+
471
+ // Update URL hash without jumping
472
+ history.pushState(null, null, `#${heading.id}`);
473
+ }
474
+ });
475
+
476
+ tocItem.appendChild(tocLink);
477
+ tocList.appendChild(tocItem);
478
+ });
479
+
480
+ // Handle toggle button
481
+ if (tocToggle) {
482
+ tocToggle.addEventListener('click', () => {
483
+ tocContainer.classList.toggle('collapsed');
484
+ const isCollapsed = tocContainer.classList.contains('collapsed');
485
+ tocToggle.setAttribute('aria-expanded', !isCollapsed);
486
+ });
487
+ }
488
+
489
+ // Highlight active section on scroll
490
+ const observerOptions = {
491
+ rootMargin: '-100px 0px -66%',
492
+ threshold: 0
493
+ };
494
+
495
+ const observer = new IntersectionObserver((entries) => {
496
+ entries.forEach(entry => {
497
+ const id = entry.target.getAttribute('id');
498
+ const tocLink = tocList.querySelector(`[data-heading-id="${id}"]`);
499
+
500
+ if (entry.isIntersecting) {
501
+ // Remove active from all links
502
+ tocList.querySelectorAll('.toc-link').forEach(link => {
503
+ link.classList.remove('active');
504
+ });
505
+
506
+ // Add active to current link
507
+ if (tocLink) {
508
+ tocLink.classList.add('active');
509
+
510
+ // Scroll TOC to show active item
511
+ const tocNav = document.getElementById('toc-nav');
512
+ if (tocNav) {
513
+ const linkRect = tocLink.getBoundingClientRect();
514
+ const navRect = tocNav.getBoundingClientRect();
515
+
516
+ if (linkRect.top < navRect.top || linkRect.bottom > navRect.bottom) {
517
+ tocLink.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
518
+ }
519
+ }
520
+ }
521
+ }
522
+ });
523
+ }, observerOptions);
524
+
525
+ // Observe all headings
526
+ headings.forEach(heading => {
527
+ observer.observe(heading);
528
+ });
529
+ }
530
+
531
+ // Helper function to create URL-friendly slugs
532
+ function slugify(text) {
533
+ return text
534
+ .toString()
535
+ .toLowerCase()
536
+ .trim()
537
+ .replace(/\s+/g, '-') // Replace spaces with -
538
+ .replace(/[^\w\-]+/g, '') // Remove all non-word chars
539
+ .replace(/\-\-+/g, '-') // Replace multiple - with single -
540
+ .replace(/^-+/, '') // Trim - from start
541
+ .replace(/-+$/, ''); // Trim - from end
542
+ }
543
+
544
+ // =======================
545
+ // External Links Handler
546
+ // =======================
547
+
548
+ function initExternalLinks() {
549
+ // Get all links in the post content
550
+ const postContent = document.querySelector('.post-content');
551
+ if (!postContent) return;
552
+
553
+ const links = postContent.querySelectorAll('a[href]');
554
+
555
+ links.forEach(link => {
556
+ const href = link.getAttribute('href');
557
+
558
+ // Check if link is external (starts with http:// or https://)
559
+ // and doesn't link to the current domain
560
+ if (href && (href.startsWith('http://') || href.startsWith('https://'))) {
561
+ const currentDomain = window.location.hostname;
562
+ const linkDomain = new URL(href).hostname;
563
+
564
+ if (linkDomain !== currentDomain) {
565
+ // Add target="_blank" and security attributes
566
+ link.setAttribute('target', '_blank');
567
+ link.setAttribute('rel', 'noopener noreferrer');
568
+
569
+ // Add external link icon if not already present
570
+ if (!link.querySelector('.external-link-icon')) {
571
+ const icon = document.createElement('span');
572
+ icon.className = 'external-link-icon';
573
+ icon.setAttribute('aria-label', '(si apre in una nuova scheda)');
574
+ icon.innerHTML = '↗';
575
+ link.appendChild(icon);
576
+ }
577
+ }
578
+ }
579
+ });
580
+ }
581
+
582
+ // =======================
583
+ // Page Load Animations
584
+ // =======================
585
+
586
+ function initPageAnimations() {
587
+ // Animate hero section if present (home page)
588
+ const hero = document.querySelector('.hero');
589
+ if (hero) {
590
+ hero.classList.add('hero-animate');
591
+ // Animate hero children
592
+ const heroChildren = hero.querySelectorAll('.hero-title, .hero-subtitle, .hero-tagline, .hero-description, .hero-cta');
593
+ heroChildren.forEach(child => child.classList.add('animate-on-load'));
594
+ }
595
+
596
+ // Animate post hero section if present
597
+ const postHero = document.querySelector('.post-hero');
598
+ if (postHero) {
599
+ postHero.classList.add('post-hero-animate');
600
+ // Animate post hero children
601
+ const postHeroChildren = postHero.querySelectorAll('.post-hero-overlay, .post-hero-date, .post-hero-categories, .post-hero-title, .post-hero-subtitle, .post-hero-reading-time');
602
+ postHeroChildren.forEach(child => child.classList.add('animate-on-load'));
603
+ }
604
+
605
+ // Animate post header if present (posts without hero image)
606
+ const postHeader = document.querySelector('.post-header');
607
+ if (postHeader) {
608
+ postHeader.classList.add('post-header-animate');
609
+ // Animate post header children
610
+ const postHeaderChildren = postHeader.querySelectorAll('.post-date, .post-categories, .post-title, .post-subtitle, .post-meta');
611
+ postHeaderChildren.forEach(child => child.classList.add('animate-on-load'));
612
+ }
613
+
614
+ // Animate navigation
615
+ const nav = document.querySelector('.nav');
616
+ if (nav) {
617
+ nav.classList.add('nav-animate', 'animate-on-load');
618
+ }
619
+
620
+ // Animate main content sections
621
+ const mainContent = document.querySelector('#main-content, main');
622
+ if (mainContent) {
623
+ mainContent.classList.add('content-animate', 'animate-on-load');
624
+ }
625
+
626
+ // Animate blog post cards with stagger effect
627
+ const blogCards = document.querySelectorAll('.blog-post-card, .post-card');
628
+ blogCards.forEach((card, index) => {
629
+ card.classList.add('card-animate', 'animate-on-load');
630
+ // Add staggered delay for each card (max 8 cards with visible delay)
631
+ if (index < 8) {
632
+ card.classList.add(`delay-${index + 1}`);
633
+ }
634
+ });
635
+
636
+ // Animate footer
637
+ const footer = document.querySelector('.footer');
638
+ if (footer) {
639
+ // Use Intersection Observer for footer animation when it comes into view
640
+ if ('IntersectionObserver' in window) {
641
+ const footerObserver = new IntersectionObserver((entries) => {
642
+ entries.forEach(entry => {
643
+ if (entry.isIntersecting) {
644
+ entry.target.classList.add('fade-in-up', 'animate-on-load');
645
+ footerObserver.unobserve(entry.target);
646
+ }
647
+ });
648
+ }, {
649
+ threshold: 0.1,
650
+ rootMargin: '0px 0px -50px 0px'
651
+ });
652
+
653
+ footerObserver.observe(footer);
654
+ }
655
+ }
656
+
657
+ // Animate sections on scroll using Intersection Observer
658
+ const animateSections = document.querySelectorAll('.blog-section, .contact-section, .category-section, section');
659
+
660
+ if ('IntersectionObserver' in window && animateSections.length > 0) {
661
+ const sectionObserver = new IntersectionObserver((entries) => {
662
+ entries.forEach(entry => {
663
+ if (entry.isIntersecting) {
664
+ entry.target.classList.add('fade-in-up');
665
+ sectionObserver.unobserve(entry.target);
666
+ }
667
+ });
668
+ }, {
669
+ threshold: 0.15,
670
+ rootMargin: '0px 0px -100px 0px'
671
+ });
672
+
673
+ animateSections.forEach(section => {
674
+ // Skip sections that already have animations
675
+ if (!section.classList.contains('hero') && !section.querySelector('.hero')) {
676
+ sectionObserver.observe(section);
677
+ }
678
+ });
679
+ }
680
+
681
+ // Animate post content elements on scroll
682
+ const postContent = document.querySelector('.post-content');
683
+ if (postContent && 'IntersectionObserver' in window) {
684
+ const contentElements = postContent.querySelectorAll('h2, h3, p, ul, ol, blockquote, pre, img, .highlight');
685
+
686
+ const contentObserver = new IntersectionObserver((entries) => {
687
+ entries.forEach(entry => {
688
+ if (entry.isIntersecting) {
689
+ entry.target.classList.add('fade-in-up');
690
+ contentObserver.unobserve(entry.target);
691
+ }
692
+ });
693
+ }, {
694
+ threshold: 0.1,
695
+ rootMargin: '0px 0px -50px 0px'
696
+ });
697
+
698
+ contentElements.forEach((element, index) => {
699
+ // Add small staggered delay for consecutive elements
700
+ element.style.opacity = '0';
701
+ element.style.animationDelay = `${Math.min(index * 0.05, 0.3)}s`;
702
+ contentObserver.observe(element);
703
+ });
704
+ }
705
+ }
706
+