Optimize images for faster mobile loading

- Host images locally instead of loading from external inwardtravel.com
- Convert PNGs to WebP format (89% size reduction on teotihuacan)
- Add JPEG fallbacks for older browsers
- Implement lazy loading for below-fold images
- Add WebP feature detection
- Preload critical hero image
- Total payload reduced from 2.3MB to ~600KB

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-11 17:26:31 +01:00
parent a18fc217be
commit 110e815703
20 changed files with 160 additions and 46 deletions

View File

@ -2,6 +2,7 @@ FROM nginx:alpine
# Copy static files # Copy static files
COPY index.html /usr/share/nginx/html/ COPY index.html /usr/share/nginx/html/
COPY images/ /usr/share/nginx/html/images/
# Custom nginx config for SPA # Custom nginx config for SPA
RUN echo 'server { \ RUN echo 'server { \

BIN
images/ceremony.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
images/ceremony.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
images/community.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
images/community.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
images/cta-mountain.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
images/cta-mountain.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
images/hero-mountain.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

BIN
images/hero-mountain.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
images/sabina-art.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
images/sabina-art.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
images/sabina-poem.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
images/sabina-poem.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
images/temazcal.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
images/temazcal.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
images/teotihuacan.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

BIN
images/teotihuacan.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

BIN
images/waterfall.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
images/waterfall.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -8,6 +8,11 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@300;400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@300;400;500&display=swap" rel="stylesheet">
<!-- Preload critical hero image -->
<link rel="preload" as="image" href="images/hero-mountain.webp" type="image/webp">
<link rel="preload" as="image" href="images/hero-mountain.jpg" type="image/jpeg">
<style> <style>
:root { :root {
--color-bg: #0a0a0a; --color-bg: #0a0a0a;
@ -48,14 +53,29 @@
text-align: center; text-align: center;
padding: 2rem; padding: 2rem;
position: relative; position: relative;
background: background-color: var(--color-bg);
linear-gradient(to bottom, rgba(10, 10, 10, 0.3) 0%, rgba(10, 10, 10, 0.6) 50%, rgba(10, 10, 10, 0.95) 100%), }
url('https://www.inwardtravel.com/content/image/251215-124345/acommodation-mountain-view-huautla-de-jimenez-variants/image-1759-main_adventure_image/acommodation-mountain-view-huautla-de-jimenez.jpeg');
.hero-bg {
position: absolute;
inset: 0;
background-size: cover; background-size: cover;
background-position: center; background-position: center;
background-attachment: fixed; background-attachment: fixed;
} }
.webp .hero-bg {
background-image:
linear-gradient(to bottom, rgba(10, 10, 10, 0.3) 0%, rgba(10, 10, 10, 0.6) 50%, rgba(10, 10, 10, 0.95) 100%),
url('images/hero-mountain.webp');
}
.no-webp .hero-bg {
background-image:
linear-gradient(to bottom, rgba(10, 10, 10, 0.3) 0%, rgba(10, 10, 10, 0.6) 50%, rgba(10, 10, 10, 0.95) 100%),
url('images/hero-mountain.jpg');
}
.hero-content { .hero-content {
max-width: 900px; max-width: 900px;
position: relative; position: relative;
@ -164,11 +184,12 @@
50% { transform: translateX(-50%) translateY(8px); } 50% { transform: translateX(-50%) translateY(8px); }
} }
/* Full-width image break */ /* Full-width image break - lazy loaded */
.image-break { .image-break {
width: 100%; width: 100%;
height: 50vh; height: 50vh;
min-height: 300px; min-height: 300px;
background-color: var(--color-bg-alt);
background-size: cover; background-size: cover;
background-position: center; background-position: center;
background-attachment: fixed; background-attachment: fixed;
@ -182,20 +203,19 @@
background: linear-gradient(to bottom, var(--color-bg) 0%, transparent 15%, transparent 85%, var(--color-bg) 100%); background: linear-gradient(to bottom, var(--color-bg) 0%, transparent 15%, transparent 85%, var(--color-bg) 100%);
} }
.image-break-teotihuacan { /* Lazy load backgrounds - only load when visible */
background-image: url('https://www.inwardtravel.com/content/image/251216-165925/teotihuacan-mexico-temple-variants/itinerary-33-adventure_itinerary_image/teotihuacan-mexico-temple.png'); .webp .image-break-teotihuacan.loaded {
background-image: url('images/teotihuacan.webp');
}
.no-webp .image-break-teotihuacan.loaded {
background-image: url('images/teotihuacan.jpg');
} }
.image-break-ceremony { .webp .image-break-sabina.loaded {
background-image: url('https://www.inwardtravel.com/content/image/251216-203848/mushroom-mazatec-tradition-curandera-mexico-variants/itinerary-35-adventure_itinerary_image_mobile/mushroom-mazatec-tradition-curandera-mexico.png'); background-image: url('images/sabina-art.webp');
} }
.no-webp .image-break-sabina.loaded {
.image-break-waterfall { background-image: url('images/sabina-art.jpg');
background-image: url('https://www.inwardtravel.com/content/image/251217-210223/oaxaca-tropical-waterfall-bridge-variants/itinerary-40-adventure_itinerary_image_mobile/oaxaca-tropical-waterfall-bridge.jpeg');
}
.image-break-sabina {
background-image: url('https://www.inwardtravel.com/content/image/251215-125710/mexican-art-maria-sabina-mushrooms-variants/itinerary-37-adventure_itinerary_image_mobile/mexican-art-maria-sabina-mushrooms.jpeg');
} }
/* Section Styles */ /* Section Styles */
@ -257,6 +277,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: flex-end;
background-color: var(--color-bg);
} }
.journey-card-bg { .journey-card-bg {
@ -387,14 +408,34 @@
text-align: center; text-align: center;
padding: 8rem 2rem; padding: 8rem 2rem;
position: relative; position: relative;
background: background-color: var(--color-bg);
linear-gradient(to bottom, rgba(10, 10, 10, 0.9) 0%, rgba(10, 10, 10, 0.7) 50%, rgba(10, 10, 10, 0.95) 100%), }
url('https://www.inwardtravel.com/content/image/251215-125850/mountain-view-with-flowerts-huautla-de-jimenez-variants/itinerary-41-adventure_itinerary_image_mobile/mountain-view-with-flowerts-huautla-de-jimenez.jpeg');
.cta-section-bg {
position: absolute;
inset: 0;
background-size: cover; background-size: cover;
background-position: center; background-position: center;
background-attachment: fixed; background-attachment: fixed;
} }
.webp .cta-section-bg.loaded {
background-image:
linear-gradient(to bottom, rgba(10, 10, 10, 0.9) 0%, rgba(10, 10, 10, 0.7) 50%, rgba(10, 10, 10, 0.95) 100%),
url('images/cta-mountain.webp');
}
.no-webp .cta-section-bg.loaded {
background-image:
linear-gradient(to bottom, rgba(10, 10, 10, 0.9) 0%, rgba(10, 10, 10, 0.7) 50%, rgba(10, 10, 10, 0.95) 100%),
url('images/cta-mountain.jpg');
}
.cta-section-content {
position: relative;
z-index: 1;
}
.cta-section h2 { .cta-section h2 {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
@ -471,6 +512,7 @@
.image-gallery-item { .image-gallery-item {
aspect-ratio: 1; aspect-ratio: 1;
background-color: var(--color-bg-alt);
background-size: cover; background-size: cover;
background-position: center; background-position: center;
position: relative; position: relative;
@ -490,19 +532,16 @@
/* Responsive */ /* Responsive */
@media (max-width: 768px) { @media (max-width: 768px) {
.hero { .hero-bg,
.image-break,
.cta-section-bg {
background-attachment: scroll; background-attachment: scroll;
} }
.image-break { .image-break {
background-attachment: scroll;
height: 40vh; height: 40vh;
} }
.cta-section {
background-attachment: scroll;
}
.image-gallery { .image-gallery {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@ -529,8 +568,46 @@
</style> </style>
</head> </head>
<body> <body>
<!-- WebP detection and lazy loading -->
<script>
// Detect WebP support
(function() {
var webp = new Image();
webp.onload = webp.onerror = function() {
document.documentElement.classList.add(webp.height === 1 ? 'webp' : 'no-webp');
};
webp.src = 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=';
})();
// Lazy load background images
document.addEventListener('DOMContentLoaded', function() {
var lazyBgs = document.querySelectorAll('[data-lazy-bg]');
if ('IntersectionObserver' in window) {
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
entry.target.classList.add('loaded');
observer.unobserve(entry.target);
}
});
}, { rootMargin: '200px' });
lazyBgs.forEach(function(el) {
observer.observe(el);
});
} else {
// Fallback: load all immediately
lazyBgs.forEach(function(el) {
el.classList.add('loaded');
});
}
});
</script>
<!-- Hero --> <!-- Hero -->
<section class="hero"> <section class="hero">
<div class="hero-bg"></div>
<div class="hero-content"> <div class="hero-content">
<p class="hero-tagline">Inward Travel</p> <p class="hero-tagline">Inward Travel</p>
<h1>Journey to the Heart of <em>Sacred Mushrooms</em> in Oaxaca</h1> <h1>Journey to the Heart of <em>Sacred Mushrooms</em> in Oaxaca</h1>
@ -580,7 +657,7 @@
</section> </section>
<!-- Image Break: Teotihuacan --> <!-- Image Break: Teotihuacan -->
<div class="image-break image-break-teotihuacan"></div> <div class="image-break image-break-teotihuacan" data-lazy-bg></div>
<!-- Journey Overview --> <!-- Journey Overview -->
<div class="journey"> <div class="journey">
@ -588,29 +665,29 @@
<p class="section-label">Your Journey</p> <p class="section-label">Your Journey</p>
<h2>9 Days of Ceremony & Connection</h2> <h2>9 Days of Ceremony & Connection</h2>
<div class="journey-grid"> <div class="journey-grid">
<div class="journey-card"> <div class="journey-card" data-lazy-bg>
<div class="journey-card-bg" style="background-image: url('https://www.inwardtravel.com/content/image/251216-165925/teotihuacan-mexico-temple-variants/itinerary-33-adventure_itinerary_image/teotihuacan-mexico-temple.png')"></div> <div class="journey-card-bg" data-bg-webp="images/teotihuacan.webp" data-bg-jpg="images/teotihuacan.jpg"></div>
<div class="journey-card-content"> <div class="journey-card-content">
<h3>Arrival & Teotihuacán</h3> <h3>Arrival & Teotihuacán</h3>
<p>Begin in Mexico City with an exploration of the ancient temple complex, setting intentions before journeying into the mountains.</p> <p>Begin in Mexico City with an exploration of the ancient temple complex, setting intentions before journeying into the mountains.</p>
</div> </div>
</div> </div>
<div class="journey-card"> <div class="journey-card" data-lazy-bg>
<div class="journey-card-bg" style="background-image: url('https://www.inwardtravel.com/content/image/251216-203848/mushroom-mazatec-tradition-curandera-mexico-variants/itinerary-35-adventure_itinerary_image_mobile/mushroom-mazatec-tradition-curandera-mexico.png')"></div> <div class="journey-card-bg" data-bg-webp="images/ceremony.webp" data-bg-jpg="images/ceremony.jpg"></div>
<div class="journey-card-content"> <div class="journey-card-content">
<h3>Three Sacred Ceremonies</h3> <h3>Three Sacred Ceremonies</h3>
<p>Experience traditional Mazatec mushroom ceremonies led by Magdalena or Eugenia Casimiro in the village of Huautla de Jiménez.</p> <p>Experience traditional Mazatec mushroom ceremonies led by Magdalena or Eugenia Casimiro in the village of Huautla de Jiménez.</p>
</div> </div>
</div> </div>
<div class="journey-card"> <div class="journey-card" data-lazy-bg>
<div class="journey-card-bg" style="background-image: url('https://www.inwardtravel.com/content/image/251217-210223/oaxaca-tropical-waterfall-bridge-variants/itinerary-40-adventure_itinerary_image_mobile/oaxaca-tropical-waterfall-bridge.jpeg')"></div> <div class="journey-card-bg" data-bg-webp="images/waterfall.webp" data-bg-jpg="images/waterfall.jpg"></div>
<div class="journey-card-content"> <div class="journey-card-content">
<h3>Integration & Exploration</h3> <h3>Integration & Exploration</h3>
<p>Daily yoga, breathwork, hikes to waterfalls, a traditional temazcal ceremony, and visits to María Sabina's sacred sites.</p> <p>Daily yoga, breathwork, hikes to waterfalls, a traditional temazcal ceremony, and visits to María Sabina's sacred sites.</p>
</div> </div>
</div> </div>
<div class="journey-card"> <div class="journey-card" data-lazy-bg>
<div class="journey-card-bg" style="background-image: url('https://www.inwardtravel.com/content/image/251215-124551/huautla-de-imenez-place-of-worship-mountain-view-variants/itinerary-36-adventure_itinerary_image_mobile/huautla-de-imenez-place-of-worship-mountain-view.jpeg')"></div> <div class="journey-card-bg" data-bg-webp="images/community.webp" data-bg-jpg="images/community.jpg"></div>
<div class="journey-card-content"> <div class="journey-card-content">
<h3>Community & Connection</h3> <h3>Community & Connection</h3>
<p>Shared meals, integration circles, and meaningful conversations with fellow travelers in an intimate group setting.</p> <p>Shared meals, integration circles, and meaningful conversations with fellow travelers in an intimate group setting.</p>
@ -622,9 +699,9 @@
<!-- Image Gallery --> <!-- Image Gallery -->
<div class="image-gallery"> <div class="image-gallery">
<div class="image-gallery-item" style="background-image: url('https://www.inwardtravel.com/content/image/251215-125710/mexican-art-maria-sabina-mushrooms-variants/itinerary-37-adventure_itinerary_image_mobile/mexican-art-maria-sabina-mushrooms.jpeg')"></div> <div class="image-gallery-item" data-lazy-bg data-bg-webp="images/sabina-art.webp" data-bg-jpg="images/sabina-art.jpg"></div>
<div class="image-gallery-item" style="background-image: url('https://www.inwardtravel.com/content/image/251216-211822/temazcal-ceremony-mexico-variants/itinerary-38-adventure_itinerary_image_mobile/temazcal-ceremony-mexico.png')"></div> <div class="image-gallery-item" data-lazy-bg data-bg-webp="images/temazcal.webp" data-bg-jpg="images/temazcal.jpg"></div>
<div class="image-gallery-item" style="background-image: url('https://www.inwardtravel.com/content/image/251215-130709/paiting-with-poem-from-maria-sabina-variants/itinerary-39-adventure_itinerary_image_mobile/paiting-with-poem-from-maria-sabina.jpeg')"></div> <div class="image-gallery-item" data-lazy-bg data-bg-webp="images/sabina-poem.webp" data-bg-jpg="images/sabina-poem.jpg"></div>
</div> </div>
<!-- Lineage --> <!-- Lineage -->
@ -648,8 +725,8 @@
</p> </p>
</section> </section>
<!-- Image Break: Ceremony --> <!-- Image Break: Sabina Art -->
<div class="image-break image-break-sabina"></div> <div class="image-break image-break-sabina" data-lazy-bg></div>
<!-- What's Included --> <!-- What's Included -->
<div class="included"> <div class="included">
@ -688,14 +765,17 @@
<!-- CTA --> <!-- CTA -->
<section class="cta-section"> <section class="cta-section">
<h2>Begin Your Journey</h2> <div class="cta-section-bg" data-lazy-bg></div>
<p> <div class="cta-section-content">
Join us for this rare opportunity to experience authentic Mazatec mushroom ceremonies in the mountains of Oaxaca. <h2>Begin Your Journey</h2>
</p> <p>
<a href="https://www.inwardtravel.com/registration/mexico-mushrooms-9-days/trip" class="cta-button">Reserve Your Spot</a> Join us for this rare opportunity to experience authentic Mazatec mushroom ceremonies in the mountains of Oaxaca.
<p class="contact-email"> </p>
Questions? Reach out at <a href="mailto:info@inwardtravel.com">info@inwardtravel.com</a> <a href="https://www.inwardtravel.com/registration/mexico-mushrooms-9-days/trip" class="cta-button">Reserve Your Spot</a>
</p> <p class="contact-email">
Questions? Reach out at <a href="mailto:info@inwardtravel.com">info@inwardtravel.com</a>
</p>
</div>
</section> </section>
<!-- Footer --> <!-- Footer -->
@ -708,5 +788,38 @@
</div> </div>
<p class="copyright">&copy; 2025 Inward Travel. All rights reserved.</p> <p class="copyright">&copy; 2025 Inward Travel. All rights reserved.</p>
</footer> </footer>
<!-- Enhanced lazy loading for inline background images -->
<script>
document.addEventListener('DOMContentLoaded', function() {
var isWebP = document.documentElement.classList.contains('webp');
var lazyItems = document.querySelectorAll('[data-bg-webp]');
if ('IntersectionObserver' in window) {
var imgObserver = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
var el = entry.target;
var src = isWebP ? el.dataset.bgWebp : el.dataset.bgJpg;
if (src) {
el.style.backgroundImage = 'url(' + src + ')';
}
imgObserver.unobserve(el);
}
});
}, { rootMargin: '200px' });
lazyItems.forEach(function(el) {
imgObserver.observe(el);
});
} else {
// Fallback
lazyItems.forEach(function(el) {
var src = isWebP ? el.dataset.bgWebp : el.dataset.bgJpg;
if (src) el.style.backgroundImage = 'url(' + src + ')';
});
}
});
</script>
</body> </body>
</html> </html>