A comprehensive guide on how to use JavaScript and custom scripts in the hbstack framework, covering the assets folder structure and custom script implementation. And when to use the assets/hb folder structure.
A comprehensive guide on how to use JavaScript and custom scripts in the HBStack framework, covering the assets folder structure, custom script implementation, and when to use the assets/hb folder structure.
HBStack provides a powerful and flexible system for managing JavaScript files, CSS assets, and custom scripts. Understanding the proper folder structure and implementation patterns is crucial for extending your HBStack-powered Hugo site.
The assets/ folder in HBStack follows a specific organization pattern that Hugo uses for asset processing and bundling.
1assets/
2├── css/ # Custom CSS files
3│ ├── main.css # Site-wide custom styles
4│ └── components/ # Component-specific styles
5├── js/ # Custom JavaScript files
6│ ├── main.js # Site-wide custom scripts
7│ └── modules/ # Modular JavaScript files
8├── images/ # Static images and icons
9│ ├── logo.png
10│ └── icons/
11├── hb/ # HBStack-specific assets
12│ └── modules/ # HB module customizations
13└── fonts/ # Custom web fonts
| Folder | Purpose | Use Cases |
|---|---|---|
assets/css/ | Custom stylesheets | Site-wide styles, component overrides, theme customizations |
assets/js/ | General JavaScript | Site functionality, third-party integrations, utilities |
assets/hb/ | HBStack modules | Framework-specific customizations, module extensions |
assets/images/ | Image assets | Logos, backgrounds, content images |
assets/fonts/ | Typography | Custom web fonts, icon fonts |
assets/hb FolderThe assets/hb/ folder is specifically designed for HBStack framework customizations and module extensions.
1assets/hb/
2└── modules/
3 ├── custom/ # Your custom HB modules
4 │ ├── js/
5 │ │ └── index.ts # Main custom script entry
6 │ └── scss/
7 │ └── index.scss # Custom styles
8 ├── header/ # Header module customizations
9 ├── footer/ # Footer module customizations
10 └── blog/ # Blog module customizations
assets/hbUse the assets/hb/ folder when you need to:
assets/hb/modules/custom/This is the recommended approach for HBStack-specific customizations.
1mkdir -p assets/hb/modules/custom/js
2mkdir -p assets/hb/modules/custom/scss
Create assets/hb/modules/custom/js/index.ts:
1// Main custom script entry point
2console.log('HBStack custom module loaded')
3
4// Example: Add custom functionality
5document.addEventListener('DOMContentLoaded', function () {
6 // Your custom JavaScript code here
7 initCustomFeatures()
8})
9
10function initCustomFeatures() {
11 // Custom navigation enhancements
12 setupSmoothScrolling()
13
14 // Custom form handling
15 setupFormValidation()
16
17 // Custom analytics tracking
18 setupCustomTracking()
19}
20
21function setupSmoothScrolling() {
22 const links = document.querySelectorAll('a[href^="#"]')
23 links.forEach(link => {
24 link.addEventListener('click', function (e) {
25 e.preventDefault()
26 const target = document.querySelector(this.getAttribute('href'))
27 if (target) {
28 target.scrollIntoView({
29 behavior: 'smooth',
30 block: 'start'
31 })
32 }
33 })
34 })
35}
36
37function setupFormValidation() {
38 const forms = document.querySelectorAll('form[data-validate]')
39 forms.forEach(form => {
40 form.addEventListener('submit', function (e) {
41 if (!validateForm(this)) {
42 e.preventDefault()
43 }
44 })
45 })
46}
47
48function validateForm(form: HTMLFormElement): boolean {
49 const requiredFields = form.querySelectorAll('[required]')
50 let isValid = true
51
52 requiredFields.forEach(field => {
53 if (!(field as HTMLInputElement).value.trim()) {
54 isValid = false
55 field.classList.add('is-invalid')
56 } else {
57 field.classList.remove('is-invalid')
58 }
59 })
60
61 return isValid
62}
63
64function setupCustomTracking() {
65 // Example: Custom event tracking
66 document.addEventListener('click', function (e) {
67 const target = e.target as HTMLElement
68 if (target.matches('[data-track]')) {
69 const event = target.getAttribute('data-track')
70 console.log(`Tracking event: ${event}`)
71 // Send to your analytics service
72 }
73 })
74}
75
76// Export functions for use in other modules
77export {
78 initCustomFeatures,
79 setupSmoothScrolling,
80 setupFormValidation,
81 setupCustomTracking
82}
Create assets/hb/modules/custom/scss/index.scss:
1// Custom styles for your HBStack site
2.custom-feature {
3 background: var(--bs-primary);
4 color: white;
5 padding: 1rem;
6 border-radius: 0.5rem;
7
8 &:hover {
9 background: var(--bs-primary-dark);
10 transform: translateY(-2px);
11 transition: all 0.3s ease;
12 }
13}
14
15// Form validation styles
16.is-invalid {
17 border-color: var(--bs-danger) !important;
18 box-shadow: 0 0 0 0.2rem rgba(var(--bs-danger-rgb), 0.25);
19}
20
21// Smooth scroll enhancement
22html {
23 scroll-behavior: smooth;
24}
25
26// Custom button styles
27.btn-custom {
28 background: linear-gradient(45deg, var(--bs-primary), var(--bs-secondary));
29 border: none;
30 color: white;
31
32 &:hover {
33 transform: scale(1.05);
34 transition: transform 0.2s ease;
35 }
36}
assets/js/ for General ScriptsFor non-HBStack specific JavaScript, use the standard assets/js/ folder.
Create assets/js/custom.js:
1// General purpose custom JavaScript
2;(function () {
3 'use strict'
4
5 // Initialize when DOM is ready
6 document.addEventListener('DOMContentLoaded', function () {
7 console.log('Custom scripts loaded')
8
9 // Example: Image lazy loading
10 setupLazyLoading()
11
12 // Example: Custom search functionality
13 setupCustomSearch()
14
15 // Example: Social media integration
16 setupSocialShare()
17 })
18
19 function setupLazyLoading() {
20 if ('IntersectionObserver' in window) {
21 const imageObserver = new IntersectionObserver((entries, observer) => {
22 entries.forEach(entry => {
23 if (entry.isIntersecting) {
24 const img = entry.target
25 img.src = img.dataset.src
26 img.classList.remove('lazy')
27 imageObserver.unobserve(img)
28 }
29 })
30 })
31
32 document.querySelectorAll('img[data-src]').forEach(img => {
33 imageObserver.observe(img)
34 })
35 }
36 }
37
38 function setupCustomSearch() {
39 const searchInput = document.querySelector('#custom-search')
40 if (searchInput) {
41 searchInput.addEventListener(
42 'input',
43 debounce(function (e) {
44 const query = e.target.value
45 performSearch(query)
46 }, 300)
47 )
48 }
49 }
50
51 function setupSocialShare() {
52 document.querySelectorAll('[data-share]').forEach(button => {
53 button.addEventListener('click', function (e) {
54 e.preventDefault()
55 const platform = this.dataset.share
56 const url = encodeURIComponent(window.location.href)
57 const title = encodeURIComponent(document.title)
58
59 const shareUrls = {
60 twitter: `https://twitter.com/intent/tweet?url=${url}&text=${title}`,
61 facebook: `https://www.facebook.com/sharer/sharer.php?u=${url}`,
62 linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${url}`
63 }
64
65 if (shareUrls[platform]) {
66 window.open(shareUrls[platform], '_blank', 'width=600,height=400')
67 }
68 })
69 })
70 }
71
72 function debounce(func, wait) {
73 let timeout
74 return function executedFunction(...args) {
75 const later = () => {
76 clearTimeout(timeout)
77 func(...args)
78 }
79 clearTimeout(timeout)
80 timeout = setTimeout(later, wait)
81 }
82 }
83
84 function performSearch(query) {
85 // Implement your custom search logic here
86 console.log('Searching for:', query)
87 }
88})()
Create a partial template layouts/partials/custom-scripts.html:
1{{- $customJS := resources.Get "js/custom.js" -}} {{- if $customJS -}} {{-
2$customJS = $customJS | js.Build | minify -}}
3<script src="{{ $customJS.RelPermalink }}" defer></script>
4{{- end -}} {{- $hbCustom := resources.Get "hb/modules/custom/js/index.ts" -}}
5{{- if $hbCustom -}} {{- $hbCustom = $hbCustom | js.Build | minify -}}
6<script src="{{ $hbCustom.RelPermalink }}" defer></script>
7{{- end -}}
Add to your layout templates or config:
1<!-- In layouts/_default/baseof.html or specific templates -->
2<script src="{{ "js/custom.js" | relURL }}" defer></script>
Add to config/_default/params.yaml:
1hb:
2 custom_js:
3 - 'js/custom.js'
4 - 'js/analytics.js'
5 - 'js/third-party.js'
HBStack supports TypeScript out of the box. Create .ts files in assets/hb/modules/custom/js/:
1// assets/hb/modules/custom/js/advanced.ts
2interface CustomConfig {
3 apiUrl: string
4 timeout: number
5 retries: number
6}
7
8class CustomAPI {
9 private config: CustomConfig
10
11 constructor(config: CustomConfig) {
12 this.config = config
13 }
14
15 async fetchData(endpoint: string): Promise<any> {
16 try {
17 const response = await fetch(`${this.config.apiUrl}/${endpoint}`, {
18 signal: AbortSignal.timeout(this.config.timeout)
19 })
20
21 if (!response.ok) {
22 throw new Error(`HTTP error! status: ${response.status}`)
23 }
24
25 return await response.json()
26 } catch (error) {
27 console.error('API fetch error:', error)
28 throw error
29 }
30 }
31}
32
33// Export for use in other modules
34export { CustomAPI, CustomConfig }
Create assets/hb/modules/custom/js/package.json:
1{
2 "name": "hbstack-custom-module",
3 "version": "1.0.0",
4 "description": "Custom HBStack module",
5 "main": "index.ts",
6 "dependencies": {
7 "@types/node": "^20.0.0"
8 }
9}
Create advanced styles in assets/hb/modules/custom/scss/:
1// assets/hb/modules/custom/scss/variables.scss
2:root {
3 --custom-primary: #007bff;
4 --custom-secondary: #6c757d;
5 --custom-success: #28a745;
6 --custom-danger: #dc3545;
7 --custom-warning: #ffc107;
8 --custom-info: #17a2b8;
9}
10
11// assets/hb/modules/custom/scss/mixins.scss
12@mixin button-variant($bg-color, $text-color: white) {
13 background-color: $bg-color;
14 color: $text-color;
15 border: 1px solid $bg-color;
16
17 &:hover {
18 background-color: darken($bg-color, 10%);
19 border-color: darken($bg-color, 10%);
20 }
21
22 &:focus {
23 box-shadow: 0 0 0 0.2rem rgba($bg-color, 0.25);
24 }
25}
26
27// assets/hb/modules/custom/scss/components.scss
28.custom-card {
29 border-radius: 0.5rem;
30 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
31 transition: all 0.3s ease;
32
33 &:hover {
34 transform: translateY(-4px);
35 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
36 }
37}
38
39.custom-btn {
40 @include button-variant(var(--custom-primary));
41 padding: 0.5rem 1rem;
42 border-radius: 0.25rem;
43 text-decoration: none;
44 display: inline-block;
45 transition: all 0.2s ease;
46}
1// Use event delegation for better performance
2document.addEventListener('click', function (e) {
3 if (e.target.matches('.custom-button')) {
4 handleButtonClick(e.target)
5 }
6})
7
8// Debounce expensive operations
9function debounce(func, wait) {
10 let timeout
11 return function executedFunction(...args) {
12 const later = () => {
13 clearTimeout(timeout)
14 func(...args)
15 }
16 clearTimeout(timeout)
17 timeout = setTimeout(later, wait)
18 }
19}
1// Wrap your code in try-catch blocks
2try {
3 initCustomFeatures()
4} catch (error) {
5 console.error('Error initializing custom features:', error)
6 // Fallback behavior
7}
8
9// Use proper error boundaries for async operations
10async function safeAsyncOperation() {
11 try {
12 const result = await fetch('/api/data')
13 return await result.json()
14 } catch (error) {
15 console.error('Async operation failed:', error)
16 return null
17 }
18}
1// Check for feature support before using
2if ('IntersectionObserver' in window) {
3 setupLazyLoading()
4} else {
5 // Fallback for older browsers
6 loadAllImages()
7}
8
9// Check for API availability
10if ('serviceWorker' in navigator) {
11 registerServiceWorker()
12}
1// Create reusable modules
2// assets/hb/modules/custom/js/utils.ts
3export const utils = {
4 formatDate: (date: Date): string => {
5 return date.toLocaleDateString('en-US', {
6 year: 'numeric',
7 month: 'long',
8 day: 'numeric'
9 })
10 },
11
12 throttle: (func: Function, limit: number) => {
13 let inThrottle: boolean
14 return function (this: any, ...args: any[]) {
15 if (!inThrottle) {
16 func.apply(this, args)
17 inThrottle = true
18 setTimeout(() => (inThrottle = false), limit)
19 }
20 }
21 }
22}
Based on our Hello World implementation, here are some common issues you might encounter:
Problem: Using {{< partial "filename.html" . >}} in markdown content.
Solution: Create a proper Hugo shortcode instead of trying to call partials directly from markdown.
1# Create the shortcode file
2mkdir -p layouts/shortcodes
File: layouts/shortcodes/your-script.html
1{{- $customJS := resources.Get "js/your-script.js" -}} {{- if $customJS -}} {{-
2$customJS = $customJS | js.Build | minify -}}
3<script src="{{ $customJS.RelPermalink }}" defer></script>
4{{- 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:
layouts/shortcodes/ 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})
assets/js/ folder is for general-purpose JavaScript files that don’t specifically relate to HBStack modules, while assets/hb/modules/ is specifically for HBStack framework customizations. Use assets/hb/modules/custom/ when you want to leverage HBStack’s TypeScript compilation, module system, and integration with the framework’s build pipeline.assets/hb/modules/ directory to ensure the newer created files are properly loaded by the build system. However, changes to existing files are usually picked up automatically in development mode..ts files in the assets/hb/modules/custom/js/ directory, and they will be automatically compiled to JavaScript. You can also add type definitions and use modern ES6+ features.You can include third-party libraries in several ways:
assets/js/ and import using Hugo’s asset pipelinestatic/js/ for direct linkingFor CSS organization:
assets/css/ for general site stylesassets/hb/modules/custom/scss/ for HBStack-specific stylescomponents/, layouts/, utilities/ for better organizationTo debug JavaScript issues:
console.log() statements in your codeYes, you can extend or override HBStack modules by:
assets/hb/modules/custom/layouts/For optimal performance:
For responsive JavaScript behavior:
window.matchMedia()To implement custom analytics:
assets/hb/modules/custom/js/