This guide shows you how to add custom transformations to markdown files before Hugo renders them.
Overview
DocBuilder uses a pluggable transform pipeline that processes markdown files during the content copy stage. Each file goes through a series of transformers that can:
Modify markdown content
Add or modify front matter metadata
Rewrite links
Convert syntax between formats
Add custom fields
Transform Pipeline Architecture
Transformers run in dependency-based order organized by stages during the CopyContent stage:
Stage: parse
1. Front Matter Parser - Extract YAML headers
Stage: build
2. Front Matter Builder - Add repository metadata
Stage: enrich
3. Edit Link Injector - Generate edit URLs
Stage: merge
4. Front Matter Merge - Combine metadata
Stage: transform
5. Relative Link Rewriter - Fix relative links
6. [Your Custom Transform] - Your transformation
Stage: finalize
7. Strip First Heading - Remove duplicate titles
8. Shortcode Escaper - Escape Hugo shortcodes
9. Hextra Type Enforcer - Set page types
Stage: serialize
10. Serializer - Write final YAML + content
Within each stage, transforms are ordered by their declared dependencies.
// internal/hugo/transforms/my_custom.gopackagetransformsimport("fmt""strings")typeMyCustomTransformstruct{}func(tMyCustomTransform)Name()string{return"my_custom_transform"}func(tMyCustomTransform)Stage()TransformStage{returnStageTransform// Runs during the transform stage}func(tMyCustomTransform)Dependencies()TransformDependencies{returnTransformDependencies{MustRunAfter:[]string{"relative_link_rewriter"},// Run after link rewriting}}func(tMyCustomTransform)Transform(pPageAdapter)error{// Type assert to access page datapg,ok:=p.(*PageShim)if!ok{returnfmt.Errorf("unexpected page adapter type")}// Transform the markdown contentpg.Content=strings.ReplaceAll(pg.Content,"{{OLD}}","{{NEW}}")returnnil}funcinit(){// Auto-register on package loadRegister(MyCustomTransform{})}
Step 2: Build the Project
1
go build ./...
The transformer is automatically registered and will run on all markdown files.
Step 3: Test Your Transform
1
docbuilder build -c config.yaml -v
Check the output files in content/ to verify your transformation.
typeCodeBlockEnhancerstruct{}func(tCodeBlockEnhancer)Name()string{return"code_block_enhancer"}func(tCodeBlockEnhancer)Stage()TransformStage{returnStageTransform}func(tCodeBlockEnhancer)Dependencies()TransformDependencies{returnTransformDependencies{MustRunAfter:[]string{"relative_link_rewriter"},}}func(tCodeBlockEnhancer)Transform(pPageAdapter)error{pg:=p.(*PageShim)// Add line numbers to all code blocksre:=regexp.MustCompile("(?s)```(\\w+)\\n(.*?)```")pg.Content=re.ReplaceAllString(pg.Content,"```$1 {linenos=true}\n$2```")returnnil}funcinit(){Register(CodeBlockEnhancer{})}
typeConditionalTransformstruct{}func(tConditionalTransform)Name()string{return"conditional_transform"}func(tConditionalTransform)Stage()TransformStage{returnStageTransform}func(tConditionalTransform)Dependencies()TransformDependencies{returnTransformDependencies{MustRunAfter:[]string{"relative_link_rewriter"},}}func(tConditionalTransform)Transform(pPageAdapter)error{pg:=p.(*PageShim)// Only transform API documentationifstrings.Contains(pg.FilePath,"/api/"){pg.AddPatch(fmcore.FrontMatterPatch{Key:"type",Value:"api-reference",})// Add API-specific stylingpg.Content="{{< api-layout >}}\n"+pg.Content+"\n{{< /api-layout >}}"}returnnil}funcinit(){Register(ConditionalTransform{})}
PageShim Interface
Your transform receives a PageAdapter which you type-assert to *PageShim:
1
2
3
4
5
6
7
8
9
10
11
12
13
typePageShimstruct{FilePathstring// Relative file pathContentstring// Markdown content (no front matter)OriginalFrontMattermap[string]any// Parsed YAML front matterHadFrontMatterbool// Whether file had front matterDocdocs.DocFile// Full document metadata// MethodsAddPatch(patchFrontMatterPatch)// Add front matter fieldApplyPatches()// Merge patchesRewriteLinks(contentstring)string// Fix relative linksSerialize()error// Write final output}
Important: Don’t call Serialize() in your transform - it’s automatically called by the pipeline.
Setting Transform Stage and Dependencies
Transforms are organized by stages and dependencies (not priorities):
Available Stages
Stage
Purpose
Example Transforms
StageParse
Extract/parse source content
Front matter parsing
StageBuild
Generate base metadata
Repository info, titles
StageEnrich
Add computed fields
Edit links, custom metadata
StageMerge
Combine/merge data
Merge user + generated data
StageTransform
Modify content
Link rewriting, syntax conversion
StageFinalize
Post-process
Strip headings, escape shortcodes
StageSerialize
Output generation
Write final YAML + content
Declaring Dependencies
Use Dependencies() to specify ordering within a stage:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func(tMyTransform)Dependencies()TransformDependencies{returnTransformDependencies{// This transform must run after these transformsMustRunAfter:[]string{"front_matter_merge","relative_link_rewriter"},// This transform must run before these transformsMustRunBefore:[]string{"front_matter_serialize"},// Capability flags (for documentation)RequiresOriginalFrontMatter:false,ModifiesContent:true,ModifiesFrontMatter:false,}}
Guidelines:
StageParse: Early processing (parsing, reading)
StageBuild-StageEnrich: Metadata manipulation
StageTransform: Content modification
StageFinalize: Cleanup and validation
StageSerialize: Output serialization
Within each stage, transforms are ordered by their dependency declarations using topological sort.
The registry logs the execution order at startup with verbose logging enabled.
Test Individual Transforms
Create a unit test with sample content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
funcTestTransformOutput(t*testing.T){input:=`---
title: Test
---
# Content
> [!NOTE]
> This is a note
`pg:=&PageShim{Content:input}transform:=AdmonitionConverter{}err:=transform.Transform(pg)require.NoError(t,err)assert.Contains(t,pg.Content,"{{ callout }}")}