Introduction

Video content is essential for technical blogs, but embedding videos in static sites like Hugo requires careful consideration of performance, accessibility, and user experience. This guide covers everything from self-hosted videos to external embeds.

The Challenge with Static Site Videos

Static site generators excel at text and images, but video presents unique challenges:

  • File size: Video files are large and can slow down your site
  • Bandwidth costs: Self-hosting videos can be expensive
  • Browser compatibility: Different formats for different browsers
  • Performance: Videos can block page rendering
  • SEO: Proper video markup for search engines

Custom Video Shortcode

I’ve created a flexible Hugo shortcode that handles multiple video sources:

layouts/shortcodes/video.html
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211

{{/* 
Video shortcode for embedding self-hosted and external videos
Usage:
  
  
  
  
  
  
  
  
  
  
  
  <div class="video-container">
    
      
      <video 
        controls
        
        
        poster="/images/video-poster.jpg"
        preload="metadata"
        style="width: 100%; height: auto; max-width: 100%;"
        >
        
        
        
        
        
        
          <source src="/videos/demo.mp4" type="video/mp4">
        
        
        
        <p>Your browser doesn't support HTML5 video. 
           <a href="/videos/demo.mp4">Download the video</a> instead.
        </p>
      </video>
    
    
    
  </div>
  
  <style>
  .video-container {
    margin: 1.5rem 0;
  }
  
  .video-wrapper {
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  }
  
  .video-container video {
    border-radius: 8px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  }
  
  @media (max-width: 768px) {
    .video-container {
      margin: 1rem -1rem;
    }
    
    .video-container video,
    .video-wrapper {
      border-radius: 0;
    }
  }
  </style>
  
  
  
  
  
  
  
  
  
  
  
  <div class="video-container">
    
      
      <div class="video-wrapper" style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
        <iframe 
          src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ?rel=0&showinfo=0&modestbranding=1"
          style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
          frameborder="0"
          allowfullscreen
          loading="lazy"
          title="YouTube video">
        </iframe>
      </div>
    
    
    
  </div>
  
  <style>
  .video-container {
    margin: 1.5rem 0;
  }
  
  .video-wrapper {
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  }
  
  .video-container video {
    border-radius: 8px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  }
  
  @media (max-width: 768px) {
    .video-container {
      margin: 1rem -1rem;
    }
    
    .video-container video,
    .video-wrapper {
      border-radius: 0;
    }
  }
  </style>
  
  
  
  
  
  
  
  
  
  
  
  <div class="video-container">
    
      
      <div class="video-wrapper" style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
        <iframe 
          src="https://player.vimeo.com/video/123456789?dnt=1&title=0&byline=0&portrait=0"
          style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
          frameborder="0"
          allowfullscreen
          loading="lazy"
          title="Vimeo video">
        </iframe>
      </div>
    
    
    
  </div>
  
  <style>
  .video-container {
    margin: 1.5rem 0;
  }
  
  .video-wrapper {
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  }
  
  .video-container video {
    border-radius: 8px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  }
  
  @media (max-width: 768px) {
    .video-container {
      margin: 1rem -1rem;
    }
    
    .video-container video,
    .video-wrapper {
      border-radius: 0;
    }
  }
  </style>
*/}}

{{ $src := .Get "src" }}
{{ $poster := .Get "poster" }}
{{ $youtube := .Get "youtube" }}
{{ $vimeo := .Get "vimeo" }}
{{ $caption := .Get "caption" }}

<div class="video-container">
  {{ if $youtube }}
    <!-- YouTube embed with privacy-enhanced mode -->
    <div class="video-wrapper">
      <iframe 
        src="https://www.youtube-nocookie.com/embed/{{ $youtube }}?rel=0&showinfo=0"
        frameborder="0"
        allowfullscreen
        loading="lazy">
      </iframe>
    </div>
  {{ else if $src }}
    <!-- Self-hosted video with multiple formats -->
    <video controls preload="metadata" poster="{{ $poster | relURL }}">
      <source src="{{ $src | relURL }}" type="video/mp4">
      <p>Your browser doesn't support HTML5 video.</p>
    </video>
  {{ end }}
</div>

Usage Examples

Self-Hosted Videos

For short clips and demos, self-hosting gives you complete control:

1











CDK deployment in action

Best practices for self-hosted videos:

  1. Optimize file size: Use H.264 codec with appropriate bitrate
  2. Multiple formats: Provide WebM for better compression
  3. Poster images: Always include a poster for faster loading
  4. CloudFront delivery: Serve videos through CDN

YouTube Embeds

For longer content, YouTube provides free hosting and global CDN:

1











AWS re:Invent 2024 Keynote Highlights

YouTube embed benefits:

  • Free hosting and bandwidth
  • Automatic quality adaptation
  • Mobile-optimized player
  • Analytics and engagement metrics

Vimeo Embeds

For professional content with better privacy controls:

1











Advanced CDK Patterns Workshop

Video Optimization Strategy

File Preparation

video-optimization.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

#!/bin/bash
# Optimize video files for web delivery

INPUT_FILE="$1"
OUTPUT_DIR="static/videos"

# Create MP4 with H.264 (widely compatible)
ffmpeg -i "$INPUT_FILE" \
  -c:v libx264 \
  -preset slow \
  -crf 23 \
  -c:a aac \
  -b:a 128k \
  -movflags +faststart \
  "$OUTPUT_DIR/$(basename "$INPUT_FILE" .mov).mp4"

