Type-safe Mustache templating for markdown with schema validation.
Build reliable documentation and content systems with frontmatter schemas that catch errors at build time. Use the CLI for quick validation or the programmatic API for advanced workflows.
Frontmatter errors surface too late or never get caught:
---
publishedAt: 2024-13-45 ❌ Invalid date
tags: "tutorial" ❌ Should be array
authenticated: "yes" ❌ Should be boolean
---Define validation, validate all documents at build time:
import { BatchProcessor } from '@markdown-di/core';
import { z } from 'zod';
// Define your schemas with any validation library (Zod, Yup, etc.)
const schemas = {
'blog-post': z.object({
author: z.string(),
publishedAt: z.string().datetime(),
tags: z.array(z.string())
})
};
const processor = new BatchProcessor({
baseDir: './docs',
validateFrontmatter: (frontmatter, schemaName) => {
if (!schemaName || !schemas[schemaName]) {
return { valid: true };
}
const result = schemas[schemaName].safeParse(frontmatter);
if (result.success) {
return { valid: true, data: result.data };
}
return {
valid: false,
errors: result.error.issues.map(issue => issue.message)
};
}
});
await processor.process();Write documents with validated frontmatter + Mustache templating:
---
schema: blog-post
name: Getting Started
author: Jane Doe
publishedAt: 2024-01-15T10:00:00Z
tags: [tutorial, beginners]
partials:
footer: common/footer.md
---
# {{name}}
By {{author}}
{{#tags}}
- {{.}}
{{/tags}}
{{partials.footer}}Get build-time errors with exact locations:
✗ Found 2 errors in 1 files
docs/getting-started.md:
schema: Invalid datetime string at publishedAt
schema: Expected array, received string at tags
- ✅ Schema validation - Bring your own validation library (Zod, Yup, Ajv, etc.)
- ✅ CLI tool - Validate and build markdown files from the command line
- ✅ Mustache templating - Variables, loops, conditionals
- ✅ File injection - Include external files with
{{partials.xxx}} - ✅ Glob patterns -
guides/*.mdexpands to multiple files - ✅ Security - Path traversal protection, circular dependency detection
- ✅ Dynamic fields -
$dynamickeyword works with hooks and variants API - ✅ Multi-variant generation - One template → many output files with different data
- ✅ Batch processing - Process entire directories with one API call
npm install -g @markdown-di/cli
# or use npx
npx @markdown-di/cli validate docs/npm install @markdown-di/coreThe CLI is the easiest way to get started. It uses JSON Schema for validation.
Create .markdown-di.json in your project root:
{
"schemas": {
"blog-post": {
"type": "object",
"required": ["author", "publishedAt", "tags"],
"properties": {
"author": { "type": "string" },
"publishedAt": { "type": "string", "format": "date" },
"tags": {
"type": "array",
"items": { "type": "string" }
}
}
}
}
}---
schema: blog-post
name: Getting Started
author: Jane Doe
publishedAt: 2024-01-15
tags: [tutorial, beginners]
---
# {{name}}
By {{author}}# Install globally (after publishing)
npm install -g @markdown-di/cli
# Or use with npx (no install needed)
npx @markdown-di/cli --help# Validate files (no output)
npx @markdown-di/cli validate docs/
# Build with processed output
npx @markdown-di/cli build docs/ --output dist/
# Use explicit config path
npx @markdown-di/cli validate docs/ --config path/to/.markdown-di.jsonThe CLI will:
- Auto-discover
.markdown-di.jsonby walking up directories - Validate all frontmatter against your schemas
- Report errors with exact locations
- Process Mustache templates and inject partials
Process multiple markdown files with a simple API:
import { BatchProcessor } from '@markdown-di/core';
import { z } from 'zod';
// Define your schemas
const schemas = {
'blog-post': z.object({
author: z.string(),
publishedAt: z.string().datetime(),
tags: z.array(z.string())
})
};
const processor = new BatchProcessor({
baseDir: './docs',
include: ['**/*.md'],
exclude: ['node_modules/**'],
outDir: './dist', // Optional: output to different directory
validateFrontmatter: (frontmatter, schemaName) => {
if (!schemaName || !schemas[schemaName]) {
return { valid: true };
}
const result = schemas[schemaName].safeParse(frontmatter);
if (result.success) {
return { valid: true, data: result.data };
}
return {
valid: false,
errors: result.error.issues.map(issue => issue.message)
};
}
});
const result = await processor.process();
if (!result.success) {
console.error(`Found ${result.totalErrors} errors`);
// Easy access to all error messages
console.error(result.errorMessages);
// Or access errors by file
for (const [file, errors] of Object.entries(result.errorsByFile)) {
console.error(`${file}:`, errors.map(e => e.message));
}
process.exit(1);
}
console.log(`✓ Processed ${result.totalFiles} files`);For processing individual files:
import { MarkdownDI } from '@markdown-di/core';
import { z } from 'zod';
const mdi = new MarkdownDI();
const schemas = {
'blog-post': z.object({
author: z.string(),
publishedAt: z.string().datetime(),
tags: z.array(z.string())
})
};
const result = await mdi.process({
content: markdownContent,
baseDir: './docs',
currentFile: './docs/post.md',
validateFrontmatter: (frontmatter, schemaName) => {
if (!schemaName || !schemas[schemaName]) {
return { valid: true };
}
const result = schemas[schemaName].safeParse(frontmatter);
if (result.success) {
return { valid: true, data: result.data };
}
return {
valid: false,
errors: result.error.issues.map(issue => issue.message)
};
}
});
if (result.errors.length > 0) {
console.error('Validation errors:', result.errors.map(e => e.message));
}- Documentation sites - Validate 100s of markdown files in CI/CD
- Content workflows - Enforce consistent frontmatter across teams
- AI/Agent systems - Validate generated markdown at build time
- API docs - Type-safe schemas for endpoints, methods, auth
- Static site generators - Pre-process markdown with type safety
The default schema requires only name:
---
# Required field
name: string
# Optional: Reference a registered schema
schema: string
# Optional: Partial definitions (file injection)
partials:
key: path/to/file.md # Single file
multi: path/to/*.md # Glob pattern
combined: # Array of files/patterns
- path/to/file1.md
- path/to/*.md
# Optional: Control output frontmatter
output-frontmatter:
- name
# Only these fields will appear in the output
---Note: HTML escaping is disabled by default since markdown-di works with markdown content, not HTML. All template variables ({{var}}) are inserted as-is without escaping special characters.
You can customize the Mustache template delimiters (default is {{ and }}). This is useful when working with content that already uses the default delimiters or when you prefer alternative syntax.
Add to your .markdown-di.json config file:
{
"schemas": {},
"mustache": {
"tags": ["<%", "%>"]
}
}Then use the custom delimiters in your markdown:
---
name: Example
author: John Doe
---
# <% name %>
By <% author %>import { MarkdownDI } from '@markdown-di/core';
const mdi = new MarkdownDI();
const result = await mdi.process({
content: markdownContent,
baseDir: './docs',
mustache: {
tags: ['<%', '%>']
}
});import { BatchProcessor } from '@markdown-di/core';
const processor = new BatchProcessor({
baseDir: './docs',
mustache: {
tags: ['<%', '%>']
}
});
await processor.process();Note: Custom delimiters work with all Mustache features including variables, sections, conditionals, and partials.
Access any frontmatter field as a variable:
---
name: John Doe
age: 30
author:
name: Jane Smith
email: jane@example.com
---
# Document by {{name}}
Age: {{age}}
Author: {{author.name}} ({{author.email}})Iterate over arrays:
---
name: Team List
team:
- name: Alice
role: Developer
- name: Bob
role: Designer
---
# Team Members
{{#team}}
- **{{name}}** - {{role}}
{{/team}}Use sections for conditional rendering:
---
name: Document
published: true
draft: false
---
{{#published}}
This document is published!
{{/published}}
{{^draft}}
This is not a draft.
{{/draft}}Inject external file content:
---
name: Main Doc
partials:
header: common/header.md
footer: common/footer.md
---
{{partials.header}}
# Main Content
{{partials.footer}}partials:
intro: sections/intro.mdpartials:
guides: guides/*.md # All markdown files in guides/
nested: docs/**/*.md # Recursive globpartials:
allContent:
- sections/intro.md
- guides/*.md
- docs/advanced/*.mdFiles matched by globs are:
- Sorted alphabetically for consistency
- Joined with
\n\n(double newline) between them - Automatically excluded from
node_modules,dist, andbuilddirectories
Partials can have their own frontmatter with variables that support:
- Access to parent variables: Partials can use any variable from the parent document
- Variable overrides: Partial frontmatter takes precedence over parent values
- Parent references: Use
$parentor$parent('key')to explicitly get parent values - Nested partials: Partials can include other partials
Parent document (main.md):
---
name: Product Documentation
author: John Doe
version: 2.0
theme: dark
partials:
header: sections/header.md
---
{{partials.header}}Partial with frontmatter (sections/header.md):
---
name: Header Section
description: Auto-generated header
---
# {{name}}
Version: {{version}} | Author: {{author}} | Theme: {{theme}}Result: The partial can access version, author, and theme from the parent, while using its own name.
Use $parent when you want the exact value from the parent with the same key:
---
name: Header
author: $parent
theme: $parent
---
# {{name}}
By {{author}} - {{theme}} themeUse $parent('key') to get a parent variable with a different key:
---
name: Header
title: $parent('name')
authorName: $parent('author')
---
# {{name}}
Document: {{title}}
Written by: {{authorName}}Partials can include other partials, creating a hierarchy:
Main document:
---
name: Main Doc
author: Alice
theme: light
partials:
layout: partials/layout.md
---
{{partials.layout}}Layout partial (partials/layout.md):
---
name: Layout
partials:
header: partials/header.md
footer: partials/footer.md
---
{{partials.header}}
Main content area
{{partials.footer}}Header partial (partials/header.md):
---
name: Site Header
---
# {{name}}
By {{author}} | Theme: {{theme}}All nested partials have access to the parent document's variables (author, theme), while each can define their own name.
markdown-di lets you bring your own validation library. Use Zod, Yup, Ajv, or any other validation library by providing a validateFrontmatter callback.
import { MarkdownDI } from '@markdown-di/core';
import { z } from 'zod';
const schemas = {
'blog-post': z.object({
author: z.string(),
publishedAt: z.string().datetime(),
tags: z.array(z.string())
})
};
const result = await mdi.process({
content: markdownContent,
baseDir: './docs',
validateFrontmatter: (frontmatter, schemaName) => {
if (!schemaName || !schemas[schemaName]) {
return { valid: true };
}
const result = schemas[schemaName].safeParse(frontmatter);
if (result.success) {
return { valid: true, data: result.data };
}
return {
valid: false,
errors: result.error.issues.map(issue => issue.message)
};
}
});import { MarkdownDI } from '@markdown-di/core';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
const ajv = new Ajv();
addFormats(ajv);
const schemas = {
'blog-post': {
type: 'object',
required: ['author', 'date'],
properties: {
author: { type: 'string', minLength: 1 },
date: { type: 'string', format: 'date' },
tags: { type: 'array', items: { type: 'string' } }
}
}
};
// Compile schemas
Object.entries(schemas).forEach(([name, schema]) => {
ajv.addSchema(schema, name);
});
const result = await mdi.process({
content: markdownContent,
baseDir: './docs',
validateFrontmatter: (frontmatter, schemaName) => {
if (!schemaName) {
return { valid: true };
}
const validate = ajv.getSchema(schemaName);
if (!validate) {
return {
valid: false,
errors: [`Schema '${schemaName}' not found`]
};
}
const valid = validate(frontmatter);
if (valid) {
return { valid: true };
}
return {
valid: false,
errors: (validate.errors || []).map(err => err.message || 'Validation error')
};
}
});import { MarkdownDI } from '@markdown-di/core';
import * as yup from 'yup';
const schemas = {
'blog-post': yup.object({
author: yup.string().required(),
publishedAt: yup.date().required(),
tags: yup.array().of(yup.string()).required()
})
};
const result = await mdi.process({
content: markdownContent,
baseDir: './docs',
validateFrontmatter: async (frontmatter, schemaName) => {
if (!schemaName || !schemas[schemaName]) {
return { valid: true };
}
try {
const data = await schemas[schemaName].validate(frontmatter, { abortEarly: false });
return { valid: true, data };
} catch (err) {
if (err instanceof yup.ValidationError) {
return {
valid: false,
errors: err.inner.map(e => e.message)
};
}
throw err;
}
}
});Reference a registered schema using the schema field:
---
schema: blog-post
name: Getting Started
author: Jane Doe
publishedAt: 2024-01-15T10:00:00Z
tags: [tutorial, beginners]
---
# {{name}}
By {{author}}Important: Schemas always extend the default schema, so name is always required.
Use the onBeforeCompile hook to inject dynamic values at runtime:
const processor = new BatchProcessor({
baseDir: './docs',
onBeforeCompile: async (context) => ({
buildTime: new Date().toISOString(),
version: process.env.VERSION,
gitCommit: await getGitCommit()
})
});Mark fields as $dynamic in frontmatter to require dynamic data:
---
name: Documentation
buildTime: $dynamic
version: $dynamic
---
Built at {{buildTime}}
Version: {{version}}Note: The $dynamic keyword works with both:
- The
onBeforeCompilehook (as shown above) - The variants API (see Multi-Variant Template Generation)
- Both combined (hook + variant data)
Generate multiple output files from a single template with different data for each variant. Perfect for creating product pages, documentation in multiple languages, or any scenario where you need many similar files with different values.
const processor = new BatchProcessor({
baseDir: './templates',
outDir: './dist',
variants: {
'product-template': {
data: [
{ product: 'Widget A', price: '$10', sku: 'WA-001' },
{ product: 'Widget B', price: '$20', sku: 'WB-001' },
{ product: 'Widget C', price: '$30', sku: 'WC-001' }
],
getOutputPath: (context, data, index) => {
const slug = data.product.toLowerCase().replace(/\s+/g, '-')
return `products/${slug}.md`
}
}
}
})Template file (templates/product.md):
---
id: product-template
name: Product Template
---
# {{product}}
Price: {{price}}
SKU: {{sku}}Generated output:
dist/products/widget-a.mddist/products/widget-b.mddist/products/widget-c.md
Key features:
- Each variant gets its own output file
- Custom output path via
getOutputPathcallback - Original template is not written (only variants)
- Works with
onBeforeCompilefor additional dynamic data - File-specific variants via
idfield in frontmatter
The $dynamic keyword works seamlessly with the variants API. Mark fields as $dynamic in your template, and provide values via the variant data:
Template file (templates/command.md):
---
id: command-template
name: $dynamic
command: $dynamic
description: $dynamic
---
# {{name}}
Command: `{{command}}`
{{description}}Variant configuration:
const processor = new BatchProcessor({
baseDir: './templates',
outDir: './dist',
variants: {
'command-template': {
data: [
{
name: 'Recipe Command',
command: '/recipe',
description: 'Generate cooking recipes'
},
{
name: 'Code Command',
command: '/code',
description: 'Generate code snippets'
}
],
getOutputPath: (context, data, index) =>
`commands/${data.command.replace('/', '')}.md`
}
}
});Generated output:
dist/commands/recipe.mdwith recipe datadist/commands/code.mdwith code data
Important: If you mark fields as $dynamic, you must provide them via either:
- The variants API (as shown above)
- The
onBeforeCompilehook - Both combined (hook values + variant data)
If any $dynamic fields remain unresolved, you'll get a clear error message.
Control which frontmatter fields appear in the final output:
---
name: Public Document
description: This will be in output
author: Internal Team
draft-notes: TODO review
internal-id: ABC123
output-frontmatter:
- name
- description
# Only name and description will appear in the final output
---Use Cases:
- Strip internal metadata before publishing
- Remove draft/workflow fields from production documents
- Keep sensitive information in source but not in output
The @markdown-di/cli package provides command-line tools for validation and building.
# Global installation
npm install -g @markdown-di/cli
# Or use with npx
npx @markdown-di/cli validate docs/Validate markdown files without writing output. Perfect for CI/CD pipelines.
# Validate single file
npx @markdown-di/cli validate docs/post.md
# Validate directory
npx @markdown-di/cli validate docs/
# Validate with glob pattern
npx @markdown-di/cli validate "docs/**/*.md"
# Use explicit config
npx @markdown-di/cli validate docs/ --config path/to/.markdown-di.jsonOptions:
-c, --config <path>- Path to config file (overrides auto-discovery)
Exit codes:
0- All files valid1- Validation errors found
Build markdown files with dependency injection and optional output directory.
# Build single file to output directory
npx @markdown-di/cli build docs/post.md --output dist/
# Build entire directory
npx @markdown-di/cli build docs/ --output dist/
# Build in-place (overwrites source files)
npx @markdown-di/cli build docs/
# Use explicit config
npx @markdown-di/cli build docs/ --output dist/ --config .markdown-di.jsonOptions:
-o, --output <dir>- Output directory for processed files-c, --config <path>- Path to config file (overrides auto-discovery)-w, --watch- Watch mode (not yet implemented)
The CLI looks for config files in this order:
- Path specified with
--configflag .markdown-di.json(auto-discovered by walking up directories).markdown-di.schemas.jsonmarkdown-di.config.json
Config format:
{
"schemas": {
"schema-name": {
"type": "object",
"required": ["field1"],
"properties": {
"field1": { "type": "string" },
"field2": { "type": "number" }
}
}
},
"mustache": {
"tags": ["<%", "%>"]
}
}Config options:
schemas- JSON Schema definitions for frontmatter validationmustache- Optional Mustache template engine configurationtags- Custom delimiters (default:["{{", "}}"])
The config file uses standard JSON Schema format with support for:
- Type validation (
string,number,boolean,array,object,null) - Format validation (
date,date-time,email,uri,uuid, etc.) - Array items validation
- Nested objects
- Required fields
- Min/max constraints
Use the validate command in your CI pipeline:
# GitHub Actions example
- name: Validate markdown
run: npx @markdown-di/cli validate docs/
# Will exit with code 1 if validation failsBatch processor for multiple markdown files. Simplifies processing entire directories.
new BatchProcessor(config?: BatchConfig)Config Options:
interface BatchConfig {
baseDir?: string; // Base directory (default: process.cwd())
include?: string[]; // Glob patterns (default: ['**/*.md'])
exclude?: string[]; // Exclude patterns (default: ['node_modules/**', '.git/**'])
outDir?: string; // Output directory (default: in-place updates)
validateFrontmatter?: (frontmatter: FrontmatterData, schemaName?: string) => SchemaValidationResult | Promise<SchemaValidationResult>;
onBeforeCompile?: (context: HookContext) => Promise<Record<string, unknown>> | Record<string, unknown>;
variants?: Record<string, VariantGenerator>; // Multi-variant generation config
mustache?: MustacheConfig; // Custom Mustache template engine configuration
check?: boolean; // Check mode - don't write files (default: false)
silent?: boolean; // Suppress console output (default: false)
}
interface MustacheConfig {
tags?: [string, string]; // Custom delimiters (default: ['{{', '}}'])
}
interface VariantGenerator {
data: Record<string, unknown>[]; // Array of data objects, one per variant
getOutputPath: (context: HookContext, data: Record<string, unknown>, index: number) => string;
}Process all matching files:
const result = await processor.process();
// Returns:
interface BatchResult {
totalFiles: number;
changedFiles: number;
totalErrors: number;
files: FileResult[];
success: boolean;
// Easy error access helpers
errorsByFile: Record<string, ValidationError[]>; // Errors grouped by file
allErrors: ValidationError[]; // All errors flattened
errorMessages: string[]; // Simple error messages
}
interface FileResult {
file: string;
changed: boolean;
errors: ValidationError[];
messages: string[]; // Simple error messages
}Examples:
// Process with output directory
const processor = new BatchProcessor({
baseDir: './docs',
outDir: './dist',
validateFrontmatter: myValidationFunction
});
// Check mode (CI/CD)
const processor = new BatchProcessor({
baseDir: './docs',
check: true
});
// With onBeforeCompile hook
const processor = new BatchProcessor({
baseDir: './docs',
onBeforeCompile: async (context) => ({
buildTime: new Date().toISOString(),
version: process.env.VERSION
})
});Main class for processing individual markdown documents.
Process a markdown document with dependency injection.
Options:
interface ProcessOptions {
content: string; // Markdown content with frontmatter
baseDir: string; // Base directory for resolving file paths
mode?: 'validate' | 'build'; // Processing mode (default: 'build')
currentFile?: string; // Current file path (for circular detection)
onBeforeCompile?: (context: HookContext) => Promise<Record<string, unknown>> | Record<string, unknown>;
mustache?: MustacheConfig; // Custom Mustache template engine configuration
}Returns:
interface ProcessResult {
content: string; // Processed markdown with frontmatter
frontmatter: FrontmatterData; // Parsed frontmatter object
errors: ValidationError[]; // All validation errors
dependencies: string[]; // Resolved file paths (absolute)
}Validate without processing (runs in mode: 'validate'):
const result = await mdi.validate({
content,
baseDir: './docs'
});
// Returns errors but doesn't inject partialsAll file paths are validated to prevent escaping the baseDir:
partials:
# ❌ These will fail validation
bad1: ../../../etc/passwd
bad2: /absolute/path/to/file.md
# ✅ These are safe
good1: sections/intro.md
good2: guides/getting-started.mdAutomatically detects and prevents circular dependencies:
[circular] Circular dependency detected: /docs/A.md -> /docs/B.md -> /docs/A.md
at /docs/A.md
const result = await mdi.process({ content, baseDir });
if (result.errors.length > 0) {
// Access detailed error objects
for (const error of result.errors) {
console.error(`[${error.type}] ${error.message}`);
console.error(` at ${error.location}`);
}
}The batch processor provides multiple ways to access errors for convenience:
const result = await processor.process();
if (!result.success) {
// Option 1: Simple error messages array
console.error('Errors:', result.errorMessages);
// ["Schema product-planner-agent not found", "Missing required field: author"]
// Option 2: Errors grouped by file
for (const [file, errors] of Object.entries(result.errorsByFile)) {
console.error(`\n${file}:`);
errors.forEach(e => console.error(` - ${e.message}`));
}
// Option 3: All errors flattened (with full ValidationError objects)
console.error(`Total errors: ${result.allErrors.length}`);
// Option 4: Per-file simple messages
for (const fileResult of result.files) {
if (fileResult.messages.length > 0) {
console.error(`${fileResult.file}:`, fileResult.messages);
}
}
}When implementing validateFrontmatter, return a simplified structure:
interface SchemaValidationResult {
valid: boolean;
errors?: string[]; // Simple error messages (optional when valid=true)
data?: unknown; // Optional transformed data
}Example:
validateFrontmatter: (frontmatter, schemaName) => {
// Valid with no data transformation
if (!schemaName) return { valid: true };
// Valid with data transformation
const parsed = schema.safeParse(frontmatter);
if (parsed.success) return { valid: true, data: parsed.data };
// Invalid with error messages
return {
valid: false,
errors: parsed.error.issues.map(i => i.message)
};
}The library automatically converts simple error strings into full ValidationError objects with type, location, and other metadata.
Error Types:
frontmatter- Invalid frontmatter structurepartial- Partial syntax errorsfile- File not found or path traversalcircular- Circular dependency detectedsyntax- Template syntax errorsschema- Schema validation errorsinjection- Dynamic injection errors
import { BatchProcessor, z } from '@markdown-di/core';
const processor = new BatchProcessor({
baseDir: './blog',
outDir: './dist',
schemas: {
'blog-post': z.object({
author: z.string(),
publishedAt: z.string().datetime(),
tags: z.array(z.string()),
featured: z.boolean().optional()
})
},
onBeforeCompile: async (context) => ({
buildTime: new Date().toISOString(),
siteUrl: 'https://example.com'
})
});
const result = await processor.process();
if (!result.success) {
console.error('Validation errors:', result.files
.filter(f => f.errors.length > 0)
.map(f => ({ file: f.file, errors: f.errors }))
);
process.exit(1);
}
console.log(`✓ Processed ${result.totalFiles} files`);
console.log(`✓ ${result.changedFiles} files updated`);Check out the examples/ directory for real-world use cases:
Organize and document Claude Code agents and commands with schema validation.
Features:
- Agent schema with system prompts, tools, and examples
- Command schema with categories and usage patterns
- Auto-generated documentation with consistent structure
- Validation for naming conventions and required fields
Use cases:
- Document AI agents and their capabilities
- Standardize slash commands across projects
- Ensure consistency in .claude folder structure
Manage personal notes, meeting notes, projects, and book summaries with type-safe schemas.
Features:
- Meeting notes with attendees, topics, and action items
- Daily notes with mood tracking and focus areas
- Project tracking with status, milestones, and links
- Book notes with ratings and reading progress
Use cases:
- Personal knowledge management
- Team meeting documentation
- Project tracking and planning
- Reading list and book summaries
Generate multiple Claude Code slash commands from a single template using the variants API.
Features:
- One template generates 5 recipe management commands
- Type-safe variant data in TypeScript
- Dynamic output paths via
getOutputPathcallback - Demonstrates
$dynamicfields andonBeforeCompilehook
Use cases:
- Generate multiple similar slash commands efficiently
- Maintain consistency across command definitions
- Avoid manual duplication or LLM generation
- Create command families with shared structure
All examples include:
- ✅ Complete JSON Schema definitions
- ✅ Sample markdown files with Mustache templates
- ✅ Pre-built
dist/folders showing output - ✅ CLI and programmatic usage examples
- ✅ README with setup instructions
Full TypeScript support with exported types:
import type {
BatchConfig,
BatchResult,
FileResult,
ProcessOptions,
ProcessResult,
ValidationError,
FrontmatterData,
HookContext,
MustacheConfig
} from '@markdown-di/core';MIT © Pepijn Senders