Template Rendering - Chris-Cullins/wiki_bot GitHub Wiki

Template Rendering

Overview

The Template Rendering area provides a flexible, cache-enabled system for loading and interpolating Markdown templates used throughout the wiki generation process. The TemplateRenderer class manages template discovery across multiple directories with variant support, while the loadPrompt function handles LLM prompt template loading with variable substitution.

Key Responsibilities:

  • Load Markdown templates from configurable directory hierarchies
  • Support template variants (e.g., area-specific templates)
  • Cache template contents to minimize filesystem I/O
  • Perform placeholder interpolation using {{variable}} syntax
  • Provide fallback mechanisms when templates are not found

Key Components

src/template-renderer.ts

TemplateRenderer class — The core template engine that handles loading, caching, and rendering Markdown templates.

src/templates/*.md — Default template files containing placeholder-based Markdown skeletons for wiki pages (architecture, area, home).

src/prompt-loader.ts — Standalone utility for loading LLM prompts from the src/prompts/ directory with variable injection.


How It Works

Template Discovery & Priority

When constructing a TemplateRenderer, you can optionally provide a custom template directory. The renderer establishes a search hierarchy:

  1. Custom directory (if provided) — User-specified templates take precedence
  2. Default directory (src/templates/) — Built-in fallback templates

When rendering, the system checks each directory in order, stopping at the first match. This allows users to override default templates without modifying the source.

Variant Resolution

The render method accepts an options parameter with optional variant and variantSubdir fields. Candidate paths are generated in priority order:

  1. {baseDir}/{variantSubdir}/{variant}.md (if both variant and variantSubdir specified)
  2. {baseDir}/{templateName}-{variant}.md (if variant specified)
  3. {baseDir}/{variant}.md (if variant specified)
  4. {baseDir}/{templateName}.md (base template)

For example, rendering the "area" template with variant "configuration-management" and variantSubdir "areas" would check:

  • {customDir}/areas/configuration-management.md
  • {customDir}/area-configuration-management.md
  • {customDir}/configuration-management.md
  • {customDir}/area.md
  • {defaultDir}/areas/configuration-management.md
  • ... (continues through default directory)

Caching Strategy

Templates are cached using composite keys: {templateName}:{variant} (or just {templateName} if no variant). The cache stores:

  • String values for successfully loaded templates
  • null for templates that don't exist (preventing repeated filesystem checks)

The cache is instance-scoped and persists for the lifetime of the TemplateRenderer object.

Interpolation

The interpolate method performs simple find-and-replace using regular expressions:

  • Searches for {{\s*key\s*}} patterns (whitespace-tolerant)
  • Replaces with corresponding context values
  • Treats missing context values as empty strings
  • Escapes special regex characters in keys to prevent injection issues

Important Functions/Classes

TemplateRenderer.constructor(customDir?: string)

Initializes the template search directories and cache.

const renderer = new TemplateRenderer('/path/to/custom/templates');

Parameters:

  • customDir — Optional absolute path to custom template directory

Behavior:

  • Resolves the default template directory relative to the module location
  • Uses uniquePaths() to deduplicate directory entries
  • Initializes an empty cache

TemplateRenderer.render(templateName: string, context: Record<string, string>, options?: RenderOptions): Promise<string>

Primary method for loading and rendering a template.

const output = await renderer.render('area', {
  title: 'Authentication',
  content: '# Authentication\n\nHandles user login...',
  depth: 'standard'
}, {
  variant: 'authentication',
  variantSubdir: 'areas'
});

Parameters:

  • templateName — Base name of the template (without extension)
  • context — Key-value pairs for placeholder substitution
  • options.variant — Optional variant identifier for specialized templates
  • options.variantSubdir — Optional subdirectory for variant templates

Returns: Rendered template string, or context.content if no template found

Error Handling:

  • ENOENT errors (file not found) are silently ignored during candidate probing
  • Other filesystem errors trigger console warnings but don't throw

loadPrompt(promptName: string, variables?: Record<string, string>): Promise<string>

Loads an LLM prompt from src/prompts/{promptName}.md and injects variables.

const prompt = await loadPrompt('generate-area-documentation', {
  area: 'Configuration',
  fileContentText: '--- config.ts ---\n...'
});

Parameters:

  • promptName — Name of the prompt file (without extension)
  • variables — Variables to substitute in {{key}} placeholders

Returns: Prompt string with all placeholders replaced

Key Difference from TemplateRenderer:

  • No caching (prompts may use dynamic variables)
  • No variant support
  • Fixed directory (src/prompts/)
  • Uses global regex replacement for all occurrences

Developer Notes

Escaping Gotcha

The escapeRegExp method contains a subtle bug on src/template-renderer.ts:97:

return value.replace(/[.*+?^${}()|[\]\\]/g, '\\{{fileContentText}}');

This should be '\\{{content}}' to properly escape matched characters. The current implementation replaces special characters with the literal string \{{fileContentText}}, which will cause incorrect interpolation behavior if context keys contain regex metacharacters.

Template Fallback Strategy

When no template is found, render() returns context.content ?? ''. This means:

  • If you pass content in the context, it becomes the fallback
  • If no content key exists, you get an empty string
  • The caller in WikiGenerator relies on this fallback for passthrough rendering

Cache Invalidation

There is no cache invalidation mechanism. If templates change on disk during execution, the renderer will continue serving stale cached versions. For long-running processes that need to respond to template changes, consider:

  • Creating new TemplateRenderer instances
  • Adding a manual cache clearing method
  • Implementing filesystem watchers

Variant Naming Best Practices

When generating variants from area names, WikiGenerator.slugify() is used to normalize names:

  • Lowercase conversion
  • Non-alphanumeric characters replaced with hyphens
  • Leading/trailing hyphens stripped

Ensure custom variant templates follow this naming convention for automatic discovery.

Error Suppression Trade-offs

The renderer suppresses most errors to support graceful fallback behavior. This aids resilience but can mask configuration issues. When debugging template problems:

  • Check console warnings for non-ENOENT errors
  • Verify search directories using renderer.searchDirectories
  • Add temporary logging in loadTemplate() to trace candidate path checks

Usage Examples

Basic Template Rendering

const renderer = new TemplateRenderer();

const homeContent = await renderer.render('home', {
  content: '# Home\n\nWelcome to the wiki!'
});

Custom Template Directory

const renderer = new TemplateRenderer('/workspace/.wiki-templates');

// Will check /workspace/.wiki-templates/area.md first,
// then fall back to src/templates/area.md
const areaDoc = await renderer.render('area', {
  title: 'Data Layer',
  content: '# Data Layer\n\n...'
});

Area-Specific Variants

const renderer = new TemplateRenderer();

// For the "Configuration Management" area
const configDoc = await renderer.render(
  'area',
  {
    title: 'Configuration Management',
    content: '# Configuration Management\n\n...',
    depth: 'deep'
  },
  {
    variant: 'configuration-management',
    variantSubdir: 'areas'
  }
);

Prompt Loading for LLM Queries

import { loadPrompt } from './prompt-loader.js';

const prompt = await loadPrompt('generate-home-page', {
  structureText: repoTree,
  repoRoot: '/workspace/my-project'
});

// Use with LLM query...
const query = createQuery(prompt);
⚠️ **GitHub.com Fallback** ⚠️