fix: nuclear option - inject video directly into DOM bypassing Vue/Nuxt
All checks were successful
build-website / build (push) Successful in 1m42s

- Remove video from Vue template entirely
- Inject video HTML directly into document.body on mount
- Video now exists completely outside #__nuxt and Vue control
- Bypasses all framework hydration and virtual DOM issues
- Add cleanup on unmount to remove injected element

This nuclear approach ensures the video is rendered as pure HTML/CSS without any framework interference that could be blocking iOS Safari safe area extension.
This commit is contained in:
Matt
2025-09-25 16:39:07 +02:00
parent f36eca27b9
commit dc2b97e5e3

View File

@@ -1,35 +1,6 @@
<template> <template>
<section class="hero-voyage" id="heroSection"> <section class="hero-voyage" id="heroSection">
<!-- Hero video background using safe-area-max-inset-top for iOS Safari --> <!-- Video is now injected directly into DOM to bypass Vue/Nuxt issues -->
<div ref="videoContainer" class="hero-video-container">
<!-- White overlay that fades out when video is ready -->
<div
class="video-white-overlay"
:class="{ 'fade-out': videoLoaded }"
/>
<video
ref="videoElement"
autoplay
loop
muted
playsinline
preload="auto"
class="hero-video"
:class="{ 'video-loaded': videoLoaded }"
@loadeddata="handleVideoLoaded"
@canplaythrough="handleVideoLoaded"
@play="handleVideoLoaded"
>
<source
src="https://videos.pexels.com/video-files/3571264/3571264-uhd_2560_1440_30fps.mp4"
type="video/mp4"
>
</video>
<div class="hero-overlay gradient-warm" />
<div class="hero-overlay gradient-depth" />
</div>
<div class="hero-content" :class="{ 'is-ready': videoLoaded }"> <div class="hero-content" :class="{ 'is-ready': videoLoaded }">
<div class="hero-main"> <div class="hero-main">
@@ -66,7 +37,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { useParallax } from '~/composables/useParallax' import { useParallax } from '~/composables/useParallax'
import { useIntersectionAnimations } from '~/composables/useIntersectionAnimations' import { useIntersectionAnimations } from '~/composables/useIntersectionAnimations'
import { useRipple } from '~/composables/useRipple' import { useRipple } from '~/composables/useRipple'
@@ -78,7 +49,8 @@ const videoContainer = ref<HTMLElement | null>(null)
const videoElement = ref<HTMLVideoElement | null>(null) const videoElement = ref<HTMLVideoElement | null>(null)
const animatedCount = ref(0) const animatedCount = ref(0)
useParallax(videoContainer, 0.5) // Parallax will be disabled for injected video
// useParallax(videoContainer, 0.5)
useIntersectionAnimations() useIntersectionAnimations()
useRipple() useRipple()
@@ -98,25 +70,7 @@ const handleScrollToExplore = () => {
scrollToElement('#services', 800) // Constant-speed scroll to services scrollToElement('#services', 800) // Constant-speed scroll to services
} }
const handleVideoVisibility = () => { // Video visibility handling removed - video is now injected directly into DOM
if (!videoElement.value) {
return
}
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
videoElement.value?.play()
} else {
videoElement.value?.pause()
}
})
}, { threshold: 0.25 })
observer.observe(videoElement.value)
return () => observer.disconnect()
}
const initSmoothScroll = () => { const initSmoothScroll = () => {
document.querySelectorAll('a[href^="#"]').forEach((anchor) => { document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
@@ -158,7 +112,66 @@ const animateCount = () => {
} }
onMounted(() => { onMounted(() => {
handleVideoVisibility() // NUCLEAR OPTION: Inject video directly into DOM, bypassing Vue/Nuxt entirely
const videoHTML = `
<div id="ios-video-bg" style="
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
z-index: -1;
background: #000;
">
<video
id="bg-video-element"
autoplay
muted
playsinline
loop
style="
width: 100%;
height: 100%;
object-fit: cover;
object-position: center top;
">
<source src="https://videos.pexels.com/video-files/3571264/3571264-uhd_2560_1440_30fps.mp4" type="video/mp4">
</video>
<div style="
position: absolute;
inset: 0;
background: linear-gradient(to bottom,
rgba(0, 31, 63, 0.3) 0%,
rgba(0, 31, 63, 0.5) 50%,
rgba(0, 31, 63, 0.7) 100%);
"></div>
</div>
`;
// Insert at the very beginning of body, outside #__nuxt
document.body.insertAdjacentHTML('afterbegin', videoHTML);
// Get reference to injected video
const injectedVideo = document.getElementById('bg-video-element') as HTMLVideoElement;
// Handle video load state
if (injectedVideo) {
injectedVideo.addEventListener('loadeddata', () => {
videoLoaded.value = true;
});
// iOS autoplay fallback
const tryPlay = () => {
if (injectedVideo.paused) {
injectedVideo.play().catch(() => {});
}
};
window.addEventListener('pointerdown', tryPlay, { once: true });
window.addEventListener('touchstart', tryPlay, { once: true });
}
initSmoothScroll() initSmoothScroll()
animateCount() animateCount()
@@ -168,18 +181,11 @@ onMounted(() => {
videoLoaded.value = true videoLoaded.value = true
} }
}, 1500) }, 1500)
})
// iOS autoplay fallback - play on first user interaction onUnmounted(() => {
const tryPlay = () => { // Clean up injected video on component unmount
if (videoElement.value?.paused) { document.getElementById('ios-video-bg')?.remove();
videoElement.value.play().catch(() => {
// Silently fail if autoplay is still blocked
})
}
}
// Listen for any user interaction to trigger video play
window.addEventListener('pointerdown', tryPlay, { once: true })
window.addEventListener('touchstart', tryPlay, { once: true })
}) })
</script> </script>