Hands-on examples of implementing JavaScript functionality in HBStack using Hugo shortcodes, including a working Hello World demonstration troubleshooting tips and real-world integration patterns.
This guide provides practical, working examples of JavaScript implementation in HBStack. From a simple Hello World demonstration to advanced integration patterns, you'll see exactly how to build interactive features using Hugo shortcodes and the HBStack framework.
Let’s start with a simple example to demonstrate how custom JavaScript works in HBStack. Below is a live demonstration of a basic JavaScript file being loaded using Hugo shortcodes.
The script assets/js/helloworld.js is loaded on this page using a Hugo shortcode. Here’s how the complete implementation works:
assets/js/helloworld.js) 1document.addEventListener('DOMContentLoaded', () => {
2 console.log('Hello from custom.js')
3
4 // Create a visible notification to show the script is working
5 showHelloWorldNotification()
6
7 // Add click handler for demo button if it exists
8 const demoButton = document.getElementById('hello-demo-btn')
9 if (demoButton) {
10 demoButton.addEventListener('click', function () {
11 alert('Hello World! The JavaScript is working correctly.')
12 console.log('Demo button clicked at:', new Date().toLocaleTimeString())
13 })
14 }
15})
16
17function showHelloWorldNotification() {
18 // Create a temporary notification
19 const notification = document.createElement('div')
20 notification.className = 'helloworld-notification'
21
22 notification.innerHTML = `
23 <strong>✓ Hello World Script Loaded!</strong><br>
24 <small>Check the console for more details</small>
25 `
26
27 document.body.appendChild(notification)
28
29 // Remove notification after 4 seconds
30 setTimeout(() => {
31 notification.classList.add('slide-out')
32 setTimeout(() => {
33 if (notification.parentNode) {
34 notification.parentNode.removeChild(notification)
35 }
36 }, 300)
37 }, 4000)
38}
assets/scss/helloworld.scss) 1// Hello World notification styles
2.helloworld-notification {
3 position: fixed;
4 top: 20px;
5 right: 20px;
6 background: #28a745;
7 color: white;
8 padding: 15px 20px;
9 border-radius: 5px;
10 z-index: 9999;
11 font-family:
12 system-ui,
13 -apple-system,
14 sans-serif;
15 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
16 animation: slideIn 0.3s ease-out;
17
18 strong {
19 display: block;
20 margin-bottom: 4px;
21 }
22
23 small {
24 color: rgba(255, 255, 255, 0.9);
25 }
26}
27
28// Animation keyframes
29@keyframes slideIn {
30 from {
31 transform: translateX(100%);
32 opacity: 0;
33 }
34 to {
35 transform: translateX(0);
36 opacity: 1;
37 }
38}
39
40// Animation for hiding notification
41.helloworld-notification.slide-out {
42 animation: slideIn 0.3s ease-out reverse;
43}
layouts/shortcodes/helloworld.html)1{{ partial "helloworld.html" . }}
layouts/partials/helloworld.html)1{{- /* Process SCSS file */ -}} {{- $customSCSS := resources.Get
2"scss/helloworld.scss" -}} {{- if $customSCSS -}} {{- $customSCSS = $customSCSS
3| toCSS | minify -}}
4<link rel="stylesheet" href="{{ $customSCSS.RelPermalink }}" />
5{{- end -}} {{- /* Process JavaScript file */ -}} {{- $customJS := resources.Get
6"js/helloworld.js" -}} {{- if $customJS -}} {{- $customJS = $customJS | js.Build
7| minify -}}
8<script src="{{ $customJS.RelPermalink }}" defer></script>
9{{- end -}}
1{{< helloworld >}}
assets/ directory✅ Recommended Pattern:
layouts/shortcodes/helloworld.html): Simple interface that calls a partiallayouts/partials/helloworld.html): Contains the actual implementation logic❌ Avoid:
Benefits of This Pattern:
File Structure:
1assets/
2├── js/
3│ └── helloworld.js # JavaScript logic only
4└── scss/
5 └── helloworld.scss # Styles only
Benefits of Asset Separation:
Hugo Asset Processing:
toCSS filter compiles SCSS to CSSjs.Build processes imports and modern JS featuresClick the button below to test the JavaScript interaction:
Problem: Using {{< partial "filename.html" . >}} in markdown content.
Solution: Create a proper Hugo shortcode that calls a partial instead of trying to call partials directly from markdown.
1# Create the shortcode file
2mkdir -p layouts/shortcodes
3mkdir -p layouts/partials
File: layouts/shortcodes/your-script.html
1{{ partial "your-script.html" . }}
File: layouts/partials/your-script.html
1{{- /* Process SCSS file */ -}} {{- $customSCSS := resources.Get
2"scss/your-script.scss" -}} {{- if $customSCSS -}} {{- $customSCSS = $customSCSS
3| toCSS | minify -}}
4<link rel="stylesheet" href="{{ $customSCSS.RelPermalink }}" />
5{{- end -}} {{- /* Process JavaScript file */ -}} {{- $customJS := resources.Get
6"js/your-script.js" -}} {{- if $customJS -}} {{- $customJS = $customJS |
7js.Build | minify -}}
8<script src="{{ $customJS.RelPermalink }}" defer></script>
9{{- end -}}
Then use in markdown:
1{{< your-script >}}
Symptoms: No console messages, no visual effects.
Debugging Steps:
assets/js/Common Causes:
Solution: Always wrap your code in DOMContentLoaded:
1document.addEventListener('DOMContentLoaded', () => {
2 // Your code here
3})
Problem: “failed to extract shortcode” or similar build errors.
Solutions:
1{{< shortcode-name >}}
layouts/shortcodes/Symptoms: JavaScript works but styles don’t apply, elements appear unstyled.
Debugging Steps:
assets/scss/ directoryCommon SCSS Issues:
1// ❌ Wrong - missing semicolon
2.notification {
3 position: fixed
4 top: 20px;
5}
6
7// ✅ Correct
8.notification {
9 position: fixed;
10 top: 20px;
11}
Hugo SCSS Processing:
toCSS filter is applied in your partialProblem: JavaScript creates elements but CSS classes don’t work.
Debugging Steps:
Example Debug Pattern:
1// Add debugging to verify classes
2const notification = document.createElement('div')
3notification.className = 'helloworld-notification'
4
5// Debug: Check if element has the class
6console.log('Element classes:', notification.className)
7console.log('Element in DOM:', document.contains(notification))
1// Enhance HBStack's search functionality
2import { utils } from './utils'
3
4interface SearchResult {
5 title: string
6 url: string
7 excerpt: string
8}
9
10class CustomSearch {
11 private searchIndex: SearchResult[] = []
12
13 async init() {
14 try {
15 const response = await fetch('/search-index.json')
16 this.searchIndex = await response.json()
17 this.setupSearch()
18 } catch (error) {
19 console.error('Failed to load search index:', error)
20 }
21 }
22
23 private setupSearch() {
24 const searchInput = document.querySelector(
25 '#search-input'
26 ) as HTMLInputElement
27 if (searchInput) {
28 searchInput.addEventListener(
29 'input',
30 utils.throttle(this.performSearch.bind(this), 300)
31 )
32 }
33 }
34
35 private performSearch(event: Event) {
36 const query = (event.target as HTMLInputElement).value.toLowerCase()
37 const results = this.searchIndex.filter(
38 item =>
39 item.title.toLowerCase().includes(query) ||
40 item.excerpt.toLowerCase().includes(query)
41 )
42 this.displayResults(results)
43 }
44
45 private displayResults(results: SearchResult[]) {
46 const container = document.querySelector('#search-results')
47 if (container) {
48 container.innerHTML = results
49 .map(
50 result => `
51 <div class="search-result">
52 <h3><a href="${result.url}">${result.title}</a></h3>
53 <p>${result.excerpt}</p>
54 </div>
55 `
56 )
57 .join('')
58 }
59 }
60}
61
62// Initialize when DOM is ready
63document.addEventListener('DOMContentLoaded', () => {
64 new CustomSearch().init()
65})
1// Custom analytics tracking for HBStack
2interface AnalyticsEvent {
3 category: string
4 action: string
5 label?: string
6 value?: number
7}
8
9class CustomAnalytics {
10 private trackingId: string
11
12 constructor(trackingId: string) {
13 this.trackingId = trackingId
14 this.init()
15 }
16
17 private init() {
18 // Track page views
19 this.trackPageView()
20
21 // Track scroll depth
22 this.trackScrollDepth()
23
24 // Track click events
25 this.trackClicks()
26 }
27
28 private trackEvent(event: AnalyticsEvent) {
29 // Send to your analytics service
30 console.log('Analytics Event:', event)
31
32 // Example: Google Analytics 4
33 if (typeof gtag !== 'undefined') {
34 gtag('event', event.action, {
35 event_category: event.category,
36 event_label: event.label,
37 value: event.value
38 })
39 }
40 }
41
42 private trackPageView() {
43 this.trackEvent({
44 category: 'page',
45 action: 'view',
46 label: window.location.pathname
47 })
48 }
49
50 private trackScrollDepth() {
51 let maxScroll = 0
52 const milestones = [25, 50, 75, 100]
53
54 window.addEventListener(
55 'scroll',
56 utils.throttle(() => {
57 const scrollPercent = Math.round(
58 (window.scrollY / (document.body.scrollHeight - window.innerHeight)) *
59 100
60 )
61
62 if (scrollPercent > maxScroll) {
63 maxScroll = scrollPercent
64
65 milestones.forEach(milestone => {
66 if (scrollPercent >= milestone && maxScroll < milestone + 5) {
67 this.trackEvent({
68 category: 'scroll',
69 action: 'depth',
70 label: `${milestone}%`,
71 value: milestone
72 })
73 }
74 })
75 }
76 }, 1000)
77 )
78 }
79
80 private trackClicks() {
81 document.addEventListener('click', e => {
82 const target = e.target as HTMLElement
83
84 // Track external links
85 if (
86 target.tagName === 'A' &&
87 target.getAttribute('href')?.startsWith('http')
88 ) {
89 this.trackEvent({
90 category: 'link',
91 action: 'external_click',
92 label: target.getAttribute('href') || ''
93 })
94 }
95
96 // Track button clicks
97 if (target.tagName === 'BUTTON' || target.classList.contains('btn')) {
98 this.trackEvent({
99 category: 'button',
100 action: 'click',
101 label: target.textContent?.trim() || ''
102 })
103 }
104 })
105 }
106}
107
108// Initialize analytics
109document.addEventListener('DOMContentLoaded', () => {
110 new CustomAnalytics('YOUR_TRACKING_ID')
111})
1// Progressive enhancement example
2class ProgressiveFeature {
3 constructor() {
4 this.init()
5 }
6
7 init() {
8 // Check for required features before enhancing
9 if (this.isSupported()) {
10 this.enhance()
11 } else {
12 this.fallback()
13 }
14 }
15
16 isSupported() {
17 return (
18 'IntersectionObserver' in window &&
19 'fetch' in window &&
20 'classList' in document.documentElement
21 )
22 }
23
24 enhance() {
25 console.log('Enhanced features available')
26
27 // Modern functionality
28 this.setupIntersectionObserver()
29 this.setupModernFeatures()
30 }
31
32 fallback() {
33 console.log('Using fallback functionality')
34
35 // Basic functionality for older browsers
36 this.setupBasicFeatures()
37 }
38
39 setupIntersectionObserver() {
40 const observer = new IntersectionObserver(entries => {
41 entries.forEach(entry => {
42 if (entry.isIntersecting) {
43 entry.target.classList.add('visible')
44 }
45 })
46 })
47
48 document.querySelectorAll('.animate-on-scroll').forEach(el => {
49 observer.observe(el)
50 })
51 }
52
53 setupModernFeatures() {
54 // Use modern JavaScript features
55 document.querySelectorAll('[data-enhance]').forEach(element => {
56 element.addEventListener('click', this.handleModernClick.bind(this))
57 })
58 }
59
60 setupBasicFeatures() {
61 // Basic functionality without modern APIs
62 const elements = document.querySelectorAll('[data-enhance]')
63 for (let i = 0; i < elements.length; i++) {
64 elements[i].addEventListener('click', this.handleBasicClick.bind(this))
65 }
66 }
67
68 handleModernClick(event) {
69 event.preventDefault()
70 // Modern implementation
71 console.log('Modern click handler')
72 }
73
74 handleBasicClick(event) {
75 event.preventDefault()
76 // Basic implementation
77 console.log('Basic click handler')
78 }
79}
80
81// Initialize
82document.addEventListener('DOMContentLoaded', () => {
83 new ProgressiveFeature()
84})
1// Efficient lazy loading with Intersection Observer
2class LazyLoader {
3 constructor() {
4 this.imageObserver = null
5 this.config = {
6 rootMargin: '50px 0px',
7 threshold: 0.01
8 }
9 this.init()
10 }
11
12 init() {
13 if ('IntersectionObserver' in window) {
14 this.setupObserver()
15 } else {
16 this.loadAllImages()
17 }
18 }
19
20 setupObserver() {
21 this.imageObserver = new IntersectionObserver(entries => {
22 entries.forEach(entry => {
23 if (entry.isIntersecting) {
24 this.loadImage(entry.target)
25 this.imageObserver.unobserve(entry.target)
26 }
27 })
28 }, this.config)
29
30 this.observeImages()
31 }
32
33 observeImages() {
34 const images = document.querySelectorAll('img[data-src]')
35 images.forEach(img => this.imageObserver.observe(img))
36 }
37
38 loadImage(img) {
39 img.src = img.dataset.src
40 img.classList.remove('lazy')
41 img.classList.add('loaded')
42 }
43
44 loadAllImages() {
45 // Fallback for browsers without Intersection Observer
46 const images = document.querySelectorAll('img[data-src]')
47 images.forEach(img => this.loadImage(img))
48 }
49}
50
51// Initialize lazy loading
52document.addEventListener('DOMContentLoaded', () => {
53 new LazyLoader()
54})
Create layouts/shortcodes/interactive-demo.html:
1{{- $id := .Get "id" | default (printf "demo-%d" now.Unix) -}} {{- $type := .Get
2"type" | default "button" -}} {{- $message := .Get "message" | default "Demo
3activated!" -}}
4
5<div class="interactive-demo mb-4" data-demo-id="{{ $id }}">
6 <div class="demo-controls mb-3">
7 {{- if eq $type "button" -}}
8 <button
9 id="{{ $id }}-btn"
10 class="btn btn-primary demo-trigger"
11 data-message="{{ $message }}"
12 >
13 <i class="fas fa-play me-2"></i>Run Demo
14 </button>
15 {{- else if eq $type "form" -}}
16 <form class="demo-form" data-demo-id="{{ $id }}">
17 <div class="input-group">
18 <input
19 type="text"
20 class="form-control"
21 placeholder="Enter some text..."
22 id="{{ $id }}-input"
23 />
24 <button class="btn btn-primary" type="submit">Test</button>
25 </div>
26 </form>
27 {{- end -}}
28 </div>
29
30 <div id="{{ $id }}-output" class="demo-output border p-3 bg-light d-none">
31 <strong>Demo Output:</strong>
32 <div class="output-content mt-2"></div>
33 </div>
34</div>
35
36{{- $demoJS := resources.Get "js/interactive-demo.js" -}} {{- if $demoJS -}} {{-
37$demoJS = $demoJS | js.Build | minify -}}
38<script src="{{ $demoJS.RelPermalink }}" defer></script>
39{{- end -}}
And the corresponding JavaScript file assets/js/interactive-demo.js:
1class InteractiveDemo {
2 constructor() {
3 this.init()
4 }
5
6 init() {
7 document.addEventListener('click', this.handleButtonDemo.bind(this))
8 document.addEventListener('submit', this.handleFormDemo.bind(this))
9 }
10
11 handleButtonDemo(event) {
12 if (event.target.matches('.demo-trigger')) {
13 event.preventDefault()
14 const button = event.target
15 const demoId = button.id.replace('-btn', '')
16 const message = button.dataset.message
17
18 this.showOutput(demoId, `Button clicked! Message: ${message}`)
19 }
20 }
21
22 handleFormDemo(event) {
23 if (event.target.matches('.demo-form')) {
24 event.preventDefault()
25 const form = event.target
26 const demoId = form.dataset.demoId
27 const input = form.querySelector('input')
28 const value = input.value
29
30 this.showOutput(demoId, `Form submitted with value: "${value}"`)
31 }
32 }
33
34 showOutput(demoId, content) {
35 const outputDiv = document.getElementById(`${demoId}-output`)
36 const contentDiv = outputDiv.querySelector('.output-content')
37
38 contentDiv.innerHTML = content
39 outputDiv.classList.remove('d-none')
40
41 // Auto-hide after 5 seconds
42 setTimeout(() => {
43 outputDiv.classList.add('d-none')
44 }, 5000)
45 }
46}
47
48// Initialize
49document.addEventListener('DOMContentLoaded', () => {
50 new InteractiveDemo()
51})
Common issues include:
/assets/js/ directorydata-bs- attributes instead of data-DOMContentLoaded or use Hugo’s page loading events/layouts/shortcodes/yourname.html.Page.Scratch for data passingExample:
1<!-- layouts/shortcodes/interactive.html -->
2<div id="interactive-{{ .Get 0 }}" class="interactive-widget">
3 {{ .Inner }}
4</div>
5<script>
6 document.addEventListener('DOMContentLoaded', function() {
7 // Your JavaScript here
8 });
9</script>
Option 1: Hugo Modules (Recommended)
1# hugo.yaml
2module:
3 imports:
4 - path: github.com/user/library-module
Option 2: CDN with SRI
1<!-- layouts/partials/head/custom.html -->
2<script src="https://cdn.example.com/library.js"
3 integrity="sha384-..."
4 crossorigin="anonymous"></script>
Option 3: Local Assets
/assets/js/vendor/Development Setup:
hugo server -D --disableFastRenderCommon Debug Steps:
Yes! HBStack supports modern JavaScript through Hugo’s asset pipeline:
Configuration:
1# hugo.yaml
2build:
3 buildStats:
4 enable: true
5params:
6 hb:
7 js:
8 transpiler: esbuild # or babel
Features Available:
Bundle and Minify:
1<!-- layouts/partials/footer/custom.html -->
2{{ $js := resources.Get "js/main.js" | js.Build | minify }}
3<script src="{{ $js.RelPermalink }}"></script>
Lazy Loading:
Caching Strategy:
HBStack provides several custom events you can listen for:
1// Page navigation events
2document.addEventListener('hb:page:loaded', function(e) {
3 // Triggered when page content loads
4});
5
6// Theme switching
7document.addEventListener('hb:theme:changed', function(e) {
8 // Triggered when dark/light theme changes
9 console.log('New theme:', e.detail.theme);
10});
11
12// Search events
13document.addEventListener('hb:search:opened', function(e) {
14 // Triggered when search modal opens
15});
Language-Specific Scripts:
1<!-- layouts/partials/head/custom.html -->
2{{ if eq .Site.Language.Lang "en" }}
3 {{ $js := resources.Get "js/en-specific.js" }}
4 <script src="{{ $js.RelPermalink }}"></script>
5{{ end }}
Internationalization in JS:
1// Access Hugo's language data
2const lang = document.documentElement.lang;
3const translations = {
4 'en': { 'hello': 'Hello' },
5 'es': { 'hello': 'Hola' }
6};
Limited Integration: HBStack is primarily a Hugo theme, but you can:
1<!-- In your content or shortcode -->
2<div id="react-component"></div>
3<script>
4 // Mount React component to #react-component
5</script>
Note: Full SPA integration isn’t recommended as it conflicts with Hugo’s static nature.
Core Principle: Ensure content works without JavaScript, then enhance with JS:
1<!-- Basic HTML structure -->
2<div class="accordion" data-enhance="accordion">
3 <details open>
4 <summary>Section 1</summary>
5 <p>Content that works without JS</p>
6 </details>
7</div>
8
9<script>
10// Progressive enhancement
11document.querySelectorAll('[data-enhance="accordion"]').forEach(el => {
12 // Convert details/summary to fancy accordion
13 enhanceAccordion(el);
14});
15</script>