Overview
The AssetService handles static asset processing including CSS/JS bundling, image compression, and WASM file copying. It runs before post processing to populate the Assets map used in templates.
Source: builder/services/interfaces.go:60-63
Interface
type AssetService interface {
Build(ctx context.Context) error
}
Methods
Build
Processes all static assets in parallel: copies images/fonts, bundles CSS/JS with esbuild.
Context for cancellation and timeout control
Error if asset processing fails
The AssetService.Build() method MUST complete before post rendering begins, otherwise templates won’t have access to hashed asset filenames.
Implementation
The default implementation is assetServiceImpl:
type assetServiceImpl struct {
sourceFs afero.Fs
destFs afero.Fs
cfg *config.Config
renderer RenderService
logger *slog.Logger
}
func NewAssetService(
sourceFs, destFs afero.Fs,
cfg *config.Config,
renderer RenderService,
logger *slog.Logger,
) AssetService
Source: builder/services/asset_service.go:16-32
Build Process
The Build() method runs two parallel goroutines:
1. Static Copy
Copies assets excluding CSS/JS (handled by esbuild):
if err := utils.CopyDirVFS(
s.sourceFs, s.destFs,
s.cfg.StaticDir,
destStaticDir,
s.cfg.CompressImages,
[]string{".css", ".js"}, // Exclude CSS/JS
s.renderer.RegisterFile,
s.cfg.CacheDir+"/images",
s.cfg.ImageWorkers,
); err != nil {
s.logger.Warn("Failed to copy theme static assets", "error", err)
}
Source: builder/services/asset_service.go:50-57
Special Files
Certain files are copied without esbuild processing:
wasm_exec.js - Go WASM runtime
wasm_engine.js - Interactive math simulations
engine.js - WASM loader
search.wasm - Search engine binary
- Site logo (no WebP compression)
2. Esbuild Bundling
Bundles and minifies CSS/JS with content hashing:
assets, err := utils.BuildAssetsEsbuild(
s.sourceFs, s.destFs,
s.cfg.StaticDir,
destStaticDir,
s.cfg.CompressImages,
s.renderer.RegisterFile,
s.cfg.CacheDir+"/assets",
force,
)
// Populate Assets map for templates
s.renderer.SetAssets(assets)
Source: builder/services/asset_service.go:242-262
Assets Map
The assets map maps original paths to hashed filenames:
{
"/static/css/layout.css": "/static/css/layout-a1b2c3d4.css",
"/static/js/theme.js": "/static/js/theme-e5f6g7h8.js",
}
Templates use this map for cache-busting:
<link rel="stylesheet" href="{{ index .Assets "/static/css/layout.css" }}">
Usage Example
From builder/run/build.go:115-118:
// Static Assets (MUST complete before posts to populate Assets map)
fmt.Println("📦 Building assets...")
err := b.assetService.Build(ctx)
if err != nil {
b.logger.Error("Failed to build assets", "error", err)
}
Build Order
Assets Build
AssetService.Build() runs first, populating the Assets map
Post Rendering
PostService.Process() uses Assets map in templates
PWA Generation
PWA manifest uses Assets map for icon paths
Why Order Matters
If posts render before assets complete:
<!-- ❌ Asset not in map yet -->
<link rel="stylesheet" href="">
<!-- ✅ After assets complete -->
<link rel="stylesheet" href="/static/css/layout-a1b2c3d4.css">
Fixed in v1.2.1: Previously, assets ran in parallel with posts, causing race conditions. Now runs synchronously before posts.
Image Compression
When cfg.CompressImages is enabled:
- PNG/JPG/JPEG → WebP conversion
- Quality: 85% (configurable)
- Uses worker pool for parallel encoding
- Caches compressed images in
.kosh-cache/images/
if s.cfg.CompressImages {
// Convert image.png → image.webp
// Update HTML references
}
Cancellation Support
Respects context cancellation:
select {
case <-ctx.Done():
s.logger.Warn("Asset build cancelled", "reason", ctx.Err())
return ctx.Err()
case <-done:
return nil
}
Source: builder/services/asset_service.go:271-276
Parallel Processing
Static copy and esbuild run concurrently:
var wg sync.WaitGroup
wg.Add(2)
go func() { /* Static Copy */ }()
go func() { /* Esbuild */ }()
wg.Wait()
Image Worker Pool
Parallel image compression across CPU cores:
utils.CopyDirVFS(
// ...
s.cfg.ImageWorkers, // Number of parallel workers
)
File Registration
Tracks which files were written for VFS sync:
s.renderer.RegisterFile(destPath)