Skip to main content

Overview

The PostService handles markdown parsing, HTML rendering, search indexing, and social card generation. It’s the core service for processing content in Kosh. Source: builder/services/post_service.go:19-24

Interface

type PostService interface {
    Process(ctx context.Context, shouldForce, forceSocialRebuild, outputMissing bool) (*PostResult, error)
    ProcessSingle(ctx context.Context, path string) error
    RenderCachedPosts()
}

Methods

Process

Processes all markdown posts in the content directory, generating HTML, search indexes, and social cards.
ctx
context.Context
required
Context for cancellation and timeout control
shouldForce
bool
required
Force rebuild all posts, ignoring cache
forceSocialRebuild
bool
required
Force regeneration of all social card images
outputMissing
bool
required
Render posts whose HTML files are missing from output directory
PostResult
*PostResult
Aggregated results containing processed posts, tags, and search data
AllPosts
[]models.PostMetadata
All non-pinned posts from latest version
PinnedPosts
[]models.PostMetadata
Posts marked as pinned (displayed at top)
TagMap
map[string][]models.PostMetadata
Map of tag names to posts containing that tag
IndexedPosts
[]models.IndexedPost
Posts with BM25 search data (word frequencies, doc length)
AnyPostChanged
bool
Whether any post was modified or rendered
Has404
bool
Whether a custom 404.md was found

ProcessSingle

Processes a single markdown file for fast incremental rebuilds during watch mode.
ctx
context.Context
required
Context for cancellation
path
string
required
Absolute path to the markdown file to process

RenderCachedPosts

Re-renders all posts from cache without parsing markdown. Used for template-only changes.
This method has no parameters and is called during fast-path rebuilds when only templates/CSS changed.

Implementation

The default implementation is postServiceImpl, created via dependency injection:
func NewPostService(
    cfg *config.Config,
    cacheSvc CacheService,
    renderer RenderService,
    logger *slog.Logger,
    metrics *metrics.BuildMetrics,
    md goldmark.Markdown,
    nativeRenderer *native.Renderer,
    sourceFs, destFs afero.Fs,
    diagramAdapter *cache.DiagramCacheAdapter,
) PostService
Source: builder/services/post_service.go:49-72

Usage Example

From builder/run/build.go:247-249:
// Full post processing with cache and social cards
fmt.Println("πŸ“ Processing content...")
result, err := b.postService.Process(ctx, shouldForce, forceSocialRebuild, outputMissing)
if err != nil {
    b.logger.Error("Failed to process posts", "error", err)
    return nil, nil, nil, nil, false, false
}

allPosts := result.AllPosts
pinnedPosts := result.PinnedPosts
tagMap := result.TagMap
indexedPosts := result.IndexedPosts
From builder/run/build.go:173-174:
// Fast path: re-render from cache (template changes only)
if isTemplateOnly && cachedCount > 0 {
    fmt.Println("πŸ“ Rehydrating from cache...")
    b.postService.RenderCachedPosts()
}

Key Features

Cache Invalidation

The service uses body hash tracking (v1.2.1) to detect content changes even when file modification time hasn’t changed:
// Always read source to compute body hash (CRITICAL for cache validity)
source, _ := afero.ReadFile(s.sourceFs, path)
bodyHash := utils.GetBodyHash(source)

// Invalidate cache if body content changed (regardless of ModTime)
if exists && cachedMeta.BodyHash != bodyHash {
    exists = false
}
Source: builder/services/post_service.go:216-231

Parallel Processing

Uses worker pools for concurrent markdown parsing:
parsePool := utils.NewWorkerPool(ctx, numWorkers, func(pt struct {
    idx     int
    path    string
    version string
}) {
    // Parse markdown, render HTML, extract search data
})
parsePool.Start()
Source: builder/services/post_service.go:173-552

Search Indexing

Generates BM25 data with stemming and stop word removal:
// Analyze with stemming and stop words
words = search.DefaultAnalyzer.Analyze(sb.String())
docLen = len(words)
wordFreqs = make(map[string]int)
for _, w := range words {
    if len(w) >= 2 {
        wordFreqs[w]++
    }
}
Source: builder/services/post_service.go:397-404

Version Support

Handles versioned documentation with proper URL generation:
var destPath string
if version != "" {
    destPath = filepath.Join(s.cfg.OutputDir, version, cleanHtmlRelPath)
} else {
    destPath = filepath.Join(s.cfg.OutputDir, htmlRelPath)
}
Source: builder/services/post_service.go:188-193

Performance

  • Worker Pools: Parallel processing across CPU cores
  • Buffer Pooling: Reuses bytes.Buffer instances to reduce GC pressure
  • Batch Operations: Commits cache entries in batches for better throughput
  • LRU Cache: Hot post metadata cached in memory (5-minute TTL)

Build docs developers (and LLMs) love