Skip to main content

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.
ctx
context.Context
required
Context for cancellation and timeout control
error
error
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

1

Assets Build

AssetService.Build() runs first, populating the Assets map
2

Post Rendering

PostService.Process() uses Assets map in templates
3

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

Performance Features

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)

Build docs developers (and LLMs) love