# Create WebM with VP9 (better compression)
ffmpeg -i "$INPUT_FILE" \
  -c:v libvpx-vp9 \
  -crf 30 \
  -b:v 0 \
  -c:a libopus \
  -b:a 128k \
  "$OUTPUT_DIR/$(basename "$INPUT_FILE" .mov).webm"

# Generate poster image
ffmpeg -i "$INPUT_FILE" \
  -ss 00:00:02 \
  -vframes 1 \
  -q:v 2 \
  "static/images/$(basename "$INPUT_FILE" .mov)-poster.jpg"

CloudFront Configuration

For self-hosted videos, configure CloudFront for optimal delivery:

cloudfront-video-config.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20

// CloudFront behavior for video files
{
  pathPattern: "*.mp4",
  origin: new origins.S3Origin(bucket),
  viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
  cachePolicy: new cloudfront.CachePolicy(this, 'VideoCachePolicy', {
    cachePolicyName: 'VideoOptimized',
    defaultTtl: Duration.days(30),
    maxTtl: Duration.days(365),
    minTtl: Duration.days(1),
    // Enable byte-range requests for video seeking
    headerBehavior: cloudfront.CacheHeaderBehavior.allowList(
      'Range', 
      'If-Range'
    )
  }),
  compress: false, // Don't compress video files
  allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS
}

Performance Considerations

Lazy Loading

The shortcode includes loading="lazy" for iframe embeds, but for self-hosted videos:

lazy-video.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

<video 
  controls 
  preload="none"
  poster="{{ $poster }}"
  data-src="{{ $src }}"
  class="lazy-video">
  <source data-src="{{ $src }}" type="video/mp4">
</video>

<script>
// Intersection Observer for lazy video loading
const lazyVideos = document.querySelectorAll('.lazy-video');
const videoObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const video = entry.target;
      video.src = video.dataset.src;
      video.load();
      videoObserver.unobserve(video);
    }
  });
});

lazyVideos.forEach(video => videoObserver.observe(video));
</script>

Bandwidth Optimization

  1. Adaptive bitrate: Use multiple quality versions
  2. Format selection: Serve WebM to supporting browsers
  3. CDN delivery: Always use CloudFront for global delivery
  4. Compression: Optimize without sacrificing quality

SEO and Accessibility

Video Schema Markup

Add structured data for better SEO:

video-schema.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "VideoObject",
  "name": "{{ $caption }}",
  "description": "{{ .Page.Summary }}",
  "thumbnailUrl": "{{ $poster | absURL }}",
  "uploadDate": "{{ .Page.Date.Format "2006-01-02T15:04:05Z07:00" }}",
  "contentUrl": "{{ $src | absURL }}",
  "embedUrl": "{{ .Page.Permalink }}",
  "duration": "PT2M30S"
}
</script>

Accessibility Features

  • Captions: Always provide captions for accessibility
  • Transcripts: Include text transcripts below videos
  • Keyboard navigation: Ensure video controls are keyboard accessible
  • Screen reader support: Proper ARIA labels and descriptions

Cost Analysis

Self-Hosted vs External

Self-hosted costs (AWS):

  • S3 storage: $0.023/GB/month
  • CloudFront data transfer: $0.085/GB (first 10TB)
  • Requests: $0.0075/10,000 requests

External hosting:

  • YouTube: Free (with ads) or YouTube Premium
  • Vimeo: $7/month for basic, $20/month for Pro

Recommendation

  • Short demos (<2 minutes): Self-host with CloudFront
  • Tutorials (>5 minutes): YouTube for reach, Vimeo for professional
  • Marketing content: YouTube for SEO benefits
  • Internal training: Self-host for privacy

Advanced Techniques

Progressive Enhancement

Start with a poster image and load video on interaction:

progressive-video.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

class ProgressiveVideo {
  constructor(element) {
    this.container = element;
    this.poster = element.querySelector('.video-poster');
    this.videoData = JSON.parse(element.dataset.video);
    
    this.poster.addEventListener('click', () => this.loadVideo());
  }
  
  loadVideo() {
    const video = document.createElement('video');
    video.controls = true;
    video.autoplay = true;
    video.src = this.videoData.src;
    
    this.container.replaceChild(video, this.poster);
  }
}

// Initialize progressive videos
document.querySelectorAll('.progressive-video')
  .forEach(el => new ProgressiveVideo(el));

Video Analytics

Track video engagement with custom events:

video-analytics.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

function trackVideoEvents(video) {
  let quartiles = [25, 50, 75, 100];
  let trackedQuartiles = [];
  
  video.addEventListener('play', () => {
    gtag('event', 'video_start', {
      video_title: video.dataset.title,
      video_url: video.src
    });
  });
  
  video.addEventListener('timeupdate', () => {
    const percent = (video.currentTime / video.duration) * 100;
    
    quartiles.forEach(quartile => {
      if (percent >= quartile && !trackedQuartiles.includes(quartile)) {
        trackedQuartiles.push(quartile);
        gtag('event', 'video_progress', {
          video_title: video.dataset.title,
          progress: quartile
        });
      }
    });
  });
}

Conclusion

Video embedding in Hugo requires balancing performance, cost, and user experience. The custom shortcode approach gives you flexibility while maintaining optimal performance through:

  1. Smart loading strategies: Lazy loading and progressive enhancement
  2. Multiple format support: MP4 for compatibility, WebM for efficiency
  3. CDN optimization: CloudFront configuration for global delivery
  4. SEO integration: Structured data and accessibility features

Choose your video strategy based on content type, audience, and budget. Self-host short demos, use YouTube for reach, and Vimeo for professional content.


Want to see more Hugo advanced techniques? Follow me on LinkedIn for updates on static site optimization.