Repository: gohugoio/hugo (language: go)

Issue:
Make markdownify work as site.Home.RenderString
While working on #9521 (basically render hook support for code blocks), I had to simplify the hook code (mainly to get support for a template per code language). This lead me to the simplification that I'm surprised I didn't think about the first time around: By making `markdownify` just a delegate for `site.Home.RenderString` I can remove a lot of superfluous code. And it it also removes some of the limitations of `markdownify`, mainly that render hooks now just works.

I will consider if we want/need to go the extra mile to be called from the "current page" and not always the home page (which probably be better template wise).

I'm assuming that this is all a good thing, but I'm creating this issue to track it.

/cc  @jmooring 
Don't apply HTML escape to .Attributes passed to render hooks
See https://github.com/gohugoio/hugo/blob/master/markup/goldmark/render_hooks.go#L63

```go
a.attributes[string(attr.Name)] = string(util.EscapeHTML(attr.Value.([]byte)))
```

These are the `.Attributes` passed to the render hooks. When doing this whole _render hooks for code blocks and diagram_ thing, I stumble upon this "not so fantastic" decision of mine from the past: This assumes that 1) What gets rendered is always HTML (which is kind of true for the Markdown bit today) and 2) That all attributes are meant to be rendered. I can see many use cases for passing arbitrary data down to the templates, and having it escaped is unfortunate.

I think it escapes only `<`, `>`, `&`, `'` and `"`, so the side effects of fixing this I would say should be very small, and it's better to do it know than later. If you need to escape you can do it in the template, e.g:

```html
<h{{ .Level }}
  {{- range $k, $v := .Attributes -}}
    {{- printf " %s=%q" $k ($v | htmlEscape) | safeHTMLAttr -}}
  {{- end -}}
>{{ .Text | safeHTML }}</h{{ .Level }}>
```

/cc @jmooring @kaushalmodi @onedrawingperday @moorereason  etc.


Discussion / hints:

Fix patch:
diff --git a/common/hugio/writers.go b/common/hugio/writers.go
index 82c4dca52e4..d8be83a4043 100644
--- a/common/hugio/writers.go
+++ b/common/hugio/writers.go
@@ -18,6 +18,14 @@ import (
 	"io/ioutil"
 )
 
+// As implemented by strings.Builder.
+type FlexiWriter interface {
+	io.Writer
+	io.ByteWriter
+	WriteString(s string) (int, error)
+	WriteRune(r rune) (int, error)
+}
+
 type multiWriteCloser struct {
 	io.Writer
 	closers []io.WriteCloser
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/_default/baseof.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/_default/baseof.html
index 47019072c2d..e2886a0b88c 100644
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/_default/baseof.html
+++ b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/_default/baseof.html
@@ -66,6 +66,14 @@
 
   {{ block "footer" . }}{{ partialCached "site-footer.html" . }}{{ end }}
 
+  {{ if .Page.Store.Get "hasMermaid" }}
+  <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
+  <script>
+    mermaid.initialize({ startOnLoad: true });
+  </script>
+{{ end }}
+
+
 </body>
 
 </html>
diff --git a/docs/content/en/content-management/diagrams.md b/docs/content/en/content-management/diagrams.md
new file mode 100644
index 00000000000..4e3f6164bb3
--- /dev/null
+++ b/docs/content/en/content-management/diagrams.md
@@ -0,0 +1,217 @@
+---
+title: Diagrams
+date: 2022-02-20
+categories: [content management]
+keywords: [diagrams,drawing]
+menu:
+  docs:
+    parent: "content-management"
+    weight: 22
+weight: 22
+toc: true
+---
+
+
+## Mermaid Diagrams
+
+```mermaid
+sequenceDiagram
+    participant Alice
+    participant Bob
+    Alice->>John: Hello John, how are you?
+    loop Healthcheck
+        John->>John: Fight against hypochondria
+    end
+    Note right of John: Rational thoughts <br/>prevail!
+    John-->>Alice: Great!
+    John->>Bob: How about you?
+    Bob-->>John: Jolly good!
+```
+
+
+
+## Goat Ascii Diagram Examples
+
+### Graphics
+
+```goat
+                                                                             .
+    0       3                          P *              Eye /         ^     /
+     *-------*      +y                    \                +)          \   /  Reflection
+  1 /|    2 /|       ^                     \                \           \ v
+   *-------* |       |                v0    \       v3           --------*--------
+   | |4    | |7      |                  *----\-----*
+   | *-----|-*       +-----> +x        /      v X   \          .-.<--------        o
+   |/      |/       /                 /        o     \        | / | Refraction    / \
+   *-------*       v                 /                \        +-'               /   \
+  5       6      +z              v1 *------------------* v2    |                o-----o
+                                                               v
+
+```
+
+### Complex
+
+```goat
++-------------------+                           ^                      .---.
+|    A Box          |__.--.__    __.-->         |      .-.             |   |
+|                   |        '--'               v     | * |<---        |   |
++-------------------+                                  '-'             |   |
+                       Round                                       *---(-. |
+  .-----------------.  .-------.    .----------.         .-------.     | | |
+ |   Mixed Rounded  | |         |  / Diagonals  \        |   |   |     | | |
+ | & Square Corners |  '--. .--'  /              \       |---+---|     '-)-'       .--------.
+ '--+------------+-'  .--. |     '-------+--------'      |   |   |       |        / Search /
+    |            |   |    | '---.        |               '-------'       |       '-+------'
+    |<---------->|   |    |      |       v                Interior                 |     ^
+    '           <---'      '----'   .-----------.              ---.     .---       v     |
+ .------------------.  Diag line    | .-------. +---.              \   /           .     |
+ |   if (a > b)     +---.      .--->| |       | |    | Curved line  \ /           / \    |
+ |   obj->fcn()     |    \    /     | '-------' |<--'                +           /   \   |
+ '------------------'     '--'      '--+--------'      .--. .--.     |  .-.     +Done?+-'
+    .---+-----.                        |   ^           |\ | | /|  .--+ |   |     \   /
+    |   |     | Join        \|/        |   | Curved    | \| |/ | |    \    |      \ /
+    |   |     +---->  o    --o--        '-'  Vertical  '--' '--'  '--  '--'        +  .---.
+ <--+---+-----'       |     /|\                                                    |  | 3 |
+                      v                             not:line    'quotes'        .-'   '---'
+  .-.             .---+--------.            /            A || B   *bold*       |        ^
+ |   |           |   Not a dot  |      <---+---<--    A dash--is not a line    v        |
+  '-'             '---------+--'          /           Nor/is this.            ---
+
+```
+
+### Process
+
+```goat
+                                      .
+   .---------.                       / \
+  |   START   |                     /   \        .-+-------+-.      ___________
+   '----+----'    .-------.    A   /     \   B   | |COMPLEX| |     /           \      .-.
+        |        |   END   |<-----+CHOICE +----->| |       | +--->+ PREPARATION +--->| X |
+        v         '-------'        \     /       | |PROCESS| |     \___________/      '-'
+    .---------.                     \   /        '-+---+---+-'
+   /  INPUT  /                       \ /
+  '-----+---'                         '
+        |                             ^
+        v                             |
+  .-----------.                 .-----+-----.        .-.
+  |  PROCESS  +---------------->|  PROCESS  |<------+ X |
+  '-----------'                 '-----------'        '-'
+```
+
+### File tree
+
+Created from https://arthursonzogni.com/Diagon/#Tree
+
+```goat  { width=300  color="orange" }
+───Linux─┬─Android
+         ├─Debian─┬─Ubuntu─┬─Lubuntu
+         │        │        ├─Kubuntu
+         │        │        ├─Xubuntu
+         │        │        └─Xubuntu
+         │        └─Mint
+         ├─Centos
+         └─Fedora
+```
+
+
+### Sequence Diagram
+
+https://arthursonzogni.com/Diagon/#Sequence
+
+```goat { class="w-40" }
+┌─────┐       ┌───┐
+│Alice│       │Bob│
+└──┬──┘       └─┬─┘
+   │            │  
+   │ Hello Bob! │  
+   │───────────>│  
+   │            │  
+   │Hello Alice!│  
+   │<───────────│  
+┌──┴──┐       ┌─┴─┐
+│Alice│       │Bob│
+└─────┘       └───┘
+
+```
+
+
+### Flowchart
+
+https://arthursonzogni.com/Diagon/#Flowchart
+
+```goat
+   _________________                                                              
+  ╱                 ╲                                                     ┌─────┐ 
+ ╱ DO YOU UNDERSTAND ╲____________________________________________________│GOOD!│ 
+ ╲ FLOW CHARTS?      ╱yes                                                 └──┬──┘ 
+  ╲_________________╱                                                        │    
+           │no                                                               │    
+  _________▽_________                    ______________________              │    
+ ╱                   ╲                  ╱                      ╲    ┌────┐   │    
+╱ OKAY, YOU SEE THE   ╲________________╱ ... AND YOU CAN SEE    ╲___│GOOD│   │    
+╲ LINE LABELED 'YES'? ╱yes             ╲ THE ONES LABELED 'NO'? ╱yes└──┬─┘   │    
+ ╲___________________╱                  ╲______________________╱       │     │    
+           │no                                     │no                 │     │    
+   ________▽_________                     _________▽__________         │     │    
+  ╱                  ╲    ┌───────────┐  ╱                    ╲        │     │    
+ ╱ BUT YOU SEE THE    ╲___│WAIT, WHAT?│ ╱ BUT YOU JUST         ╲___    │     │    
+ ╲ ONES LABELED 'NO'? ╱yes└───────────┘ ╲ FOLLOWED THEM TWICE? ╱yes│   │     │    
+  ╲__________________╱                   ╲____________________╱    │   │     │    
+           │no                                     │no             │   │     │    
+       ┌───▽───┐                                   │               │   │     │    
+       │LISTEN.│                                   └───────┬───────┘   │     │    
+       └───┬───┘                                    ┌──────▽─────┐     │     │    
+     ┌─────▽────┐                                   │(THAT WASN'T│     │     │    
+     │I HATE YOU│                                   │A QUESTION) │     │     │    
+     └──────────┘                                   └──────┬─────┘     │     │    
+                                                      ┌────▽───┐       │     │    
+                                                      │SCREW IT│       │     │    
+                                                      └────┬───┘       │     │    
+                                                           └─────┬─────┘     │    
+                                                                 │           │    
+                                                                 └─────┬─────┘    
+                                                               ┌───────▽──────┐   
+                                                               │LET'S GO DRING│   
+                                                               └───────┬──────┘   
+                                                             ┌─────────▽─────────┐
+                                                             │HEY, I SHOULD TRY  │
+                                                             │INSTALLING FREEBSD!│
+                                                             └───────────────────┘
+
+```
+
+
+### Table
+
+https://arthursonzogni.com/Diagon/#Table
+
+```goat { class="w-80 dark-blue" }
+┌────────────────────────────────────────────────┐
+│                                                │
+├────────────────────────────────────────────────┤
+│SYNTAX     = { PRODUCTION } .                   │
+├────────────────────────────────────────────────┤
+│PRODUCTION = IDENTIFIER "=" EXPRESSION "." .    │
+├────────────────────────────────────────────────┤
+│EXPRESSION = TERM { "|" TERM } .                │
+├────────────────────────────────────────────────┤
+│TERM       = FACTOR { FACTOR } .                │
+├────────────────────────────────────────────────┤
+│FACTOR     = IDENTIFIER                         │
+├────────────────────────────────────────────────┤
+│          | LITERAL                             │
+├────────────────────────────────────────────────┤
+│          | "[" EXPRESSION "]"                  │
+├────────────────────────────────────────────────┤
+│          | "(" EXPRESSION ")"                  │
+├────────────────────────────────────────────────┤
+│          | "{" EXPRESSION "}" .                │
+├────────────────────────────────────────────────┤
+│IDENTIFIER = letter { letter } .                │
+├────────────────────────────────────────────────┤
+│LITERAL    = """" character { character } """" .│
+└────────────────────────────────────────────────┘
+```
+
+
+
diff --git a/docs/layouts/_default/_markup/render-codeblock-goat.html b/docs/layouts/_default/_markup/render-codeblock-goat.html
new file mode 100644
index 00000000000..b1e57e94a7b
--- /dev/null
+++ b/docs/layouts/_default/_markup/render-codeblock-goat.html
@@ -0,0 +1,18 @@
+{{ $width := .Attributes.width }}
+{{ $height := .Attributes.height }}
+{{ $class := .Attributes.class | default "" }}
+<div class="goat svg-container {{ $class }}">
+  {{ with diagrams.Goat .Code }}
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      font-family="Menlo,Lucida Console,monospace"
+      {{ if or $width $height }}
+        {{ with $width }}width="{{ . }}"{{ end }}
+        {{ with $height }}height="{{ . }}"{{ end }}
+      {{ else }}
+        viewBox="0 0 {{ .Width }} {{ .Height }}"
+      {{ end }}>
+      {{ .Body }}
+    </svg>
+  {{ end }}
+</div>
diff --git a/docs/layouts/_default/_markup/render-codeblock-mermaid.html b/docs/layouts/_default/_markup/render-codeblock-mermaid.html
new file mode 100644
index 00000000000..15e4fdfbb77
--- /dev/null
+++ b/docs/layouts/_default/_markup/render-codeblock-mermaid.html
@@ -0,0 +1,4 @@
+<div class="mermaid">
+  {{- .Code | safeHTML }}
+</div>
+{{ .Page.Store.Set "hasMermaid" true }}
diff --git a/go.mod b/go.mod
index c2870abb839..3c39fbfcdd3 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
 	github.com/aws/aws-sdk-go v1.41.14
 	github.com/bep/debounce v1.2.0
 	github.com/bep/gitmap v1.1.2
+	github.com/bep/goat v0.5.0
 	github.com/bep/godartsass v0.12.0
 	github.com/bep/golibsass v1.0.0
 	github.com/bep/gowebp v0.1.0
@@ -19,7 +20,7 @@ require (
 	github.com/dustin/go-humanize v1.0.0
 	github.com/evanw/esbuild v0.14.22
 	github.com/fortytw2/leaktest v1.3.0
-	github.com/frankban/quicktest v1.14.0
+	github.com/frankban/quicktest v1.14.2
 	github.com/fsnotify/fsnotify v1.5.1
 	github.com/getkin/kin-openapi v0.85.0
 	github.com/ghodss/yaml v1.0.0
@@ -29,9 +30,9 @@ require (
 	github.com/gohugoio/locales v0.14.0
 	github.com/gohugoio/localescompressed v0.15.0
 	github.com/gohugoio/testmodBuilder/mods v0.0.0-20190520184928-c56af20f2e95
-	github.com/google/go-cmp v0.5.6
+	github.com/google/go-cmp v0.5.7
 	github.com/gorilla/websocket v1.4.2
-	github.com/hairyhenderson/go-codeowners v0.2.3-0.20201026200250-cdc7c0759690 // indirect
+	github.com/hairyhenderson/go-codeowners v0.2.3-0.20201026200250-cdc7c0759690
 	github.com/jdkato/prose v1.2.1
 	github.com/kylelemons/godebug v1.1.0
 	github.com/kyokomi/emoji/v2 v2.2.8
@@ -57,7 +58,7 @@ require (
 	github.com/spf13/pflag v1.0.5
 	github.com/tdewolff/minify/v2 v2.9.29
 	github.com/yuin/goldmark v1.4.7
-	github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691
+	go.uber.org/atomic v1.9.0
 	gocloud.dev v0.20.0
 	golang.org/x/image v0.0.0-20211028202545-6944b10bf410
 	golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f
diff --git a/go.sum b/go.sum
index bbe41ee08e6..ad635396a6d 100644
--- a/go.sum
+++ b/go.sum
@@ -145,6 +145,10 @@ github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo=
 github.com/bep/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
 github.com/bep/gitmap v1.1.2 h1:zk04w1qc1COTZPPYWDQHvns3y1afOsdRfraFQ3qI840=
 github.com/bep/gitmap v1.1.2/go.mod h1:g9VRETxFUXNWzMiuxOwcudo6DfZkW9jOsOW0Ft4kYaY=
+github.com/bep/goat v0.0.0-20220222160823-cc97a132eb5e h1:On3hMv9ffG+0fgPIjKPXiFu5QVS9jM1Vzr5/ghmSLy4=
+github.com/bep/goat v0.0.0-20220222160823-cc97a132eb5e/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c=
+github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA=
+github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c=
 github.com/bep/godartsass v0.12.0 h1:VvGLA4XpXUjKvp53SI05YFLhRFJ78G+Ybnlaz6Oul7E=
 github.com/bep/godartsass v0.12.0/go.mod h1:nXQlHHk4H1ghUk6n/JkYKG5RD43yJfcfp5aHRqT/pc4=
 github.com/bep/golibsass v1.0.0 h1:gNguBMSDi5yZEZzVZP70YpuFQE3qogJIGUlrVILTmOw=
@@ -240,6 +244,8 @@ github.com/frankban/quicktest v1.11.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P
 github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU=
 github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss=
 github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
+github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnXyBns=
+github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
 github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
@@ -325,6 +331,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
 github.com/google/go-replayers/grpcreplay v0.1.0 h1:eNb1y9rZFmY4ax45uEEECSa8fsxGRU+8Bil52ASAwic=
 github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE=
 github.com/google/go-replayers/httpreplay v0.1.0 h1:AX7FUb4BjrrzNvblr/OlgwrmFiep6soj5K2QSDW7BGk=
@@ -624,6 +632,8 @@ go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
 gocloud.dev v0.20.0 h1:mbEKMfnyPV7W1Rj35R1xXfjszs9dXkwSOq2KoFr25g8=
diff --git a/helpers/content.go b/helpers/content.go
index 157f75079ec..3e674bca493 100644
--- a/helpers/content.go
+++ b/helpers/content.go
@@ -30,6 +30,7 @@ import (
 	"github.com/spf13/afero"
 
 	"github.com/gohugoio/hugo/markup/converter"
+	"github.com/gohugoio/hugo/markup/converter/hooks"
 
 	"github.com/gohugoio/hugo/markup"
 
@@ -47,8 +48,8 @@ var (
 // ContentSpec provides functionality to render markdown content.
 type ContentSpec struct {
 	Converters          markup.ConverterProvider
-	MardownConverter    converter.Converter // Markdown converter with no document context
 	anchorNameSanitizer converter.AnchorNameSanitizer
+	getRenderer         func(t hooks.RendererType, id interface{}) interface{}
 
 	// SummaryLength is the length of the summary that Hugo extracts from a content.
 	summaryLength int
@@ -88,7 +89,6 @@ func NewContentSpec(cfg config.Provider, logger loggers.Logger, contentFs afero.
 	if err != nil {
 		return nil, err
 	}
-	spec.MardownConverter = conv
 	if as, ok := conv.(converter.AnchorNameSanitizer); ok {
 		spec.anchorNameSanitizer = as
 	} else {
@@ -192,14 +192,6 @@ func ExtractTOC(content []byte) (newcontent []byte, toc []byte) {
 	return
 }
 
-func (c *ContentSpec) RenderMarkdown(src []byte) ([]byte, error) {
-	b, err := c.MardownConverter.Convert(converter.RenderContext{Src: src})
-	if err != nil {
-		return nil, err
-	}
-	return b.Bytes(), nil
-}
-
 func (c *ContentSpec) SanitizeAnchorName(s string) string {
 	return c.anchorNameSanitizer.SanitizeAnchorName(s)
 }
diff --git a/hugolib/page.go b/hugolib/page.go
index 83b654cc05f..7101af8143f 100644
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -22,6 +22,8 @@ import (
 	"sort"
 	"strings"
 
+	"go.uber.org/atomic"
+
 	"github.com/gohugoio/hugo/identity"
 
 	"github.com/gohugoio/hugo/markup/converter"
@@ -47,7 +49,6 @@ import (
 
 	"github.com/gohugoio/hugo/common/collections"
 	"github.com/gohugoio/hugo/common/text"
-	"github.com/gohugoio/hugo/markup/converter/hooks"
 	"github.com/gohugoio/hugo/resources"
 	"github.com/gohugoio/hugo/resources/page"
 	"github.com/gohugoio/hugo/resources/resource"
@@ -118,6 +119,9 @@ type pageState struct {
 	// formats (for all sites).
 	pageOutputs []*pageOutput
 
+	// Used to determine if we can reuse content across output formats.
+	pageOutputTemplateVariationsState *atomic.Uint32
+
 	// This will be shifted out when we start to render a new output format.
 	*pageOutput
 
@@ -125,6 +129,10 @@ type pageState struct {
 	*pageCommon
 }
 
+func (p *pageState) reusePageOutputContent() bool {
+	return p.pageOutputTemplateVariationsState.Load() == 1
+}
+
 func (p *pageState) Err() error {
 	return nil
 }
@@ -394,56 +402,6 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error {
 	return nil
 }
 
-func (p *pageState) createRenderHooks(f output.Format) (hooks.Renderers, error) {
-	layoutDescriptor := p.getLayoutDescriptor()
-	layoutDescriptor.RenderingHook = true
-	layoutDescriptor.LayoutOverride = false
-	layoutDescriptor.Layout = ""
-
-	var renderers hooks.Renderers
-
-	layoutDescriptor.Kind = "render-link"
-	templ, templFound, err := p.s.Tmpl().LookupLayout(layoutDescriptor, f)
-	if err != nil {
-		return renderers, err
-	}
-	if templFound {
-		renderers.LinkRenderer = hookRenderer{
-			templateHandler: p.s.Tmpl(),
-			SearchProvider:  templ.(identity.SearchProvider),
-			templ:           templ,
-		}
-	}
-
-	layoutDescriptor.Kind = "render-image"
-	templ, templFound, err = p.s.Tmpl().LookupLayout(layoutDescriptor, f)
-	if err != nil {
-		return renderers, err
-	}
-	if templFound {
-		renderers.ImageRenderer = hookRenderer{
-			templateHandler: p.s.Tmpl(),
-			SearchProvider:  templ.(identity.SearchProvider),
-			templ:           templ,
-		}
-	}
-
-	layoutDescriptor.Kind = "render-heading"
-	templ, templFound, err = p.s.Tmpl().LookupLayout(layoutDescriptor, f)
-	if err != nil {
-		return renderers, err
-	}
-	if templFound {
-		renderers.HeadingRenderer = hookRenderer{
-			templateHandler: p.s.Tmpl(),
-			SearchProvider:  templ.(identity.SearchProvider),
-			templ:           templ,
-		}
-	}
-
-	return renderers, nil
-}
-
 func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor {
 	p.layoutDescriptorInit.Do(func() {
 		var section string
@@ -867,7 +825,7 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
 
 	if isRenderingSite {
 		cp := p.pageOutput.cp
-		if cp == nil {
+		if cp == nil && p.reusePageOutputContent() {
 			// Look for content to reuse.
 			for i := 0; i < len(p.pageOutputs); i++ {
 				if i == idx {
@@ -875,7 +833,7 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
 				}
 				po := p.pageOutputs[i]
 
-				if po.cp != nil && po.cp.reuse {
+				if po.cp != nil {
 					cp = po.cp
 					break
 				}
diff --git a/hugolib/page__new.go b/hugolib/page__new.go
index 91847784313..b8395d5ca06 100644
--- a/hugolib/page__new.go
+++ b/hugolib/page__new.go
@@ -17,6 +17,8 @@ import (
 	"html/template"
 	"strings"
 
+	"go.uber.org/atomic"
+
 	"github.com/gohugoio/hugo/common/hugo"
 
 	"github.com/gohugoio/hugo/common/maps"
@@ -36,7 +38,8 @@ func newPageBase(metaProvider *pageMeta) (*pageState, error) {
 	s := metaProvider.s
 
 	ps := &pageState{
-		pageOutput: nopPageOutput,
+		pageOutput:                        nopPageOutput,
+		pageOutputTemplateVariationsState: atomic.NewUint32(0),
 		pageCommon: &pageCommon{
 			FileProvider:            metaProvider,
 			AuthorProvider:          metaProvider,
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
index bd4e35a5b0c..29beb672e55 100644
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -32,6 +32,7 @@ import (
 
 	"github.com/gohugoio/hugo/markup/converter"
 
+	"github.com/alecthomas/chroma/lexers"
 	"github.com/gohugoio/hugo/lazy"
 
 	bp "github.com/gohugoio/hugo/bufferpool"
@@ -109,16 +110,8 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err
 			return err
 		}
 
-		enableReuse := !(hasShortcodeVariants || cp.renderHooksHaveVariants)
-
-		if enableReuse {
-			// Reuse this for the other output formats.
-			// We may improve on this, but we really want to avoid re-rendering the content
-			// to all output formats.
-			// The current rule is that if you need output format-aware shortcodes or
-			// content rendering hooks, create a output format-specific template, e.g.
-			// myshortcode.amp.html.
-			cp.enableReuse()
+		if hasShortcodeVariants {
+			p.pageOutputTemplateVariationsState.Store(2)
 		}
 
 		cp.workContent = p.contentToRender(cp.contentPlaceholders)
@@ -199,19 +192,10 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err
 		return nil
 	}
 
-	// Recursive loops can only happen in content files with template code (shortcodes etc.)
-	// Avoid creating new goroutines if we don't have to.
-	needTimeout := p.shortcodeState.hasShortcodes() || cp.renderHooks != nil
-
-	if needTimeout {
-		cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
-			return nil, initContent()
-		})
-	} else {
-		cp.initMain = parent.Branch(func() (interface{}, error) {
-			return nil, initContent()
-		})
-	}
+	// There may be recursive loops in shortcodes and render hooks.
+	cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
+		return nil, initContent()
+	})
 
 	cp.initPlain = cp.initMain.Branch(func() (interface{}, error) {
 		cp.plain = helpers.StripHTML(string(cp.content))
@@ -229,18 +213,14 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err
 }
 
 type renderHooks struct {
-	hooks hooks.Renderers
-	init  sync.Once
+	getRenderer hooks.GetRendererFunc
+	init        sync.Once
 }
 
 // pageContentOutput represents the Page content for a given output format.
 type pageContentOutput struct {
 	f output.Format
 
-	// If we can reuse this for other output formats.
-	reuse     bool
-	reuseInit sync.Once
-
 	p *pageState
 
 	// Lazy load dependencies
@@ -250,13 +230,9 @@ type pageContentOutput struct {
 	placeholdersEnabled     bool
 	placeholdersEnabledInit sync.Once
 
+	// Renders Markdown hooks.
 	renderHooks *renderHooks
 
-	// Set if there are more than one output format variant
-	renderHooksHaveVariants bool // TODO(bep) reimplement this in another way, consolidate with shortcodes
-
-	// Content state
-
 	workContent       []byte
 	dependencyTracker identity.Manager // Set in server mode.
 
@@ -440,55 +416,107 @@ func (p *pageContentOutput) initRenderHooks() error {
 		return nil
 	}
 
-	var initErr error
-
 	p.renderHooks.init.Do(func() {
-		ps := p.p
-
-		c := ps.getContentConverter()
-		if c == nil || !c.Supports(converter.FeatureRenderHooks) {
-			return
+		if p.p.pageOutputTemplateVariationsState.Load() == 0 {
+			p.p.pageOutputTemplateVariationsState.Store(1)
 		}
 
-		h, err := ps.createRenderHooks(p.f)
-		if err != nil {
-			initErr = err
-			return
+		type cacheKey struct {
+			tp hooks.RendererType
+			id interface{}
+			f  output.Format
 		}
-		p.renderHooks.hooks = h
-
-		if !p.renderHooksHaveVariants || h.IsZero() {
-			// Check if there is a different render hooks template
-			// for any of the other page output formats.
-			// If not, we can reuse this.
-			for _, po := range ps.pageOutputs {
-				if po.f.Name != p.f.Name {
-					h2, err := ps.createRenderHooks(po.f)
-					if err != nil {
-						initErr = err
-						return
-					}
 
-					if h2.IsZero() {
-						continue
-					}
+		renderCache := make(map[cacheKey]interface{})
+		var renderCacheMu sync.Mutex
+
+		p.renderHooks.getRenderer = func(tp hooks.RendererType, id interface{}) interface{} {
+			renderCacheMu.Lock()
+			defer renderCacheMu.Unlock()
+
+			key := cacheKey{tp: tp, id: id, f: p.f}
+			if r, ok := renderCache[key]; ok {
+				return r
+			}
 
-					if p.renderHooks.hooks.IsZero() {
-						p.renderHooks.hooks = h2
+			layoutDescriptor := p.p.getLayoutDescriptor()
+			layoutDescriptor.RenderingHook = true
+			layoutDescriptor.LayoutOverride = false
+			layoutDescriptor.Layout = ""
+
+			switch tp {
+			case hooks.LinkRendererType:
+				layoutDescriptor.Kind = "render-link"
+			case hooks.ImageRendererType:
+				layoutDescriptor.Kind = "render-image"
+			case hooks.HeadingRendererType:
+				layoutDescriptor.Kind = "render-heading"
+			case hooks.CodeBlockRendererType:
+				layoutDescriptor.Kind = "render-codeblock"
+				if id != nil {
+					lang := id.(string)
+					lexer := lexers.Get(lang)
+					if lexer != nil {
+						layoutDescriptor.KindVariants = strings.Join(lexer.Config().Aliases, ",")
+					} else {
+						layoutDescriptor.KindVariants = lang
 					}
+				}
+			}
 
-					p.renderHooksHaveVariants = !h2.Eq(p.renderHooks.hooks)
+			getHookTemplate := func(f output.Format) (tpl.Template, bool) {
+				templ, found, err := p.p.s.Tmpl().LookupLayout(layoutDescriptor, f)
+				if err != nil {
+					panic(err)
+				}
+				return templ, found
+			}
+
+			templ, found1 := getHookTemplate(p.f)
 
-					if p.renderHooksHaveVariants {
-						break
+			if p.p.reusePageOutputContent() {
+				// Check if some of the other output formats would give a different template.
+				for _, f := range p.p.s.renderFormats {
+					if f.Name == p.f.Name {
+						continue
+					}
+					templ2, found2 := getHookTemplate(f)
+					if found2 {
+						if !found1 {
+							templ = templ2
+							found1 = true
+							break
+						}
+
+						if templ != templ2 {
+							p.p.pageOutputTemplateVariationsState.Store(2)
+							break
+						}
 					}
+				}
+			}
 
+			if !found1 {
+				if tp == hooks.CodeBlockRendererType {
+					// No user provided tempplate for code blocks, so we use the native Go code version -- which is also faster.
+					r := p.p.s.ContentSpec.Converters.GetHighlighter()
+					renderCache[key] = r
+					return r
 				}
+				return nil
 			}
+
+			r := hookRendererTemplate{
+				templateHandler: p.p.s.Tmpl(),
+				SearchProvider:  templ.(identity.SearchProvider),
+				templ:           templ,
+			}
+			renderCache[key] = r
+			return r
 		}
 	})
 
-	return initErr
+	return nil
 }
 
 func (p *pageContentOutput) setAutoSummary() error {
@@ -512,6 +540,9 @@ func (p *pageContentOutput) setAutoSummary() error {
 }
 
 func (cp *pageContentOutput) renderContent(content []byte, renderTOC bool) (converter.Result, error) {
+	if err := cp.initRenderHooks(); err != nil {
+		return nil, err
+	}
 	c := cp.p.getContentConverter()
 	return cp.renderContentWithConverter(c, content, renderTOC)
 }
@@ -521,7 +552,7 @@ func (cp *pageContentOutput) renderContentWithConverter(c converter.Converter, c
 		converter.RenderContext{
 			Src:         content,
 			RenderTOC:   renderTOC,
-			RenderHooks: cp.renderHooks.hooks,
+			GetRenderer: cp.renderHooks.getRenderer,
 		})
 
 	if err == nil {
@@ -570,12 +601,6 @@ func (p *pageContentOutput) enablePlaceholders() {
 	})
 }
 
-func (p *pageContentOutput) enableReuse() {
-	p.reuseInit.Do(func() {
-		p.reuse = true
-	})
-}
-
 // these will be shifted out when rendering a given output format.
 type pagePerOutputProviders interface {
 	targetPather
diff --git a/hugolib/site.go b/hugolib/site.go
index 02380a6e73c..57821ee93c4 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -30,6 +30,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/gohugoio/hugo/common/hugio"
 	"github.com/gohugoio/hugo/common/types"
 	"github.com/gohugoio/hugo/modules"
 	"golang.org/x/text/unicode/norm"
@@ -54,12 +55,11 @@ import (
 
 	"github.com/gohugoio/hugo/common/maps"
 
-	"github.com/pkg/errors"
-
 	"github.com/gohugoio/hugo/common/text"
 
 	"github.com/gohugoio/hugo/common/hugo"
 	"github.com/gohugoio/hugo/publisher"
+	"github.com/pkg/errors"
 	_errors "github.com/pkg/errors"
 
 	"github.com/gohugoio/hugo/langs"
@@ -1773,19 +1773,23 @@ var infoOnMissingLayout = map[string]bool{
 	"404": true,
 }
 
-// hookRenderer is the canonical implementation of all hooks.ITEMRenderer,
+// hookRendererTemplate is the canonical implementation of all hooks.ITEMRenderer,
 // where ITEM is the thing being hooked.
-type hookRenderer struct {
+type hookRendererTemplate struct {
 	templateHandler tpl.TemplateHandler
 	identity.SearchProvider
 	templ tpl.Template
 }
 
-func (hr hookRenderer) RenderLink(w io.Writer, ctx hooks.LinkContext) error {
+func (hr hookRendererTemplate) RenderLink(w io.Writer, ctx hooks.LinkContext) error {
+	return hr.templateHandler.Execute(hr.templ, w, ctx)
+}
+
+func (hr hookRendererTemplate) RenderHeading(w io.Writer, ctx hooks.HeadingContext) error {
 	return hr.templateHandler.Execute(hr.templ, w, ctx)
 }
 
-func (hr hookRenderer) RenderHeading(w io.Writer, ctx hooks.HeadingContext) error {
+func (hr hookRendererTemplate) RenderCodeblock(w hugio.FlexiWriter, ctx hooks.CodeblockContext) error {
 	return hr.templateHandler.Execute(hr.templ, w, ctx)
 }
 
diff --git a/hugolib/site_sections.go b/hugolib/site_sections.go
index ae343716eaa..50dfe6ffa9c 100644
--- a/hugolib/site_sections.go
+++ b/hugolib/site_sections.go
@@ -19,14 +19,10 @@ import (
 
 // Sections returns the top level sections.
 func (s *SiteInfo) Sections() page.Pages {
-	home, err := s.Home()
-	if err == nil {
-		return home.Sections()
-	}
-	return nil
+	return s.Home().Sections()
 }
 
 // Home is a shortcut to the home page, equivalent to .Site.GetPage "home".
-func (s *SiteInfo) Home() (page.Page, error) {
-	return s.s.home, nil
+func (s *SiteInfo) Home() page.Page {
+	return s.s.home
 }
diff --git a/markup/converter/converter.go b/markup/converter/converter.go
index 180208a7bfc..30addfec657 100644
--- a/markup/converter/converter.go
+++ b/markup/converter/converter.go
@@ -21,6 +21,7 @@ import (
 	"github.com/gohugoio/hugo/config"
 	"github.com/gohugoio/hugo/identity"
 	"github.com/gohugoio/hugo/markup/converter/hooks"
+	"github.com/gohugoio/hugo/markup/highlight"
 	"github.com/gohugoio/hugo/markup/markup_config"
 	"github.com/gohugoio/hugo/markup/tableofcontents"
 	"github.com/spf13/afero"
@@ -34,7 +35,7 @@ type ProviderConfig struct {
 	ContentFs afero.Fs
 	Logger    loggers.Logger
 	Exec      *hexec.Exec
-	Highlight func(code, lang, optsStr string) (string, error)
+	highlight.Highlighter
 }
 
 // ProviderProvider creates converter providers.
@@ -127,9 +128,10 @@ type DocumentContext struct {
 
 // RenderContext holds contextual information about the content to render.
 type RenderContext struct {
-	Src         []byte
-	RenderTOC   bool
-	RenderHooks hooks.Renderers
+	Src       []byte
+	RenderTOC bool
+
+	GetRenderer hooks.GetRendererFunc
 }
 
 var FeatureRenderHooks = identity.NewPathIdentity("markup", "renderingHooks")
diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go
index d36dad28806..987cb1dc36b 100644
--- a/markup/converter/hooks/hooks.go
+++ b/markup/converter/hooks/hooks.go
@@ -14,15 +14,17 @@
 package hooks
 
 import (
-	"fmt"
 	"io"
-	"strings"
 
+	"github.com/gohugoio/hugo/common/hugio"
 	"github.com/gohugoio/hugo/identity"
+	"github.com/gohugoio/hugo/markup/internal/attributes"
 )
 
+var _ AttributesOptionsSliceProvider = (*attributes.AttributesHolder)(nil)
+
 type AttributesProvider interface {
-	Attributes() map[string]string
+	Attributes() map[string]interface{}
 }
 
 type LinkContext interface {
@@ -33,11 +35,30 @@ type LinkContext interface {
 	PlainText() string
 }
 
+type CodeblockContext interface {
+	AttributesProvider
+	Options() map[string]interface{}
+	Lang() string
+	Code() string
+	Ordinal() int
+	Page() interface{}
+}
+
+type AttributesOptionsSliceProvider interface {
+	AttributesSlice() []attributes.Attribute
+	OptionsSlice() []attributes.Attribute
+}
+
 type LinkRenderer interface {
 	RenderLink(w io.Writer, ctx LinkContext) error
 	identity.Provider
 }
 
+type CodeBlockRenderer interface {
+	RenderCodeblock(w hugio.FlexiWriter, ctx CodeblockContext) error
+	identity.Provider
+}
+
 // HeadingContext contains accessors to all attributes that a HeadingRenderer
 // can use to render a heading.
 type HeadingContext interface {
@@ -63,70 +84,13 @@ type HeadingRenderer interface {
 	identity.Provider
 }
 
-type Renderers struct {
-	LinkRenderer    LinkRenderer
-	ImageRenderer   LinkRenderer
-	HeadingRenderer HeadingRenderer
-}
-
-func (r Renderers) Eq(other interface{}) bool {
-	ro, ok := other.(Renderers)
-	if !ok {
-		return false
-	}
-
-	if r.IsZero() || ro.IsZero() {
-		return r.IsZero() && ro.IsZero()
-	}
-
-	var b1, b2 bool
-	b1, b2 = r.ImageRenderer == nil, ro.ImageRenderer == nil
-	if (b1 || b2) && (b1 != b2) {
-		return false
-	}
-	if !b1 && r.ImageRenderer.GetIdentity() != ro.ImageRenderer.GetIdentity() {
-		return false
-	}
-
-	b1, b2 = r.LinkRenderer == nil, ro.LinkRenderer == nil
-	if (b1 || b2) && (b1 != b2) {
-		return false
-	}
-	if !b1 && r.LinkRenderer.GetIdentity() != ro.LinkRenderer.GetIdentity() {
-		return false
-	}
-
-	b1, b2 = r.HeadingRenderer == nil, ro.HeadingRenderer == nil
-	if (b1 || b2) && (b1 != b2) {
-		return false
-	}
-	if !b1 && r.HeadingRenderer.GetIdentity() != ro.HeadingRenderer.GetIdentity() {
-		return false
-	}
-
-	return true
-}
-
-func (r Renderers) IsZero() bool {
-	return r.HeadingRenderer == nil && r.LinkRenderer == nil && r.ImageRenderer == nil
-}
+type RendererType int
 
-func (r Renderers) String() string {
-	if r.IsZero() {
-		return "<zero>"
-	}
-
-	var sb strings.Builder
-
-	if r.LinkRenderer != nil {
-		sb.WriteString(fmt.Sprintf("LinkRenderer<%s>|", r.LinkRenderer.GetIdentity()))
-	}
-	if r.HeadingRenderer != nil {
-		sb.WriteString(fmt.Sprintf("HeadingRenderer<%s>|", r.HeadingRenderer.GetIdentity()))
-	}
-	if r.ImageRenderer != nil {
-		sb.WriteString(fmt.Sprintf("ImageRenderer<%s>|", r.ImageRenderer.GetIdentity()))
-	}
+const (
+	LinkRendererType RendererType = iota + 1
+	ImageRendererType
+	HeadingRendererType
+	CodeBlockRendererType
+)
 
-	return sb.String()
-}
+type GetRendererFunc func(t RendererType, id interface{}) interface{}
diff --git a/markup/goldmark/codeblocks/render.go b/markup/goldmark/codeblocks/render.go
new file mode 100644
index 00000000000..59d142e23d0
--- /dev/null
+++ b/markup/goldmark/codeblocks/render.go
@@ -0,0 +1,159 @@
+// Copyright 2022 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package codeblocks
+
+import (
+	"bytes"
+	"fmt"
+
+	"github.com/gohugoio/hugo/markup/converter/hooks"
+	"github.com/gohugoio/hugo/markup/goldmark/internal/render"
+	"github.com/gohugoio/hugo/markup/internal/attributes"
+	"github.com/yuin/goldmark"
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/parser"
+	"github.com/yuin/goldmark/renderer"
+	"github.com/yuin/goldmark/text"
+	"github.com/yuin/goldmark/util"
+)
+
+type (
+	diagrams     struct{}
+	htmlRenderer struct{}
+)
+
+func New() goldmark.Extender {
+	return &diagrams{}
+}
+
+func (e *diagrams) Extend(m goldmark.Markdown) {
+	m.Parser().AddOptions(
+		parser.WithASTTransformers(
+			util.Prioritized(&Transformer{}, 100),
+		),
+	)
+	m.Renderer().AddOptions(renderer.WithNodeRenderers(
+		util.Prioritized(newHTMLRenderer(), 100),
+	))
+}
+
+func newHTMLRenderer() renderer.NodeRenderer {
+	r := &htmlRenderer{}
+	return r
+}
+
+func (r *htmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+	reg.Register(KindCodeBlock, r.renderCodeBlock)
+}
+
+func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	ctx := w.(*render.Context)
+
+	if entering {
+		return ast.WalkContinue, nil
+	}
+
+	n := node.(*codeBlock)
+	lang := string(n.b.Language(src))
+	ordinal := n.ordinal
+
+	var buff bytes.Buffer
+
+	l := n.b.Lines().Len()
+	for i := 0; i < l; i++ {
+		line := n.b.Lines().At(i)
+		buff.Write(line.Value(src))
+	}
+	text := buff.String()
+
+	var info []byte
+	if n.b.Info != nil {
+		info = n.b.Info.Segment.Value(src)
+	}
+	attrs := getAttributes(n.b, info)
+
+	v := ctx.RenderContext().GetRenderer(hooks.CodeBlockRendererType, lang)
+	if v == nil {
+		return ast.WalkStop, fmt.Errorf("no code renderer found for %q", lang)
+	}
+
+	cr := v.(hooks.CodeBlockRenderer)
+
+	err := cr.RenderCodeblock(
+		w,
+		codeBlockContext{
+			page:             ctx.DocumentContext().Document,
+			lang:             lang,
+			code:             text,
+			ordinal:          ordinal,
+			AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerCodeBlock),
+		},
+	)
+
+	ctx.AddIdentity(cr)
+
+	return ast.WalkContinue, err
+}
+
+type codeBlockContext struct {
+	page    interface{}
+	lang    string
+	code    string
+	ordinal int
+	*attributes.AttributesHolder
+}
+
+func (c codeBlockContext) Page() interface{} {
+	return c.page
+}
+
+func (c codeBlockContext) Lang() string {
+	return c.lang
+}
+
+func (c codeBlockContext) Code() string {
+	return c.code
+}
+
+func (c codeBlockContext) Ordinal() int {
+	return c.ordinal
+}
+
+func getAttributes(node *ast.FencedCodeBlock, infostr []byte) []ast.Attribute {
+	if node.Attributes() != nil {
+		return node.Attributes()
+	}
+	if infostr != nil {
+		attrStartIdx := -1
+
+		for idx, char := range infostr {
+			if char == '{' {
+				attrStartIdx = idx
+				break
+			}
+		}
+
+		if attrStartIdx > 0 {
+			n := ast.NewTextBlock() // dummy node for storing attributes
+			attrStr := infostr[attrStartIdx:]
+			if attrs, hasAttr := parser.ParseAttributes(text.NewReader(attrStr)); hasAttr {
+				for _, attr := range attrs {
+					n.SetAttribute(attr.Name, attr.Value)
+				}
+				return n.Attributes()
+			}
+		}
+	}
+	return nil
+}
diff --git a/markup/goldmark/codeblocks/transform.go b/markup/goldmark/codeblocks/transform.go
new file mode 100644
index 00000000000..791e99a5c3c
--- /dev/null
+++ b/markup/goldmark/codeblocks/transform.go
@@ -0,0 +1,53 @@
+package codeblocks
+
+import (
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/parser"
+	"github.com/yuin/goldmark/text"
+)
+
+// Kind is the kind of an Hugo code block.
+var KindCodeBlock = ast.NewNodeKind("HugoCodeBlock")
+
+// Its raw contents are the plain text of the code block.
+type codeBlock struct {
+	ast.BaseBlock
+	ordinal int
+	b       *ast.FencedCodeBlock
+}
+
+func (*codeBlock) Kind() ast.NodeKind { return KindCodeBlock }
+
+func (*codeBlock) IsRaw() bool { return true }
+
+func (b *codeBlock) Dump(src []byte, level int) {
+}
+
+type Transformer struct{}
+
+// Transform transforms the provided Markdown AST.
+func (*Transformer) Transform(doc *ast.Document, reader text.Reader, pctx parser.Context) {
+	var codeBlocks []*ast.FencedCodeBlock
+
+	ast.Walk(doc, func(node ast.Node, enter bool) (ast.WalkStatus, error) {
+		if !enter {
+			return ast.WalkContinue, nil
+		}
+
+		cb, ok := node.(*ast.FencedCodeBlock)
+		if !ok {
+			return ast.WalkContinue, nil
+		}
+
+		codeBlocks = append(codeBlocks, cb)
+		return ast.WalkContinue, nil
+	})
+
+	for i, cb := range codeBlocks {
+		b := &codeBlock{b: cb, ordinal: i}
+		parent := cb.Parent()
+		if parent != nil {
+			parent.ReplaceChild(parent, cb, b)
+		}
+	}
+}
diff --git a/markup/goldmark/convert.go b/markup/goldmark/convert.go
index c547fe1e0d1..4c1641a0b56 100644
--- a/markup/goldmark/convert.go
+++ b/markup/goldmark/convert.go
@@ -17,12 +17,12 @@ package goldmark
 import (
 	"bytes"
 	"fmt"
-	"math/bits"
 	"path/filepath"
 	"runtime/debug"
 
+	"github.com/gohugoio/hugo/markup/goldmark/codeblocks"
 	"github.com/gohugoio/hugo/markup/goldmark/internal/extensions/attributes"
-	"github.com/yuin/goldmark/ast"
+	"github.com/gohugoio/hugo/markup/goldmark/internal/render"
 
 	"github.com/gohugoio/hugo/identity"
 
@@ -32,16 +32,13 @@ import (
 
 	"github.com/gohugoio/hugo/hugofs"
 	"github.com/gohugoio/hugo/markup/converter"
-	"github.com/gohugoio/hugo/markup/highlight"
 	"github.com/gohugoio/hugo/markup/tableofcontents"
 	"github.com/yuin/goldmark"
-	hl "github.com/yuin/goldmark-highlighting"
 	"github.com/yuin/goldmark/extension"
 	"github.com/yuin/goldmark/parser"
 	"github.com/yuin/goldmark/renderer"
 	"github.com/yuin/goldmark/renderer/html"
 	"github.com/yuin/goldmark/text"
-	"github.com/yuin/goldmark/util"
 )
 
 // Provider is the package entry point.
@@ -104,7 +101,7 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
 	)
 
 	if mcfg.Highlight.CodeFences {
-		extensions = append(extensions, newHighlighting(mcfg.Highlight))
+		extensions = append(extensions, codeblocks.New())
 	}
 
 	if cfg.Extensions.Table {
@@ -178,65 +175,6 @@ func (c converterResult) GetIdentities() identity.Identities {
 	return c.ids
 }
 
-type bufWriter struct {
-	*bytes.Buffer
-}
-
-const maxInt = 1<<(bits.UintSize-1) - 1
-
-func (b *bufWriter) Available() int {
-	return maxInt
-}
-
-func (b *bufWriter) Buffered() int {
-	return b.Len()
-}
-
-func (b *bufWriter) Flush() error {
-	return nil
-}
-
-type renderContext struct {
-	*bufWriter
-	positions []int
-	renderContextData
-}
-
-func (ctx *renderContext) pushPos(n int) {
-	ctx.positions = append(ctx.positions, n)
-}
-
-func (ctx *renderContext) popPos() int {
-	i := len(ctx.positions) - 1
-	p := ctx.positions[i]
-	ctx.positions = ctx.positions[:i]
-	return p
-}
-
-type renderContextData interface {
-	RenderContext() converter.RenderContext
-	DocumentContext() converter.DocumentContext
-	AddIdentity(id identity.Provider)
-}
-
-type renderContextDataHolder struct {
-	rctx converter.RenderContext
-	dctx converter.DocumentContext
-	ids  identity.Manager
-}
-
-func (ctx *renderContextDataHolder) RenderContext() converter.RenderContext {
-	return ctx.rctx
-}
-
-func (ctx *renderContextDataHolder) DocumentContext() converter.DocumentContext {
-	return ctx.dctx
-}
-
-func (ctx *renderContextDataHolder) AddIdentity(id identity.Provider) {
-	ctx.ids.Add(id)
-}
-
 var converterIdentity = identity.KeyValueIdentity{Key: "goldmark", Value: "converter"}
 
 func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result converter.Result, err error) {
@@ -251,7 +189,7 @@ func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result convert
 		}
 	}()
 
-	buf := &bufWriter{Buffer: &bytes.Buffer{}}
+	buf := &render.BufWriter{Buffer: &bytes.Buffer{}}
 	result = buf
 	pctx := c.newParserContext(ctx)
 	reader := text.NewReader(ctx.Src)
@@ -261,15 +199,15 @@ func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result convert
 		parser.WithContext(pctx),
 	)
 
-	rcx := &renderContextDataHolder{
-		rctx: ctx,
-		dctx: c.ctx,
-		ids:  identity.NewManager(converterIdentity),
+	rcx := &render.RenderContextDataHolder{
+		Rctx: ctx,
+		Dctx: c.ctx,
+		IDs:  identity.NewManager(converterIdentity),
 	}
 
-	w := &renderContext{
-		bufWriter:         buf,
-		renderContextData: rcx,
+	w := &render.Context{
+		BufWriter:   buf,
+		ContextData: rcx,
 	}
 
 	if err := c.md.Renderer().Render(w, ctx.Src, doc); err != nil {
@@ -278,7 +216,7 @@ func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result convert
 
 	return converterResult{
 		Result: buf,
-		ids:    rcx.ids.GetIdentities(),
+		ids:    rcx.IDs.GetIdentities(),
 		toc:    pctx.TableOfContents(),
 	}, nil
 }
@@ -309,63 +247,3 @@ func (p *parserContext) TableOfContents() tableofcontents.Root {
 	}
 	return tableofcontents.Root{}
 }
-
-func newHighlighting(cfg highlight.Config) goldmark.Extender {
-	return hl.NewHighlighting(
-		hl.WithStyle(cfg.Style),
-		hl.WithGuessLanguage(cfg.GuessSyntax),
-		hl.WithCodeBlockOptions(highlight.GetCodeBlockOptions()),
-		hl.WithFormatOptions(
-			cfg.ToHTMLOptions()...,
-		),
-
-		hl.WithWrapperRenderer(func(w util.BufWriter, ctx hl.CodeBlockContext, entering bool) {
-			var language string
-			if l, hasLang := ctx.Language(); hasLang {
-				language = string(l)
-			}
-
-			if ctx.Highlighted() {
-				if entering {
-					writeDivStart(w, ctx)
-				} else {
-					writeDivEnd(w)
-				}
-			} else {
-				if entering {
-					highlight.WritePreStart(w, language, "")
-				} else {
-					highlight.WritePreEnd(w)
-				}
-			}
-		}),
-	)
-}
-
-func writeDivStart(w util.BufWriter, ctx hl.CodeBlockContext) {
-	w.WriteString(`<div class="highlight`)
-
-	var attributes []ast.Attribute
-	if ctx.Attributes() != nil {
-		attributes = ctx.Attributes().All()
-	}
-
-	if attributes != nil {
-		class, found := ctx.Attributes().GetString("class")
-		if found {
-			w.WriteString(" ")
-			w.Write(util.EscapeHTML(class.([]byte)))
-
-		}
-		_, _ = w.WriteString("\"")
-		renderAttributes(w, true, attributes...)
-	} else {
-		_, _ = w.WriteString("\"")
-	}
-
-	w.WriteString(">")
-}
-
-func writeDivEnd(w util.BufWriter) {
-	w.WriteString("</div>")
-}
diff --git a/markup/goldmark/internal/render/context.go b/markup/goldmark/internal/render/context.go
new file mode 100644
index 00000000000..b18983ef3b5
--- /dev/null
+++ b/markup/goldmark/internal/render/context.go
@@ -0,0 +1,81 @@
+// Copyright 2022 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package render
+
+import (
+	"bytes"
+	"math/bits"
+
+	"github.com/gohugoio/hugo/identity"
+	"github.com/gohugoio/hugo/markup/converter"
+)
+
+type BufWriter struct {
+	*bytes.Buffer
+}
+
+const maxInt = 1<<(bits.UintSize-1) - 1
+
+func (b *BufWriter) Available() int {
+	return maxInt
+}
+
+func (b *BufWriter) Buffered() int {
+	return b.Len()
+}
+
+func (b *BufWriter) Flush() error {
+	return nil
+}
+
+type Context struct {
+	*BufWriter
+	positions []int
+	ContextData
+}
+
+func (ctx *Context) PushPos(n int) {
+	ctx.positions = append(ctx.positions, n)
+}
+
+func (ctx *Context) PopPos() int {
+	i := len(ctx.positions) - 1
+	p := ctx.positions[i]
+	ctx.positions = ctx.positions[:i]
+	return p
+}
+
+type ContextData interface {
+	RenderContext() converter.RenderContext
+	DocumentContext() converter.DocumentContext
+	AddIdentity(id identity.Provider)
+}
+
+type RenderContextDataHolder struct {
+	Rctx converter.RenderContext
+	Dctx converter.DocumentContext
+	IDs  identity.Manager
+}
+
+func (ctx *RenderContextDataHolder) RenderContext() converter.RenderContext {
+	return ctx.Rctx
+}
+
+func (ctx *RenderContextDataHolder) DocumentContext() converter.DocumentContext {
+	return ctx.Dctx
+}
+
+func (ctx *RenderContextDataHolder) AddIdentity(id identity.Provider) {
+	ctx.IDs.Add(id)
+}
diff --git a/markup/goldmark/render_hooks.go b/markup/goldmark/render_hooks.go
index 1862c212543..d5e35380a11 100644
--- a/markup/goldmark/render_hooks.go
+++ b/markup/goldmark/render_hooks.go
@@ -16,11 +16,10 @@ package goldmark
 import (
 	"bytes"
 	"strings"
-	"sync"
-
-	"github.com/spf13/cast"
 
 	"github.com/gohugoio/hugo/markup/converter/hooks"
+	"github.com/gohugoio/hugo/markup/goldmark/internal/render"
+	"github.com/gohugoio/hugo/markup/internal/attributes"
 
 	"github.com/yuin/goldmark"
 	"github.com/yuin/goldmark/ast"
@@ -44,28 +43,6 @@ func newLinks() goldmark.Extender {
 	return &links{}
 }
 
-type attributesHolder struct {
-	// What we get from Goldmark.
-	astAttributes []ast.Attribute
-
-	// What we send to the the render hooks.
-	attributesInit sync.Once
-	attributes     map[string]string
-}
-
-func (a *attributesHolder) Attributes() map[string]string {
-	a.attributesInit.Do(func() {
-		a.attributes = make(map[string]string)
-		for _, attr := range a.astAttributes {
-			if strings.HasPrefix(string(attr.Name), "on") {
-				continue
-			}
-			a.attributes[string(attr.Name)] = string(util.EscapeHTML(attr.Value.([]byte)))
-		}
-	})
-	return a.attributes
-}
-
 type linkContext struct {
 	page        interface{}
 	destination string
@@ -104,7 +81,7 @@ type headingContext struct {
 	anchor    string
 	text      string
 	plainText string
-	*attributesHolder
+	*attributes.AttributesHolder
 }
 
 func (ctx headingContext) Page() interface{} {
@@ -143,52 +120,17 @@ func (r *hookedRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer)
 	reg.Register(ast.KindHeading, r.renderHeading)
 }
 
-func (r *hookedRenderer) renderAttributesForNode(w util.BufWriter, node ast.Node) {
-	renderAttributes(w, false, node.Attributes()...)
-}
-
-// Attributes with special meaning that does not make sense to render in HTML.
-var attributeExcludes = map[string]bool{
-	"hl_lines":    true,
-	"hl_style":    true,
-	"linenos":     true,
-	"linenostart": true,
-}
-
-func renderAttributes(w util.BufWriter, skipClass bool, attributes ...ast.Attribute) {
-	for _, attr := range attributes {
-		if skipClass && bytes.Equal(attr.Name, []byte("class")) {
-			continue
-		}
-
-		a := strings.ToLower(string(attr.Name))
-		if attributeExcludes[a] || strings.HasPrefix(a, "on") {
-			continue
-		}
-
-		_, _ = w.WriteString(" ")
-		_, _ = w.Write(attr.Name)
-		_, _ = w.WriteString(`="`)
-
-		switch v := attr.Value.(type) {
-		case []byte:
-			_, _ = w.Write(util.EscapeHTML(v))
-		default:
-			w.WriteString(cast.ToString(v))
-		}
-
-		_ = w.WriteByte('"')
-	}
-}
-
 func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 	n := node.(*ast.Image)
-	var h hooks.Renderers
+	var lr hooks.LinkRenderer
 
-	ctx, ok := w.(*renderContext)
+	ctx, ok := w.(*render.Context)
 	if ok {
-		h = ctx.RenderContext().RenderHooks
-		ok = h.ImageRenderer != nil
+		h := ctx.RenderContext().GetRenderer(hooks.ImageRendererType, nil)
+		ok = h != nil
+		if ok {
+			lr = h.(hooks.LinkRenderer)
+		}
 	}
 
 	if !ok {
@@ -197,15 +139,15 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
 
 	if entering {
 		// Store the current pos so we can capture the rendered text.
-		ctx.pushPos(ctx.Buffer.Len())
+		ctx.PushPos(ctx.Buffer.Len())
 		return ast.WalkContinue, nil
 	}
 
-	pos := ctx.popPos()
+	pos := ctx.PopPos()
 	text := ctx.Buffer.Bytes()[pos:]
 	ctx.Buffer.Truncate(pos)
 
-	err := h.ImageRenderer.RenderLink(
+	err := lr.RenderLink(
 		w,
 		linkContext{
 			page:        ctx.DocumentContext().Document,
@@ -216,7 +158,7 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
 		},
 	)
 
-	ctx.AddIdentity(h.ImageRenderer)
+	ctx.AddIdentity(lr)
 
 	return ast.WalkContinue, err
 }
@@ -250,12 +192,15 @@ func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, nod
 
 func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 	n := node.(*ast.Link)
-	var h hooks.Renderers
+	var lr hooks.LinkRenderer
 
-	ctx, ok := w.(*renderContext)
+	ctx, ok := w.(*render.Context)
 	if ok {
-		h = ctx.RenderContext().RenderHooks
-		ok = h.LinkRenderer != nil
+		h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
+		ok = h != nil
+		if ok {
+			lr = h.(hooks.LinkRenderer)
+		}
 	}
 
 	if !ok {
@@ -264,15 +209,15 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
 
 	if entering {
 		// Store the current pos so we can capture the rendered text.
-		ctx.pushPos(ctx.Buffer.Len())
+		ctx.PushPos(ctx.Buffer.Len())
 		return ast.WalkContinue, nil
 	}
 
-	pos := ctx.popPos()
+	pos := ctx.PopPos()
 	text := ctx.Buffer.Bytes()[pos:]
 	ctx.Buffer.Truncate(pos)
 
-	err := h.LinkRenderer.RenderLink(
+	err := lr.RenderLink(
 		w,
 		linkContext{
 			page:        ctx.DocumentContext().Document,
@@ -286,7 +231,7 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
 	// TODO(bep) I have a working branch that fixes these rather confusing identity types,
 	// but for now it's important that it's not .GetIdentity() that's added here,
 	// to make sure we search the entire chain on changes.
-	ctx.AddIdentity(h.LinkRenderer)
+	ctx.AddIdentity(lr)
 
 	return ast.WalkContinue, err
 }
@@ -319,12 +264,15 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
 	}
 
 	n := node.(*ast.AutoLink)
-	var h hooks.Renderers
+	var lr hooks.LinkRenderer
 
-	ctx, ok := w.(*renderContext)
+	ctx, ok := w.(*render.Context)
 	if ok {
-		h = ctx.RenderContext().RenderHooks
-		ok = h.LinkRenderer != nil
+		h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
+		ok = h != nil
+		if ok {
+			lr = h.(hooks.LinkRenderer)
+		}
 	}
 
 	if !ok {
@@ -337,7 +285,7 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
 		url = "mailto:" + url
 	}
 
-	err := h.LinkRenderer.RenderLink(
+	err := lr.RenderLink(
 		w,
 		linkContext{
 			page:        ctx.DocumentContext().Document,
@@ -350,7 +298,7 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
 	// TODO(bep) I have a working branch that fixes these rather confusing identity types,
 	// but for now it's important that it's not .GetIdentity() that's added here,
 	// to make sure we search the entire chain on changes.
-	ctx.AddIdentity(h.LinkRenderer)
+	ctx.AddIdentity(lr)
 
 	return ast.WalkContinue, err
 }
@@ -383,12 +331,15 @@ func (r *hookedRenderer) renderAutoLinkDefault(w util.BufWriter, source []byte,
 
 func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 	n := node.(*ast.Heading)
-	var h hooks.Renderers
+	var hr hooks.HeadingRenderer
 
-	ctx, ok := w.(*renderContext)
+	ctx, ok := w.(*render.Context)
 	if ok {
-		h = ctx.RenderContext().RenderHooks
-		ok = h.HeadingRenderer != nil
+		h := ctx.RenderContext().GetRenderer(hooks.HeadingRendererType, nil)
+		ok = h != nil
+		if ok {
+			hr = h.(hooks.HeadingRenderer)
+		}
 	}
 
 	if !ok {
@@ -397,11 +348,11 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
 
 	if entering {
 		// Store the current pos so we can capture the rendered text.
-		ctx.pushPos(ctx.Buffer.Len())
+		ctx.PushPos(ctx.Buffer.Len())
 		return ast.WalkContinue, nil
 	}
 
-	pos := ctx.popPos()
+	pos := ctx.PopPos()
 	text := ctx.Buffer.Bytes()[pos:]
 	ctx.Buffer.Truncate(pos)
 	// All ast.Heading nodes are guaranteed to have an attribute called "id"
@@ -409,7 +360,7 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
 	anchori, _ := n.AttributeString("id")
 	anchor := anchori.([]byte)
 
-	err := h.HeadingRenderer.RenderHeading(
+	err := hr.RenderHeading(
 		w,
 		headingContext{
 			page:             ctx.DocumentContext().Document,
@@ -417,11 +368,11 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
 			anchor:           string(anchor),
 			text:             string(text),
 			plainText:        string(n.Text(source)),
-			attributesHolder: &attributesHolder{astAttributes: n.Attributes()},
+			AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
 		},
 	)
 
-	ctx.AddIdentity(h.HeadingRenderer)
+	ctx.AddIdentity(hr)
 
 	return ast.WalkContinue, err
 }
@@ -432,7 +383,7 @@ func (r *hookedRenderer) renderHeadingDefault(w util.BufWriter, source []byte, n
 		_, _ = w.WriteString("<h")
 		_ = w.WriteByte("0123456"[n.Level])
 		if n.Attributes() != nil {
-			r.renderAttributesForNode(w, node)
+			attributes.RenderASTAttributes(w, node.Attributes()...)
 		}
 		_ = w.WriteByte('>')
 	} else {
diff --git a/markup/highlight/config.go b/markup/highlight/config.go
index 1dc1e28e247..86ac02c3d99 100644
--- a/markup/highlight/config.go
+++ b/markup/highlight/config.go
@@ -20,6 +20,7 @@ import (
 	"strings"
 
 	"github.com/alecthomas/chroma/formatters/html"
+	"github.com/spf13/cast"
 
 	"github.com/gohugoio/hugo/config"
 
@@ -46,6 +47,9 @@ type Config struct {
 	// Use inline CSS styles.
 	NoClasses bool
 
+	// No highlighting.
+	NoHl bool
+
 	// When set, line numbers will be printed.
 	LineNos            bool
 	LineNumbersInTable bool
@@ -60,6 +64,9 @@ type Config struct {
 	// A space separated list of line numbers, e.g. “3-8 10-20”.
 	Hl_Lines string
 
+	// A parsed and ready to use list of line ranges.
+	HL_lines_parsed [][2]int
+
 	// TabWidth sets the number of characters for a tab. Defaults to 4.
 	TabWidth int
 
@@ -80,9 +87,19 @@ func (cfg Config) ToHTMLOptions() []html.Option {
 		html.LinkableLineNumbers(cfg.AnchorLineNos, lineAnchors),
 	}
 
-	if cfg.Hl_Lines != "" {
-		ranges, err := hlLinesToRanges(cfg.LineNoStart, cfg.Hl_Lines)
-		if err == nil {
+	if cfg.Hl_Lines != "" || cfg.HL_lines_parsed != nil {
+		var ranges [][2]int
+		if cfg.HL_lines_parsed != nil {
+			ranges = cfg.HL_lines_parsed
+		} else {
+			var err error
+			ranges, err = hlLinesToRanges(cfg.LineNoStart, cfg.Hl_Lines)
+			if err != nil {
+				ranges = nil
+			}
+		}
+
+		if ranges != nil {
 			options = append(options, html.HighlightLines(ranges))
 		}
 	}
@@ -90,14 +107,32 @@ func (cfg Config) ToHTMLOptions() []html.Option {
 	return options
 }
 
+func applyOptions(opts interface{}, cfg *Config) error {
+	if opts == nil {
+		return nil
+	}
+	switch vv := opts.(type) {
+	case map[string]interface{}:
+		return applyOptionsFromMap(vv, cfg)
+	case string:
+		return applyOptionsFromString(vv, cfg)
+	}
+	return nil
+}
+
 func applyOptionsFromString(opts string, cfg *Config) error {
-	optsm, err := parseOptions(opts)
+	optsm, err := parseHightlightOptions(opts)
 	if err != nil {
 		return err
 	}
 	return mapstructure.WeakDecode(optsm, cfg)
 }
 
+func applyOptionsFromMap(optsm map[string]interface{}, cfg *Config) error {
+	normalizeHighlightOptions(optsm)
+	return mapstructure.WeakDecode(optsm, cfg)
+}
+
 // ApplyLegacyConfig applies legacy config from back when we had
 // Pygments.
 func ApplyLegacyConfig(cfg config.Provider, conf *Config) error {
@@ -128,7 +163,7 @@ func ApplyLegacyConfig(cfg config.Provider, conf *Config) error {
 	return nil
 }
 
-func parseOptions(in string) (map[string]interface{}, error) {
+func parseHightlightOptions(in string) (map[string]interface{}, error) {
 	in = strings.Trim(in, " ")
 	opts := make(map[string]interface{})
 
@@ -142,19 +177,57 @@ func parseOptions(in string) (map[string]interface{}, error) {
 		if len(keyVal) != 2 {
 			return opts, fmt.Errorf("invalid Highlight option: %s", key)
 		}
-		if key == "linenos" {
-			opts[key] = keyVal[1] != "false"
-			if keyVal[1] == "table" || keyVal[1] == "inline" {
-				opts["lineNumbersInTable"] = keyVal[1] == "table"
-			}
-		} else {
-			opts[key] = keyVal[1]
-		}
+		opts[key] = keyVal[1]
+
 	}
 
+	normalizeHighlightOptions(opts)
+
 	return opts, nil
 }
 
+func normalizeHighlightOptions(m map[string]interface{}) {
+	if m == nil {
+		return
+	}
+
+	const (
+		lineNosKey    = "linenos"
+		hlLinesKey    = "hl_lines"
+		linosStartKey = "linenostart"
+		noHlKey       = "nohl"
+	)
+
+	baseLineNumber := 1
+	if v, ok := m[linosStartKey]; ok {
+		baseLineNumber = cast.ToInt(v)
+	}
+
+	for k, v := range m {
+		switch k {
+		case noHlKey:
+			m[noHlKey] = cast.ToBool(v)
+		case lineNosKey:
+			if v == "table" || v == "inline" {
+				m["lineNumbersInTable"] = v == "table"
+			}
+			if vs, ok := v.(string); ok {
+				m[k] = vs != "false"
+			}
+
+		case hlLinesKey:
+			if hlRanges, ok := v.([][2]int); ok {
+				for i := range hlRanges {
+					hlRanges[i][0] += baseLineNumber
+					hlRanges[i][1] += baseLineNumber
+				}
+				delete(m, k)
+				m[k+"_parsed"] = hlRanges
+			}
+		}
+	}
+}
+
 // startLine compensates for https://github.com/alecthomas/chroma/issues/30
 func hlLinesToRanges(startLine int, s string) ([][2]int, error) {
 	var ranges [][2]int
diff --git a/markup/highlight/highlight.go b/markup/highlight/highlight.go
index 319426241d5..e9cbeb3c991 100644
--- a/markup/highlight/highlight.go
+++ b/markup/highlight/highlight.go
@@ -16,47 +16,155 @@ package highlight
 import (
 	"fmt"
 	gohtml "html"
+	"html/template"
 	"io"
+	"strconv"
 	"strings"
 
 	"github.com/alecthomas/chroma"
 	"github.com/alecthomas/chroma/formatters/html"
 	"github.com/alecthomas/chroma/lexers"
 	"github.com/alecthomas/chroma/styles"
-	hl "github.com/yuin/goldmark-highlighting"
+	"github.com/gohugoio/hugo/common/hugio"
+	"github.com/gohugoio/hugo/identity"
+	"github.com/gohugoio/hugo/markup/converter/hooks"
+	"github.com/gohugoio/hugo/markup/internal/attributes"
 )
 
+// Markdown attributes used by the Chroma hightlighter.
+var chromaHightlightProcessingAttributes = map[string]bool{
+	"anchorLineNos":      true,
+	"guessSyntax":        true,
+	"hl_Lines":           true,
+	"lineAnchors":        true,
+	"lineNos":            true,
+	"lineNoStart":        true,
+	"lineNumbersInTable": true,
+	"noClasses":          true,
+	"style":              true,
+	"tabWidth":           true,
+}
+
+func init() {
+	for k, v := range chromaHightlightProcessingAttributes {
+		chromaHightlightProcessingAttributes[strings.ToLower(k)] = v
+	}
+}
+
 func New(cfg Config) Highlighter {
-	return Highlighter{
+	return chromaHighlighter{
 		cfg: cfg,
 	}
 }
 
-type Highlighter struct {
+type Highlighter interface {
+	Highlight(code, lang string, opts interface{}) (string, error)
+	HighlightCodeBlock(ctx hooks.CodeblockContext, opts interface{}) (HightlightResult, error)
+	hooks.CodeBlockRenderer
+}
+
+type chromaHighlighter struct {
 	cfg Config
 }
 
-func (h Highlighter) Highlight(code, lang, optsStr string) (string, error) {
-	if optsStr == "" {
-		return highlight(code, lang, h.cfg)
+func (h chromaHighlighter) Highlight(code, lang string, opts interface{}) (string, error) {
+	cfg := h.cfg
+	if err := applyOptions(opts, &cfg); err != nil {
+		return "", err
 	}
+	var b strings.Builder
 
-	cfg := h.cfg
-	if err := applyOptionsFromString(optsStr, &cfg); err != nil {
+	if err := highlight(&b, code, lang, nil, cfg); err != nil {
 		return "", err
 	}
 
-	return highlight(code, lang, cfg)
+	return b.String(), nil
 }
 
-func highlight(code, lang string, cfg Config) (string, error) {
-	w := &strings.Builder{}
+func (h chromaHighlighter) HighlightCodeBlock(ctx hooks.CodeblockContext, opts interface{}) (HightlightResult, error) {
+	cfg := h.cfg
+
+	var b strings.Builder
+
+	attributes := ctx.(hooks.AttributesOptionsSliceProvider).AttributesSlice()
+	options := ctx.Options()
+
+	if err := applyOptionsFromMap(options, &cfg); err != nil {
+		return HightlightResult{}, err
+	}
+
+	// Apply these last so the user can override them.
+	if err := applyOptions(opts, &cfg); err != nil {
+		return HightlightResult{}, err
+	}
+
+	err := highlight(&b, ctx.Code(), ctx.Lang(), attributes, cfg)
+	if err != nil {
+		return HightlightResult{}, err
+	}
+
+	return HightlightResult{
+		Body: template.HTML(b.String()),
+	}, nil
+}
+
+func (h chromaHighlighter) RenderCodeblock(w hugio.FlexiWriter, ctx hooks.CodeblockContext) error {
+	cfg := h.cfg
+	attributes := ctx.(hooks.AttributesOptionsSliceProvider).AttributesSlice()
+
+	if err := applyOptionsFromMap(ctx.Options(), &cfg); err != nil {
+		return err
+	}
+
+	return highlight(w, ctx.Code(), ctx.Lang(), attributes, cfg)
+}
+
+var id = identity.NewPathIdentity("chroma", "highlight")
+
+func (h chromaHighlighter) GetIdentity() identity.Identity {
+	return id
+}
+
+type HightlightResult struct {
+	Body template.HTML
+}
+
+func (h HightlightResult) Highlighted() template.HTML {
+	return h.Body
+}
+
+func (h chromaHighlighter) toHighlightOptionsAttributes(ctx hooks.CodeblockContext) (map[string]interface{}, map[string]interface{}) {
+	attributes := ctx.Attributes()
+	if attributes == nil || len(attributes) == 0 {
+		return nil, nil
+	}
+
+	options := make(map[string]interface{})
+	attrs := make(map[string]interface{})
+
+	for k, v := range attributes {
+		klow := strings.ToLower(k)
+		if chromaHightlightProcessingAttributes[klow] {
+			options[klow] = v
+		} else {
+			attrs[k] = v
+		}
+	}
+	const lineanchorsKey = "lineanchors"
+	if _, found := options[lineanchorsKey]; !found {
+		// Set it to the ordinal.
+		options[lineanchorsKey] = strconv.Itoa(ctx.Ordinal())
+	}
+	return options, attrs
+}
+
+func highlight(w hugio.FlexiWriter, code, lang string, attributes []attributes.Attribute, cfg Config) error {
 	var lexer chroma.Lexer
 	if lang != "" {
 		lexer = lexers.Get(lang)
 	}
 
-	if lexer == nil && cfg.GuessSyntax {
+	if lexer == nil && (cfg.GuessSyntax && !cfg.NoHl) {
 		lexer = lexers.Analyse(code)
 		if lexer == nil {
 			lexer = lexers.Fallback
@@ -69,7 +177,7 @@ func highlight(code, lang string, cfg Config) (string, error) {
 		fmt.Fprint(w, wrapper.Start(true, ""))
 		fmt.Fprint(w, gohtml.EscapeString(code))
 		fmt.Fprint(w, wrapper.End(true))
-		return w.String(), nil
+		return nil
 	}
 
 	style := styles.Get(cfg.Style)
@@ -80,7 +188,7 @@ func highlight(code, lang string, cfg Config) (string, error) {
 
 	iterator, err := lexer.Tokenise(nil, code)
 	if err != nil {
-		return "", err
+		return err
 	}
 
 	options := cfg.ToHTMLOptions()
@@ -88,25 +196,13 @@ func highlight(code, lang string, cfg Config) (string, error) {
 
 	formatter := html.New(options...)
 
-	fmt.Fprint(w, `<div class="highlight">`)
+	writeDivStart(w, attributes)
 	if err := formatter.Format(w, style, iterator); err != nil {
-		return "", err
+		return err
 	}
-	fmt.Fprint(w, `</div>`)
-
-	return w.String(), nil
-}
+	writeDivEnd(w)
 
-func GetCodeBlockOptions() func(ctx hl.CodeBlockContext) []html.Option {
-	return func(ctx hl.CodeBlockContext) []html.Option {
-		var language string
-		if l, ok := ctx.Language(); ok {
-			language = string(l)
-		}
-		return []html.Option{
-			getHtmlPreWrapper(language),
-		}
-	}
+	return nil
 }
 
 func getPreWrapper(language string) preWrapper {
@@ -150,3 +246,25 @@ func (p preWrapper) End(code bool) string {
 func WritePreEnd(w io.Writer) {
 	fmt.Fprint(w, preEnd)
 }
+
+func writeDivStart(w hugio.FlexiWriter, attrs []attributes.Attribute) {
+	w.WriteString(`<div class="highlight`)
+	if attrs != nil {
+		for _, attr := range attrs {
+			if attr.Name == "class" {
+				w.WriteString(" " + attr.ValueString())
+				break
+			}
+		}
+		_, _ = w.WriteString("\"")
+		attributes.RenderAttributes(w, true, attrs...)
+	} else {
+		_, _ = w.WriteString("\"")
+	}
+
+	w.WriteString(">")
+}
+
+func writeDivEnd(w hugio.FlexiWriter) {
+	w.WriteString("</div>")
+}
diff --git a/markup/internal/attributes/attributes.go b/markup/internal/attributes/attributes.go
new file mode 100644
index 00000000000..1cce7edd1e0
--- /dev/null
+++ b/markup/internal/attributes/attributes.go
@@ -0,0 +1,219 @@
+// Copyright 2022 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package attributes
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+	"sync"
+
+	"github.com/gohugoio/hugo/common/hugio"
+	"github.com/spf13/cast"
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/util"
+)
+
+// Markdown attributes used as options by the Chroma highlighter.
+var chromaHightlightProcessingAttributes = map[string]bool{
+	"anchorLineNos":      true,
+	"guessSyntax":        true,
+	"hl_Lines":           true,
+	"lineAnchors":        true,
+	"lineNos":            true,
+	"lineNoStart":        true,
+	"lineNumbersInTable": true,
+	"noClasses":          true,
+	"nohl":               true,
+	"style":              true,
+	"tabWidth":           true,
+}
+
+func init() {
+	for k, v := range chromaHightlightProcessingAttributes {
+		chromaHightlightProcessingAttributes[strings.ToLower(k)] = v
+	}
+}
+
+type AttributesOwnerType int
+
+const (
+	AttributesOwnerGeneral AttributesOwnerType = iota
+	AttributesOwnerCodeBlock
+)
+
+func New(astAttributes []ast.Attribute, ownerType AttributesOwnerType) *AttributesHolder {
+	var (
+		attrs []Attribute
+		opts  []Attribute
+	)
+	for _, v := range astAttributes {
+		nameLower := strings.ToLower(string(v.Name))
+		if strings.HasPrefix(string(nameLower), "on") {
+			continue
+		}
+		var vv interface{}
+		switch vvv := v.Value.(type) {
+		case bool, float64:
+			vv = vvv
+		case []interface{}:
+			// Highlight line number hlRanges.
+			var hlRanges [][2]int
+			for _, l := range vvv {
+				if ln, ok := l.(float64); ok {
+					hlRanges = append(hlRanges, [2]int{int(ln) - 1, int(ln) - 1})
+				} else if rng, ok := l.([]uint8); ok {
+					slices := strings.Split(string([]byte(rng)), "-")
+					lhs, err := strconv.Atoi(slices[0])
+					if err != nil {
+						continue
+					}
+					rhs := lhs
+					if len(slices) > 1 {
+						rhs, err = strconv.Atoi(slices[1])
+						if err != nil {
+							continue
+						}
+					}
+					hlRanges = append(hlRanges, [2]int{lhs - 1, rhs - 1})
+				}
+			}
+			vv = hlRanges
+		case []byte:
+			// Note that we don't do any HTML escaping here.
+			// We used to do that, but that changed in #9558.
+			// Noww it's up to the templates to decide.
+			vv = string(vvv)
+		default:
+			panic(fmt.Sprintf("not implemented: %T", vvv))
+		}
+
+		if ownerType == AttributesOwnerCodeBlock && chromaHightlightProcessingAttributes[nameLower] {
+			attr := Attribute{Name: string(v.Name), Value: vv}
+			opts = append(opts, attr)
+		} else {
+			attr := Attribute{Name: nameLower, Value: vv}
+			attrs = append(attrs, attr)
+		}
+
+	}
+
+	return &AttributesHolder{
+		attributes: attrs,
+		options:    opts,
+	}
+}
+
+type Attribute struct {
+	Name  string
+	Value interface{}
+}
+
+func (a Attribute) ValueString() string {
+	return cast.ToString(a.Value)
+}
+
+type AttributesHolder struct {
+	// What we get from Goldmark.
+	attributes []Attribute
+
+	// Attributes considered to be an option (code blocks)
+	options []Attribute
+
+	// What we send to the the render hooks.
+	attributesMapInit sync.Once
+	attributesMap     map[string]interface{}
+	optionsMapInit    sync.Once
+	optionsMap        map[string]interface{}
+}
+
+type Attributes map[string]interface{}
+
+func (a *AttributesHolder) Attributes() map[string]interface{} {
+	a.attributesMapInit.Do(func() {
+		a.attributesMap = make(map[string]interface{})
+		for _, v := range a.attributes {
+			a.attributesMap[v.Name] = v.Value
+		}
+	})
+	return a.attributesMap
+}
+
+func (a *AttributesHolder) Options() map[string]interface{} {
+	a.optionsMapInit.Do(func() {
+		a.optionsMap = make(map[string]interface{})
+		for _, v := range a.options {
+			a.optionsMap[v.Name] = v.Value
+		}
+	})
+	return a.optionsMap
+}
+
+func (a *AttributesHolder) AttributesSlice() []Attribute {
+	return a.attributes
+}
+
+func (a *AttributesHolder) OptionsSlice() []Attribute {
+	return a.options
+}
+
+// RenderASTAttributes writes the AST attributes to the given as attributes to an HTML element.
+// This is used by the default HTML renderers, e.g. for headings etc. where no hook template could be found.
+// This performs HTML esacaping of string attributes.
+func RenderASTAttributes(w hugio.FlexiWriter, attributes ...ast.Attribute) {
+	for _, attr := range attributes {
+
+		a := strings.ToLower(string(attr.Name))
+		if strings.HasPrefix(a, "on") {
+			continue
+		}
+
+		_, _ = w.WriteString(" ")
+		_, _ = w.Write(attr.Name)
+		_, _ = w.WriteString(`="`)
+
+		switch v := attr.Value.(type) {
+		case []byte:
+			_, _ = w.Write(util.EscapeHTML(v))
+		default:
+			w.WriteString(cast.ToString(v))
+		}
+
+		_ = w.WriteByte('"')
+	}
+}
+
+// Render writes the attributes to the given as attributes to an HTML element.
+// This is used for the default codeblock renderering.
+// This performs HTML esacaping of string attributes.
+func RenderAttributes(w hugio.FlexiWriter, skipClass bool, attributes ...Attribute) {
+	for _, attr := range attributes {
+		a := strings.ToLower(string(attr.Name))
+		if skipClass && a == "class" {
+			continue
+		}
+		_, _ = w.WriteString(" ")
+		_, _ = w.WriteString(attr.Name)
+		_, _ = w.WriteString(`="`)
+
+		switch v := attr.Value.(type) {
+		case []byte:
+			_, _ = w.Write(util.EscapeHTML(v))
+		default:
+			w.WriteString(cast.ToString(v))
+		}
+
+		_ = w.WriteByte('"')
+	}
+}
diff --git a/markup/markup.go b/markup/markup.go
index 287db7369ec..13e5f30424b 100644
--- a/markup/markup.go
+++ b/markup/markup.go
@@ -39,11 +39,8 @@ func NewConverterProvider(cfg converter.ProviderConfig) (ConverterProvider, erro
 		return nil, err
 	}
 
-	if cfg.Highlight == nil {
-		h := highlight.New(markupConfig.Highlight)
-		cfg.Highlight = func(code, lang, optsStr string) (string, error) {
-			return h.Highlight(code, lang, optsStr)
-		}
+	if cfg.Highlighter == nil {
+		cfg.Highlighter = highlight.New(markupConfig.Highlight)
 	}
 
 	cfg.MarkupConfig = markupConfig
@@ -95,7 +92,7 @@ type ConverterProvider interface {
 	Get(name string) converter.Provider
 	// Default() converter.Provider
 	GetMarkupConfig() markup_config.Config
-	Highlight(code, lang, optsStr string) (string, error)
+	GetHighlighter() highlight.Highlighter
 }
 
 type converterRegistry struct {
@@ -112,8 +109,8 @@ func (r *converterRegistry) Get(name string) converter.Provider {
 	return r.converters[strings.ToLower(name)]
 }
 
-func (r *converterRegistry) Highlight(code, lang, optsStr string) (string, error) {
-	return r.config.Highlight(code, lang, optsStr)
+func (r *converterRegistry) GetHighlighter() highlight.Highlighter {
+	return r.config.Highlighter
 }
 
 func (r *converterRegistry) GetMarkupConfig() markup_config.Config {
diff --git a/markup/org/convert.go b/markup/org/convert.go
index 34043e18d2c..603ec8f19cf 100644
--- a/markup/org/convert.go
+++ b/markup/org/convert.go
@@ -27,8 +27,7 @@ import (
 // Provider is the package entry point.
 var Provider converter.ProviderProvider = provide{}
 
-type provide struct {
-}
+type provide struct{}
 
 func (p provide) New(cfg converter.ProviderConfig) (converter.Provider, error) {
 	return converter.NewProvider("org", func(ctx converter.DocumentContext) (converter.Converter, error) {
diff --git a/output/layout.go b/output/layout.go
index 91c7cc6523a..dcbdf461ac3 100644
--- a/output/layout.go
+++ b/output/layout.go
@@ -31,9 +31,15 @@ var reservedSections = map[string]bool{
 type LayoutDescriptor struct {
 	Type    string
 	Section string
-	Kind    string
-	Lang    string
-	Layout  string
+
+	// E.g. "page", but also used for the _markup render kinds, e.g. "render-image".
+	Kind string
+
+	// Comma-separated list of kind variants, e.g. "go,json" as variants which would find "render-codeblock-go.html"
+	KindVariants string
+
+	Lang   string
+	Layout string
 	// LayoutOverride indicates what we should only look for the above layout.
 	LayoutOverride bool
 
@@ -139,6 +145,12 @@ func resolvePageTemplate(d LayoutDescriptor, f Format) []string {
 	}
 
 	if d.RenderingHook {
+		if d.KindVariants != "" {
+			// Add the more specific variants first.
+			for _, variant := range strings.Split(d.KindVariants, ",") {
+				b.addLayoutVariations(d.Kind + "-" + variant)
+			}
+		}
 		b.addLayoutVariations(d.Kind)
 		b.addSectionType()
 	}
diff --git a/resources/page/site.go b/resources/page/site.go
index 9728df691d6..724f23ad73d 100644
--- a/resources/page/site.go
+++ b/resources/page/site.go
@@ -32,6 +32,7 @@ type Site interface {
 	Language() *langs.Language
 	RegularPages() Pages
 	Pages() Pages
+	Home() Page
 	IsServer() bool
 	ServerPort() int
 	Title() string
@@ -89,6 +90,10 @@ func (t testSite) Language() *langs.Language {
 	return t.l
 }
 
+func (t testSite) Home() Page {
+	return nil
+}
+
 func (t testSite) Pages() Pages {
 	return nil
 }
diff --git a/tpl/compare/init.go b/tpl/compare/init.go
index 9aa533f5561..f423f615e06 100644
--- a/tpl/compare/init.go
+++ b/tpl/compare/init.go
@@ -40,14 +40,14 @@ func init() {
 		ns.AddMethodMapping(ctx.Eq,
 			[]string{"eq"},
 			[][2]string{
-				{`{{ if eq .Section "blog" }}current{{ end }}`, `current`},
+				{`{{ if eq .Section "blog" }}current-section{{ end }}`, `current-section`},
 			},
 		)
 
 		ns.AddMethodMapping(ctx.Ge,
 			[]string{"ge"},
 			[][2]string{
-				{`{{ if ge .Hugo.Version "0.36" }}Reasonable new Hugo version!{{ end }}`, `Reasonable new Hugo version!`},
+				{`{{ if ge hugo.Version "0.80" }}Reasonable new Hugo version!{{ end }}`, `Reasonable new Hugo version!`},
 			},
 		)
 
diff --git a/tpl/diagrams/diagrams.go b/tpl/diagrams/diagrams.go
new file mode 100644
index 00000000000..1bdbc2a02fd
--- /dev/null
+++ b/tpl/diagrams/diagrams.go
@@ -0,0 +1,73 @@
+// Copyright 2022 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package diagrams
+
+import (
+	"bytes"
+	"html/template"
+	"io"
+	"strings"
+
+	"github.com/bep/goat"
+	"github.com/gohugoio/hugo/deps"
+	"github.com/spf13/cast"
+)
+
+type SVGDiagram interface {
+	Body() template.HTML
+	SVG() template.HTML
+	Width() int
+	Height() int
+}
+
+type goatDiagram struct {
+	d goat.SVG
+}
+
+func (d goatDiagram) Body() template.HTML {
+	return template.HTML(d.d.Body)
+}
+
+func (d goatDiagram) SVG() template.HTML {
+	return template.HTML(d.d.String())
+}
+
+func (d goatDiagram) Width() int {
+	return d.d.Width
+}
+
+func (d goatDiagram) Height() int {
+	return d.d.Height
+}
+
+type Diagrams struct {
+	d *deps.Deps
+}
+
+func (d *Diagrams) Goat(v interface{}) SVGDiagram {
+	var r io.Reader
+
+	switch vv := v.(type) {
+	case io.Reader:
+		r = vv
+	case []byte:
+		r = bytes.NewReader(vv)
+	default:
+		r = strings.NewReader(cast.ToString(v))
+	}
+
+	return goatDiagram{
+		d: goat.BuildSVG(r),
+	}
+}
diff --git a/tpl/os/init_test.go b/tpl/diagrams/init.go
similarity index 55%
rename from tpl/os/init_test.go
rename to tpl/diagrams/init.go
index 5d756bab2df..1a55788376b 100644
--- a/tpl/os/init_test.go
+++ b/tpl/diagrams/init.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
+// Copyright 2022 The Hugo Authors. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -11,32 +11,28 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package os
+package diagrams
 
 import (
-	"testing"
-
-	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/htesting/hqt"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
 
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
+const name = "diagrams"
+
+func init() {
+	f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
+		ctx := &Diagrams{
+			d: d,
+		}
 
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{})
-		if ns.Name == name {
-			found = true
-			break
+		ns := &internal.TemplateFuncsNamespace{
+			Name:    name,
+			Context: func(args ...interface{}) (interface{}, error) { return ctx, nil },
 		}
+
+		return ns
 	}
 
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
+	internal.AddTemplateFuncsNamespace(f)
 }
diff --git a/tpl/os/os.go b/tpl/os/os.go
index 43c42f5e197..8b195a52775 100644
--- a/tpl/os/os.go
+++ b/tpl/os/os.go
@@ -19,6 +19,7 @@ import (
 	"errors"
 	"fmt"
 	_os "os"
+	"path/filepath"
 
 	"github.com/gohugoio/hugo/deps"
 	"github.com/spf13/afero"
@@ -27,17 +28,9 @@ import (
 
 // New returns a new instance of the os-namespaced template functions.
 func New(d *deps.Deps) *Namespace {
-	var rfs afero.Fs
-	if d.Fs != nil {
-		rfs = d.Fs.WorkingDir
-		if d.PathSpec != nil && d.PathSpec.BaseFs != nil {
-			rfs = afero.NewReadOnlyFs(afero.NewCopyOnWriteFs(d.PathSpec.BaseFs.Content.Fs, d.Fs.WorkingDir))
-		}
-
-	}
-
 	return &Namespace{
-		readFileFs: rfs,
+		readFileFs: afero.NewReadOnlyFs(afero.NewCopyOnWriteFs(d.PathSpec.BaseFs.Content.Fs, d.PathSpec.BaseFs.Work)),
+		workFs:     d.PathSpec.BaseFs.Work,
 		deps:       d,
 	}
 }
@@ -45,6 +38,7 @@ func New(d *deps.Deps) *Namespace {
 // Namespace provides template functions for the "os" namespace.
 type Namespace struct {
 	readFileFs afero.Fs
+	workFs     afero.Fs
 	deps       *deps.Deps
 }
 
@@ -66,8 +60,9 @@ func (ns *Namespace) Getenv(key interface{}) (string, error) {
 // readFile reads the file named by filename in the given filesystem
 // and returns the contents as a string.
 func readFile(fs afero.Fs, filename string) (string, error) {
-	if filename == "" {
-		return "", errors.New("readFile needs a filename")
+	filename = filepath.Clean(filename)
+	if filename == "" || filename == "." || filename == string(_os.PathSeparator) {
+		return "", errors.New("invalid filename")
 	}
 
 	b, err := afero.ReadFile(fs, filename)
@@ -101,7 +96,7 @@ func (ns *Namespace) ReadDir(i interface{}) ([]_os.FileInfo, error) {
 		return nil, err
 	}
 
-	list, err := afero.ReadDir(ns.deps.Fs.WorkingDir, path)
+	list, err := afero.ReadDir(ns.workFs, path)
 	if err != nil {
 		return nil, fmt.Errorf("failed to read directory %q: %s", path, err)
 	}
diff --git a/tpl/tplimpl/embedded/templates/_default/_markup/render-codeblock-goat.html b/tpl/tplimpl/embedded/templates/_default/_markup/render-codeblock-goat.html
new file mode 100644
index 00000000000..7c2b99f3f21
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_default/_markup/render-codeblock-goat.html
@@ -0,0 +1,1 @@
+adf
diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go
index 44b48640461..70671927812 100644
--- a/tpl/tplimpl/template.go
+++ b/tpl/tplimpl/template.go
@@ -281,15 +281,10 @@ func (t *templateExec) UnusedTemplates() []tpl.FileInfo {
 
 	for _, ts := range t.main.templates {
 		ti := ts.info
-		if strings.HasPrefix(ti.name, "_internal/") {
-			continue
-		}
-		if strings.HasPrefix(ti.name, "partials/inline/pagination") {
-			// TODO(bep) we need to fix this. These are internal partials, but
-			// they may also be defined in the project, which currently could
-			// lead to some false negatives.
+		if strings.HasPrefix(ti.name, "_internal/") || ti.realFilename == "" {
 			continue
 		}
+
 		if _, found := t.templateUsageTracker[ti.name]; !found {
 			unused = append(unused, ti)
 		}
@@ -740,6 +735,7 @@ func (t *templateHandler) extractIdentifiers(line string) []string {
 }
 
 //go:embed embedded/templates/*
+//go:embed embedded/templates/_default/*
 var embededTemplatesFs embed.FS
 
 func (t *templateHandler) loadEmbedded() error {
@@ -757,9 +753,19 @@ func (t *templateHandler) loadEmbedded() error {
 		// to write the templates to Go files.
 		templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n")))
 		name := strings.TrimPrefix(filepath.ToSlash(path), "embedded/templates/")
+		templateName := name
 
-		if err := t.AddTemplate(internalPathPrefix+name, templ); err != nil {
-			return err
+		// For the render hooks it does not make sense to preseve the
+		// double _indternal double book-keeping,
+		// just add it if its now provided by the user.
+		if !strings.Contains(path, "_default/_markup") {
+			templateName = internalPathPrefix + name
+		}
+
+		if _, found := t.Lookup(templateName); !found {
+			if err := t.AddTemplate(templateName, templ); err != nil {
+				return err
+			}
 		}
 
 		if aliases, found := embeddedTemplatesAliases[name]; found {
diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go
index 831b846d0c0..8692b9ee214 100644
--- a/tpl/tplimpl/template_funcs.go
+++ b/tpl/tplimpl/template_funcs.go
@@ -38,6 +38,7 @@ import (
 	_ "github.com/gohugoio/hugo/tpl/crypto"
 	_ "github.com/gohugoio/hugo/tpl/data"
 	_ "github.com/gohugoio/hugo/tpl/debug"
+	_ "github.com/gohugoio/hugo/tpl/diagrams"
 	_ "github.com/gohugoio/hugo/tpl/encoding"
 	_ "github.com/gohugoio/hugo/tpl/fmt"
 	_ "github.com/gohugoio/hugo/tpl/hugo"
diff --git a/tpl/transform/transform.go b/tpl/transform/transform.go
index 8ea91f234ca..dc7cc0342c0 100644
--- a/tpl/transform/transform.go
+++ b/tpl/transform/transform.go
@@ -19,6 +19,9 @@ import (
 	"html/template"
 
 	"github.com/gohugoio/hugo/cache/namedmemcache"
+	"github.com/gohugoio/hugo/common/herrors"
+	"github.com/gohugoio/hugo/markup/converter/hooks"
+	"github.com/gohugoio/hugo/markup/highlight"
 
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/helpers"
@@ -65,18 +68,28 @@ func (ns *Namespace) Highlight(s interface{}, lang string, opts ...interface{})
 		return "", err
 	}
 
-	sopts := ""
+	var optsv interface{}
 	if len(opts) > 0 {
-		sopts, err = cast.ToStringE(opts[0])
-		if err != nil {
-			return "", err
-		}
+		optsv = opts[0]
 	}
 
-	highlighted, _ := ns.deps.ContentSpec.Converters.Highlight(ss, lang, sopts)
+	hl := ns.deps.ContentSpec.Converters.GetHighlighter()
+	highlighted, _ := hl.Highlight(ss, lang, optsv)
 	return template.HTML(highlighted), nil
 }
 
+// HighlightCodeBlock highlights a code block on the form received in the codeblock render hooks.
+func (ns *Namespace) HighlightCodeBlock(ctx hooks.CodeblockContext, opts ...interface{}) (highlight.HightlightResult, error) {
+	var optsv interface{}
+	if len(opts) > 0 {
+		optsv = opts[0]
+	}
+
+	hl := ns.deps.ContentSpec.Converters.GetHighlighter()
+
+	return hl.HighlightCodeBlock(ctx, optsv)
+}
+
 // HTMLEscape returns a copy of s with reserved HTML characters escaped.
 func (ns *Namespace) HTMLEscape(s interface{}) (string, error) {
 	ss, err := cast.ToStringE(s)
@@ -100,20 +113,22 @@ func (ns *Namespace) HTMLUnescape(s interface{}) (string, error) {
 
 // Markdownify renders a given input from Markdown to HTML.
 func (ns *Namespace) Markdownify(s interface{}) (template.HTML, error) {
+	defer herrors.Recover()
 	ss, err := cast.ToStringE(s)
 	if err != nil {
 		return "", err
 	}
 
-	b, err := ns.deps.ContentSpec.RenderMarkdown([]byte(ss))
-	if err != nil {
-		return "", err
+	home := ns.deps.Site.Home()
+	if home == nil {
+		panic("home must not be nil")
 	}
+	sss, err := home.RenderString(ss)
 
 	// Strip if this is a short inline type of text.
-	b = ns.deps.ContentSpec.TrimShortHTML(b)
+	bb := ns.deps.ContentSpec.TrimShortHTML([]byte(sss))
 
-	return helpers.BytesToHTML(b), nil
+	return helpers.BytesToHTML(bb), nil
 }
 
 // Plainify returns a copy of s with all HTML tags removed.
@@ -125,3 +140,7 @@ func (ns *Namespace) Plainify(s interface{}) (string, error) {
 
 	return helpers.StripHTML(ss), nil
 }
+
+func (ns *Namespace) Reset() {
+	ns.cache.Clear()
+}


Test patch:
diff --git a/hugolib/content_render_hooks_test.go b/hugolib/content_render_hooks_test.go
index edfeaa82a81..33ebe1f41d9 100644
--- a/hugolib/content_render_hooks_test.go
+++ b/hugolib/content_render_hooks_test.go
@@ -231,8 +231,8 @@ SHORT3|
 	b.AssertFileContent("public/blog/p3/index.html", `PARTIAL3`)
 	// We may add type template support later, keep this for then. b.AssertFileContent("public/docs/docs1/index.html", `Link docs section: Docs 1|END`)
 	b.AssertFileContent("public/blog/p4/index.html", `<p>IMAGE: Cool Page With Image||/images/Dragster.jpg|Title: image title|Text: Drag Racing|END</p>`)
-	// The regular markdownify func currently gets regular links.
-	b.AssertFileContent("public/blog/p5/index.html", "Inner Link: <a href=\"https://www.google.com\" title=\"Google's Homepage\">Inner Link</a>\n</div>")
+	// markdownify
+	b.AssertFileContent("public/blog/p5/index.html", "Inner Link: |https://www.google.com|Title: Google's Homepage|Text: Inner Link|END")
 
 	b.AssertFileContent("public/blog/p6/index.html",
 		"Inner Inline: Inner Link: With RenderString|https://www.gohugo.io|Title: Hugo's Homepage|Text: Inner Link|END",
diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go
index 7ec7a150325..ed68783a165 100644
--- a/hugolib/integrationtest_builder.go
+++ b/hugolib/integrationtest_builder.go
@@ -125,7 +125,7 @@ func (s *IntegrationTestBuilder) AssertFileContent(filename string, matches ...s
 			if match == "" || strings.HasPrefix(match, "#") {
 				continue
 			}
-			s.Assert(content, qt.Contains, match, qt.Commentf(content))
+			s.Assert(content, qt.Contains, match, qt.Commentf(m))
 		}
 	}
 }
@@ -164,7 +164,7 @@ func (s *IntegrationTestBuilder) AssertRenderCountPage(count int) {
 func (s *IntegrationTestBuilder) Build() *IntegrationTestBuilder {
 	s.Helper()
 	_, err := s.BuildE()
-	if s.Cfg.Verbose {
+	if s.Cfg.Verbose || err != nil {
 		fmt.Println(s.logBuff.String())
 	}
 	s.Assert(err, qt.IsNil)
diff --git a/hugolib/language_content_dir_test.go b/hugolib/language_content_dir_test.go
index 117fdfb1431..9a7a78e7e3f 100644
--- a/hugolib/language_content_dir_test.go
+++ b/hugolib/language_content_dir_test.go
@@ -314,7 +314,7 @@ Content.
 	nnSect := nnSite.getPage(page.KindSection, "sect")
 	c.Assert(nnSect, qt.Not(qt.IsNil))
 	c.Assert(len(nnSect.Pages()), qt.Equals, 12)
-	nnHome, _ := nnSite.Info.Home()
+	nnHome := nnSite.Info.Home()
 	c.Assert(nnHome.RelPermalink(), qt.Equals, "/nn/")
 }
 
diff --git a/hugolib/page_test.go b/hugolib/page_test.go
index c281ad36c17..04ca696c8d6 100644
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -428,8 +428,7 @@ func testAllMarkdownEnginesForPages(t *testing.T,
 
 			assertFunc(t, e.ext, s.RegularPages())
 
-			home, err := s.Info.Home()
-			b.Assert(err, qt.IsNil)
+			home := s.Info.Home()
 			b.Assert(home, qt.Not(qt.IsNil))
 			b.Assert(home.File().Path(), qt.Equals, homePath)
 			b.Assert(content(home), qt.Contains, "Home Page Content")
@@ -1286,7 +1285,7 @@ func TestTranslationKey(t *testing.T) {
 
 	c.Assert(len(s.RegularPages()), qt.Equals, 2)
 
-	home, _ := s.Info.Home()
+	home := s.Info.Home()
 	c.Assert(home, qt.Not(qt.IsNil))
 	c.Assert(home.TranslationKey(), qt.Equals, "home")
 	c.Assert(s.RegularPages()[0].TranslationKey(), qt.Equals, "page/k1")
diff --git a/hugolib/pagebundler_test.go b/hugolib/pagebundler_test.go
index 1694b02ee8a..238c725bd86 100644
--- a/hugolib/pagebundler_test.go
+++ b/hugolib/pagebundler_test.go
@@ -150,7 +150,7 @@ func TestPageBundlerSiteRegular(t *testing.T) {
 						c.Assert(leafBundle1.Section(), qt.Equals, "b")
 						sectionB := s.getPage(page.KindSection, "b")
 						c.Assert(sectionB, qt.Not(qt.IsNil))
-						home, _ := s.Info.Home()
+						home := s.Info.Home()
 						c.Assert(home.BundleType(), qt.Equals, files.ContentClassBranch)
 
 						// This is a root bundle and should live in the "home section"
@@ -290,7 +290,7 @@ func TestPageBundlerSiteMultilingual(t *testing.T) {
 
 				c.Assert(len(s.RegularPages()), qt.Equals, 8)
 				c.Assert(len(s.Pages()), qt.Equals, 16)
-				//dumpPages(s.AllPages()...)
+				// dumpPages(s.AllPages()...)
 
 				c.Assert(len(s.AllPages()), qt.Equals, 31)
 
diff --git a/markup/goldmark/codeblocks/integration_test.go b/markup/goldmark/codeblocks/integration_test.go
new file mode 100644
index 00000000000..d662b39118e
--- /dev/null
+++ b/markup/goldmark/codeblocks/integration_test.go
@@ -0,0 +1,115 @@
+// Copyright 2022 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package codeblocks_test
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/gohugoio/hugo/hugolib"
+)
+
+func TestCodeblocks(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- config.toml --
+[markup]
+  [markup.highlight]
+    anchorLineNos = false
+    codeFences = true
+    guessSyntax = false
+    hl_Lines = ''
+    lineAnchors = ''
+    lineNoStart = 1
+    lineNos = false
+    lineNumbersInTable = true
+    noClasses = false
+    style = 'monokai'
+    tabWidth = 4
+-- layouts/_default/_markup/render-codeblock-goat.html --
+{{ $diagram := diagrams.Goat .Code }}
+Goat SVG:{{ substr $diagram.SVG 0 100 | safeHTML }}  }}|
+Goat Attribute: {{ .Attributes.width}}|
+-- layouts/_default/_markup/render-codeblock-go.html --
+Go Code: {{ .Code | safeHTML }}|
+Go Language: {{ .Lang }}|
+-- layouts/_default/single.html --
+{{ .Content }}
+-- content/p1.md --
+---
+title: "p1"
+---
+
+## Ascii Diagram
+
+CODE_FENCEgoat { width="600" }
+--->
+CODE_FENCE
+
+## Go Code
+
+CODE_FENCEgo
+fmt.Println("Hello, World!");
+CODE_FENCE
+
+## Golang Code
+
+CODE_FENCEgolang
+fmt.Println("Hello, Golang!");
+CODE_FENCE
+
+## Bash Code
+
+CODE_FENCEbash { linenos=inline,hl_lines=[2,"5-6"],linenostart=32 class=blue }
+echo "l1";
+echo "l2";
+echo "l3";
+echo "l4";
+echo "l5";
+echo "l6";
+echo "l7";
+echo "l8";
+CODE_FENCE
+`
+
+	files = strings.ReplaceAll(files, "CODE_FENCE", "```")
+
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			NeedsOsFS:   false,
+		},
+	).Build()
+
+	b.AssertFileContent("public/p1/index.html", `
+Goat SVG:<svg class='diagram'
+Goat Attribute: 600|
+
+Go Language: go|
+Go Code: fmt.Println("Hello, World!");
+
+Go Code: fmt.Println("Hello, Golang!");
+Go Language: golang|
+
+
+	`,
+		"Goat SVG:<svg class='diagram' xmlns='http://www.w3.org/2000/svg' version='1.1' height='25' width='40'",
+		"Goat Attribute: 600|",
+		"<h2 id=\"go-code\">Go Code</h2>\nGo Code: fmt.Println(\"Hello, World!\");\n|\nGo Language: go|",
+		"<h2 id=\"golang-code\">Golang Code</h2>\nGo Code: fmt.Println(\"Hello, Golang!\");\n|\nGo Language: golang|",
+		"<h2 id=\"bash-code\">Bash Code</h2>\n<div class=\"highlight blue\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"ln\">32</span><span class=\"cl\"><span class=\"nb\">echo</span> <span class=\"s2\">&#34;l1&#34;</span><span class=\"p\">;</span>\n</span></span><span class=\"line hl\"><span class=\"ln\">33</span>",
+	)
+}
diff --git a/markup/goldmark/convert_test.go b/markup/goldmark/convert_test.go
index 684f22c5455..ecb308eba69 100644
--- a/markup/goldmark/convert_test.go
+++ b/markup/goldmark/convert_test.go
@@ -20,6 +20,7 @@ import (
 
 	"github.com/spf13/cast"
 
+	"github.com/gohugoio/hugo/markup/converter/hooks"
 	"github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
 
 	"github.com/gohugoio/hugo/markup/highlight"
@@ -41,9 +42,18 @@ func convert(c *qt.C, mconf markup_config.Config, content string) converter.Resu
 		},
 	)
 	c.Assert(err, qt.IsNil)
+	h := highlight.New(mconf.Highlight)
+
+	getRenderer := func(t hooks.RendererType, id interface{}) interface{} {
+		if t == hooks.CodeBlockRendererType {
+			return h
+		}
+		return nil
+	}
+
 	conv, err := p.New(converter.DocumentContext{DocumentID: "thedoc"})
 	c.Assert(err, qt.IsNil)
-	b, err := conv.Convert(converter.RenderContext{RenderTOC: true, Src: []byte(content)})
+	b, err := conv.Convert(converter.RenderContext{RenderTOC: true, Src: []byte(content), GetRenderer: getRenderer})
 	c.Assert(err, qt.IsNil)
 
 	return b
@@ -372,12 +382,21 @@ LINE5
 			},
 		)
 
+		h := highlight.New(conf)
+
+		getRenderer := func(t hooks.RendererType, id interface{}) interface{} {
+			if t == hooks.CodeBlockRendererType {
+				return h
+			}
+			return nil
+		}
+
 		content := "```" + language + "\n" + code + "\n```"
 
 		c.Assert(err, qt.IsNil)
 		conv, err := p.New(converter.DocumentContext{})
 		c.Assert(err, qt.IsNil)
-		b, err := conv.Convert(converter.RenderContext{Src: []byte(content)})
+		b, err := conv.Convert(converter.RenderContext{Src: []byte(content), GetRenderer: getRenderer})
 		c.Assert(err, qt.IsNil)
 
 		return string(b.Bytes())
@@ -391,7 +410,7 @@ LINE5
 		// TODO(bep) there is a whitespace mismatch (\n) between this and the highlight template func.
 		c.Assert(result, qt.Equals, "<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"cl\"><span class=\"nb\">echo</span> <span class=\"s2\">&#34;Hugo Rocks!&#34;</span>\n</span></span></code></pre></div>")
 		result = convertForConfig(c, cfg, `echo "Hugo Rocks!"`, "unknown")
-		c.Assert(result, qt.Equals, "<pre tabindex=\"0\"><code class=\"language-unknown\" data-lang=\"unknown\">echo &quot;Hugo Rocks!&quot;\n</code></pre>")
+		c.Assert(result, qt.Equals, "<pre tabindex=\"0\"><code class=\"language-unknown\" data-lang=\"unknown\">echo &#34;Hugo Rocks!&#34;\n</code></pre>")
 	})
 
 	c.Run("Highlight lines, default config", func(c *qt.C) {
diff --git a/markup/goldmark/integration_test.go b/markup/goldmark/integration_test.go
index 4ace04f756b..f1fa745c5ae 100644
--- a/markup/goldmark/integration_test.go
+++ b/markup/goldmark/integration_test.go
@@ -36,12 +36,12 @@ func TestAttributeExclusion(t *testing.T) {
 ---
 title: "p1"
 ---
-## Heading {class="a" onclick="alert('heading')" linenos="inline"}
+## Heading {class="a" onclick="alert('heading')"}
 
 > Blockquote
-{class="b" ondblclick="alert('blockquote')" LINENOS="inline"}
+{class="b" ondblclick="alert('blockquote')"}
 
-~~~bash {id="c" onmouseover="alert('code fence')"}
+~~~bash {id="c" onmouseover="alert('code fence')" LINENOS=true}
 foo
 ~~~
 -- layouts/_default/single.html --
@@ -96,6 +96,63 @@ title: "p1"
 	`)
 }
 
+func TestAttributesDefaultRenderer(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- content/p1.md --
+---
+title: "p1"
+---
+## Heading Attribute Which Needs Escaping { class="a < b" }
+-- layouts/_default/single.html --
+{{ .Content }}
+`
+
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			NeedsOsFS:   false,
+		},
+	).Build()
+
+	b.AssertFileContent("public/p1/index.html", `
+class="a &lt; b"
+	`)
+}
+
+// Issue 9558.
+func TestAttributesHookNoEscape(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- content/p1.md --
+---
+title: "p1"
+---
+## Heading Attribute Which Needs Escaping { class="Smith & Wesson" }
+-- layouts/_default/_markup/render-heading.html --
+plain: |{{- range $k, $v := .Attributes -}}{{ $k }}: {{ $v }}|{{ end }}|
+safeHTML: |{{- range $k, $v := .Attributes -}}{{ $k }}: {{ $v | safeHTML }}|{{ end }}|
+-- layouts/_default/single.html --
+{{ .Content }}
+`
+
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			NeedsOsFS:   false,
+		},
+	).Build()
+
+	b.AssertFileContent("public/p1/index.html", `
+plain: |class: Smith &amp; Wesson|id: heading-attribute-which-needs-escaping|
+safeHTML: |class: Smith & Wesson|id: heading-attribute-which-needs-escaping|
+	`)
+}
+
 // Issue 9504
 func TestLinkInTitle(t *testing.T) {
 	t.Parallel()
@@ -132,6 +189,84 @@ title: "p1"
 	)
 }
 
+func TestHighlight(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- config.toml --
+[markup]
+[markup.highlight]
+anchorLineNos = false
+codeFences = true
+guessSyntax = false
+hl_Lines = ''
+lineAnchors = ''
+lineNoStart = 1
+lineNos = false
+lineNumbersInTable = true
+noClasses = false
+style = 'monokai'
+tabWidth = 4
+-- layouts/_default/single.html --
+{{ .Content }}
+-- content/p1.md --
+---
+title: "p1"
+---
+
+## Code Fences
+
+§§§bash
+LINE1
+§§§
+
+## Code Fences No Lexer
+
+§§§moo
+LINE1
+§§§
+
+## Code Fences Simple Attributes
+
+§§A§bash { .myclass id="myid" }
+LINE1
+§§A§
+
+## Code Fences Line Numbers
+
+§§§bash {linenos=table,hl_lines=[8,"15-17"],linenostart=199}
+LINE1
+LINE2
+LINE3
+LINE4
+LINE5
+LINE6
+LINE7
+LINE8
+§§§
+
+
+
+
+`
+
+	// Code fences
+	files = strings.ReplaceAll(files, "§§§", "```")
+
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+		},
+	).Build()
+
+	b.AssertFileContent("public/p1/index.html",
+		"<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"cl\">LINE1\n</span></span></code></pre></div>",
+		"Code Fences No Lexer</h2>\n<pre tabindex=\"0\"><code class=\"language-moo\" data-lang=\"moo\">LINE1\n</code></pre>",
+		"lnt",
+	)
+}
+
 func BenchmarkRenderHooks(b *testing.B) {
 	files := `
 -- config.toml --
diff --git a/markup/goldmark/toc_test.go b/markup/goldmark/toc_test.go
index f8fcf79d4d9..6e080bf468d 100644
--- a/markup/goldmark/toc_test.go
+++ b/markup/goldmark/toc_test.go
@@ -18,6 +18,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/gohugoio/hugo/markup/converter/hooks"
 	"github.com/gohugoio/hugo/markup/markup_config"
 
 	"github.com/gohugoio/hugo/common/loggers"
@@ -27,6 +28,8 @@ import (
 	qt "github.com/frankban/quicktest"
 )
 
+var nopGetRenderer = func(t hooks.RendererType, id interface{}) interface{} { return nil }
+
 func TestToc(t *testing.T) {
 	c := qt.New(t)
 
@@ -58,7 +61,7 @@ And then some.
 	c.Assert(err, qt.IsNil)
 	conv, err := p.New(converter.DocumentContext{})
 	c.Assert(err, qt.IsNil)
-	b, err := conv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true})
+	b, err := conv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true, GetRenderer: nopGetRenderer})
 	c.Assert(err, qt.IsNil)
 	got := b.(converter.TableOfContentsProvider).TableOfContents().ToHTML(2, 3, false)
 	c.Assert(got, qt.Equals, `<nav id="TableOfContents">
@@ -108,7 +111,7 @@ func TestEscapeToc(t *testing.T) {
 		"# `echo codeblock`",
 	}, "\n")
 	// content := ""
-	b, err := safeConv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true})
+	b, err := safeConv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true, GetRenderer: nopGetRenderer})
 	c.Assert(err, qt.IsNil)
 	got := b.(converter.TableOfContentsProvider).TableOfContents().ToHTML(1, 2, false)
 	c.Assert(got, qt.Equals, `<nav id="TableOfContents">
@@ -120,7 +123,7 @@ func TestEscapeToc(t *testing.T) {
   </ul>
 </nav>`, qt.Commentf(got))
 
-	b, err = unsafeConv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true})
+	b, err = unsafeConv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true, GetRenderer: nopGetRenderer})
 	c.Assert(err, qt.IsNil)
 	got = b.(converter.TableOfContentsProvider).TableOfContents().ToHTML(1, 2, false)
 	c.Assert(got, qt.Equals, `<nav id="TableOfContents">
diff --git a/tpl/cast/init_test.go b/tpl/cast/init_test.go
deleted file mode 100644
index 5eb4a908651..00000000000
--- a/tpl/cast/init_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package cast
-
-import (
-	"testing"
-
-	"github.com/gohugoio/hugo/htesting/hqt"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/collections/init_test.go b/tpl/collections/init_test.go
deleted file mode 100644
index 570e58d933b..00000000000
--- a/tpl/collections/init_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package collections
-
-import (
-	"testing"
-
-	"github.com/gohugoio/hugo/htesting/hqt"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/compare/init_test.go b/tpl/compare/init_test.go
deleted file mode 100644
index 8698cb5e397..00000000000
--- a/tpl/compare/init_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package compare
-
-import (
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/htesting/hqt"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/crypto/init_test.go b/tpl/crypto/init_test.go
deleted file mode 100644
index 1c200d7776d..00000000000
--- a/tpl/crypto/init_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package crypto
-
-import (
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/htesting/hqt"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/data/init_test.go b/tpl/data/init_test.go
deleted file mode 100644
index 631a91b3931..00000000000
--- a/tpl/data/init_test.go
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package data
-
-import (
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/config"
-	"github.com/gohugoio/hugo/htesting/hqt"
-	"github.com/gohugoio/hugo/langs"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	v := config.New()
-	v.Set("contentDir", "content")
-	langs.LoadLanguageSettings(v, nil)
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(newDeps(v))
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/debug/init_test.go b/tpl/debug/init_test.go
deleted file mode 100644
index 226915b340c..00000000000
--- a/tpl/debug/init_test.go
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2020 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package debug
-
-import (
-	"testing"
-
-	"github.com/gohugoio/hugo/htesting/hqt"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/common/loggers"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{Log: loggers.NewErrorLogger()})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/encoding/init_test.go b/tpl/encoding/init_test.go
deleted file mode 100644
index 666a4e5495f..00000000000
--- a/tpl/encoding/init_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package encoding
-
-import (
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/htesting/hqt"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/fmt/init_test.go b/tpl/fmt/init_test.go
deleted file mode 100644
index 07b740a73d6..00000000000
--- a/tpl/fmt/init_test.go
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package fmt
-
-import (
-	"testing"
-
-	"github.com/gohugoio/hugo/htesting/hqt"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/common/loggers"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{Log: loggers.NewIgnorableLogger(loggers.NewErrorLogger())})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/hugo/init_test.go b/tpl/hugo/init_test.go
deleted file mode 100644
index bc806448ed3..00000000000
--- a/tpl/hugo/init_test.go
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package hugo
-
-import (
-	"testing"
-
-	"github.com/gohugoio/hugo/config"
-
-	"github.com/gohugoio/hugo/htesting/hqt"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/resources/page"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-	v := config.New()
-	v.Set("contentDir", "content")
-	s := page.NewDummyHugoSite(v)
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{Site: s})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, s.Hugo())
-}
diff --git a/tpl/images/init_test.go b/tpl/images/init_test.go
deleted file mode 100644
index d8d8d78394c..00000000000
--- a/tpl/images/init_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package images
-
-import (
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/htesting/hqt"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/inflect/init_test.go b/tpl/inflect/init_test.go
deleted file mode 100644
index 38499838c96..00000000000
--- a/tpl/inflect/init_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package inflect
-
-import (
-	"testing"
-
-	"github.com/gohugoio/hugo/htesting/hqt"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/lang/init_test.go b/tpl/lang/init_test.go
deleted file mode 100644
index e62db95b947..00000000000
--- a/tpl/lang/init_test.go
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package lang
-
-import (
-	"testing"
-
-	"github.com/gohugoio/hugo/config"
-	"github.com/gohugoio/hugo/langs"
-
-	"github.com/gohugoio/hugo/htesting/hqt"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{
-			Language: langs.NewDefaultLanguage(config.New()),
-		})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/math/init_test.go b/tpl/math/init_test.go
deleted file mode 100644
index 9998eaf902e..00000000000
--- a/tpl/math/init_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package math
-
-import (
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/htesting/hqt"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/os/os_test.go b/tpl/os/os_test.go
index bbc0d018c0f..59491e97c73 100644
--- a/tpl/os/os_test.go
+++ b/tpl/os/os_test.go
@@ -11,34 +11,26 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package os
+package os_test
 
 import (
 	"path/filepath"
 	"testing"
 
-	"github.com/gohugoio/hugo/config"
+	"github.com/gohugoio/hugo/hugolib"
+	"github.com/gohugoio/hugo/tpl/os"
 
 	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/hugofs"
-	"github.com/spf13/afero"
 )
 
 func TestReadFile(t *testing.T) {
 	t.Parallel()
-	c := qt.New(t)
-
-	workingDir := "/home/hugo"
 
-	v := config.New()
-	v.Set("workingDir", workingDir)
+	b := newFileTestBuilder(t).Build()
 
-	// f := newTestFuncsterWithViper(v)
-	ns := New(&deps.Deps{Fs: hugofs.NewMem(v)})
+	// helpers.PrintFs(b.H.PathSpec.BaseFs.Work, "", _os.Stdout)
 
-	afero.WriteFile(ns.deps.Fs.Source, filepath.Join(workingDir, "/f/f1.txt"), []byte("f1-content"), 0755)
-	afero.WriteFile(ns.deps.Fs.Source, filepath.Join("/home", "f2.txt"), []byte("f2-content"), 0755)
+	ns := os.New(b.H.Deps)
 
 	for _, test := range []struct {
 		filename string
@@ -53,13 +45,13 @@ func TestReadFile(t *testing.T) {
 
 		result, err := ns.ReadFile(test.filename)
 
-		if b, ok := test.expect.(bool); ok && !b {
-			c.Assert(err, qt.Not(qt.IsNil))
+		if bb, ok := test.expect.(bool); ok && !bb {
+			b.Assert(err, qt.Not(qt.IsNil))
 			continue
 		}
 
-		c.Assert(err, qt.IsNil)
-		c.Assert(result, qt.Equals, test.expect)
+		b.Assert(err, qt.IsNil)
+		b.Assert(result, qt.Equals, test.expect)
 	}
 }
 
@@ -67,15 +59,8 @@ func TestFileExists(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	workingDir := "/home/hugo"
-
-	v := config.New()
-	v.Set("workingDir", workingDir)
-
-	ns := New(&deps.Deps{Fs: hugofs.NewMem(v)})
-
-	afero.WriteFile(ns.deps.Fs.Source, filepath.Join(workingDir, "/f/f1.txt"), []byte("f1-content"), 0755)
-	afero.WriteFile(ns.deps.Fs.Source, filepath.Join("/home", "f2.txt"), []byte("f2-content"), 0755)
+	b := newFileTestBuilder(t).Build()
+	ns := os.New(b.H.Deps)
 
 	for _, test := range []struct {
 		filename string
@@ -101,15 +86,8 @@ func TestFileExists(t *testing.T) {
 
 func TestStat(t *testing.T) {
 	t.Parallel()
-	c := qt.New(t)
-	workingDir := "/home/hugo"
-
-	v := config.New()
-	v.Set("workingDir", workingDir)
-
-	ns := New(&deps.Deps{Fs: hugofs.NewMem(v)})
-
-	afero.WriteFile(ns.deps.Fs.Source, filepath.Join(workingDir, "/f/f1.txt"), []byte("f1-content"), 0755)
+	b := newFileTestBuilder(t).Build()
+	ns := os.New(b.H.Deps)
 
 	for _, test := range []struct {
 		filename string
@@ -123,11 +101,28 @@ func TestStat(t *testing.T) {
 		result, err := ns.Stat(test.filename)
 
 		if test.expect == nil {
-			c.Assert(err, qt.Not(qt.IsNil))
+			b.Assert(err, qt.Not(qt.IsNil))
 			continue
 		}
 
-		c.Assert(err, qt.IsNil)
-		c.Assert(result.Size(), qt.Equals, test.expect)
+		b.Assert(err, qt.IsNil)
+		b.Assert(result.Size(), qt.Equals, test.expect)
 	}
 }
+
+func newFileTestBuilder(t *testing.T) *hugolib.IntegrationTestBuilder {
+	files := `
+-- f/f1.txt --
+f1-content
+-- home/f2.txt --
+f2-content
+	`
+
+	return hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			WorkingDir:  "/mywork",
+		},
+	)
+}
diff --git a/tpl/partials/init_test.go b/tpl/partials/init_test.go
deleted file mode 100644
index b18ac0a00a8..00000000000
--- a/tpl/partials/init_test.go
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package partials
-
-import (
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/common/loggers"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/htesting/hqt"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{
-			BuildStartListeners: &deps.Listeners{},
-			Log:                 loggers.NewErrorLogger(),
-		})
-		if ns.Name == namespaceName {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/path/init_test.go b/tpl/path/init_test.go
deleted file mode 100644
index 2282c330566..00000000000
--- a/tpl/path/init_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2018 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package path
-
-import (
-	"testing"
-
-	"github.com/gohugoio/hugo/htesting/hqt"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/reflect/init_test.go b/tpl/reflect/init_test.go
deleted file mode 100644
index 2ad33fc2583..00000000000
--- a/tpl/reflect/init_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package reflect
-
-import (
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/common/loggers"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/htesting/hqt"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{Log: loggers.NewErrorLogger()})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/safe/init_test.go b/tpl/safe/init_test.go
deleted file mode 100644
index 7aa1473d21c..00000000000
--- a/tpl/safe/init_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package safe
-
-import (
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/htesting/hqt"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/site/init_test.go b/tpl/site/init_test.go
deleted file mode 100644
index 46af2ef5b37..00000000000
--- a/tpl/site/init_test.go
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package site
-
-import (
-	"testing"
-
-	"github.com/gohugoio/hugo/config"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/htesting/hqt"
-	"github.com/gohugoio/hugo/resources/page"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-	v := config.New()
-	v.Set("contentDir", "content")
-	s := page.NewDummyHugoSite(v)
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{Site: s})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, s)
-}
diff --git a/tpl/strings/init_test.go b/tpl/strings/init_test.go
deleted file mode 100644
index 39d928601f4..00000000000
--- a/tpl/strings/init_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package strings
-
-import (
-	"testing"
-
-	"github.com/gohugoio/hugo/config"
-
-	"github.com/gohugoio/hugo/htesting/hqt"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{Cfg: config.New()})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/templates/init_test.go b/tpl/templates/init_test.go
deleted file mode 100644
index ada53b185aa..00000000000
--- a/tpl/templates/init_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2018 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package templates
-
-import (
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/htesting/hqt"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/time/init_test.go b/tpl/time/init_test.go
deleted file mode 100644
index d7efabfa777..00000000000
--- a/tpl/time/init_test.go
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package time
-
-import (
-	"testing"
-
-	"github.com/gohugoio/hugo/config"
-	"github.com/gohugoio/hugo/langs"
-
-	"github.com/gohugoio/hugo/htesting/hqt"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{
-			Language: langs.NewDefaultLanguage(config.New()),
-		})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go
index 6d2587bf77c..cb1aa6febd4 100644
--- a/tpl/tplimpl/template_funcs_test.go
+++ b/tpl/tplimpl/template_funcs_test.go
@@ -11,223 +11,74 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package tplimpl
+package tplimpl_test
 
 import (
-	"bytes"
-	"context"
 	"fmt"
-	"path/filepath"
-	"reflect"
+	"strings"
 	"testing"
-	"time"
 
-	"github.com/gohugoio/hugo/modules"
+	"github.com/gohugoio/hugo/hugolib"
 
-	"github.com/gohugoio/hugo/resources/page"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/common/hugo"
-	"github.com/gohugoio/hugo/common/loggers"
-	"github.com/gohugoio/hugo/config"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/hugofs"
-	"github.com/gohugoio/hugo/langs"
-	"github.com/gohugoio/hugo/langs/i18n"
-	"github.com/gohugoio/hugo/tpl"
 	"github.com/gohugoio/hugo/tpl/internal"
-	"github.com/gohugoio/hugo/tpl/partials"
-	"github.com/spf13/afero"
 )
 
-var logger = loggers.NewErrorLogger()
-
-func newTestConfig() config.Provider {
-	v := config.New()
-	v.Set("contentDir", "content")
-	v.Set("dataDir", "data")
-	v.Set("i18nDir", "i18n")
-	v.Set("layoutDir", "layouts")
-	v.Set("archetypeDir", "archetypes")
-	v.Set("assetDir", "assets")
-	v.Set("resourceDir", "resources")
-	v.Set("publishDir", "public")
-
-	langs.LoadLanguageSettings(v, nil)
-	mod, err := modules.CreateProjectModule(v)
-	if err != nil {
-		panic(err)
-	}
-	v.Set("allModules", modules.Modules{mod})
-
-	return v
-}
-
-func newDepsConfig(cfg config.Provider) deps.DepsCfg {
-	l := langs.NewLanguage("en", cfg)
-	return deps.DepsCfg{
-		Language:            l,
-		Site:                page.NewDummyHugoSite(cfg),
-		Cfg:                 cfg,
-		Fs:                  hugofs.NewMem(l),
-		Logger:              logger,
-		TemplateProvider:    DefaultTemplateProvider,
-		TranslationProvider: i18n.NewTranslationProvider(),
-	}
-}
-
 func TestTemplateFuncsExamples(t *testing.T) {
 	t.Parallel()
-	c := qt.New(t)
-
-	workingDir := "/home/hugo"
-
-	v := newTestConfig()
-
-	v.Set("workingDir", workingDir)
-	v.Set("multilingual", true)
-	v.Set("contentDir", "content")
-	v.Set("assetDir", "assets")
-	v.Set("baseURL", "http://mysite.com/hugo/")
-	v.Set("CurrentContentLanguage", langs.NewLanguage("en", v))
-
-	fs := hugofs.NewMem(v)
 
-	afero.WriteFile(fs.Source, filepath.Join(workingDir, "files", "README.txt"), []byte("Hugo Rocks!"), 0755)
-
-	depsCfg := newDepsConfig(v)
-	depsCfg.Fs = fs
-	d, err := deps.New(depsCfg)
-	defer d.Close()
-	c.Assert(err, qt.IsNil)
-
-	var data struct {
-		Title   string
-		Section string
-		Hugo    map[string]interface{}
-		Params  map[string]interface{}
-	}
-
-	data.Title = "**BatMan**"
-	data.Section = "blog"
-	data.Params = map[string]interface{}{"langCode": "en"}
-	data.Hugo = map[string]interface{}{"Version": hugo.MustParseVersion("0.36.1").Version()}
+	files := `
+-- config.toml --
+disableKinds=["home", "section", "taxonomy", "term", "sitemap", "robotsTXT"]
+ignoreErrors = ["my-err-id"]
+[outputs]
+home=["HTML"]
+-- layouts/partials/header.html --
+<title>Hugo Rocks!</title>
+-- files/README.txt --
+Hugo Rocks!
+-- content/blog/hugo-rocks.md --
+--- 
+title: "**BatMan**"
+---
+`
+
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			NeedsOsFS:   true,
+		},
+	).Build()
+
+	d := b.H.Sites[0].Deps
+
+	var (
+		templates []string
+		expected  []string
+	)
 
 	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
 		ns := nsf(d)
 		for _, mm := range ns.MethodMappings {
-			for i, example := range mm.Examples {
-				in, expected := example[0], example[1]
-				d.WithTemplate = func(templ tpl.TemplateManager) error {
-					c.Assert(templ.AddTemplate("test", in), qt.IsNil)
-					c.Assert(templ.AddTemplate("partials/header.html", "<title>Hugo Rocks!</title>"), qt.IsNil)
-					return nil
-				}
-				c.Assert(d.LoadResources(), qt.IsNil)
-
-				var b bytes.Buffer
-				templ, _ := d.Tmpl().Lookup("test")
-				c.Assert(d.Tmpl().Execute(templ, &b, &data), qt.IsNil)
-				if b.String() != expected {
-					t.Fatalf("%s[%d]: got %q expected %q", ns.Name, i, b.String(), expected)
+			for _, example := range mm.Examples {
+				if strings.Contains(example[0], "errorf") {
+					// This will fail the build, so skip for now.
+					continue
 				}
+				templates = append(templates, example[0])
+				expected = append(expected, example[1])
 			}
 		}
 	}
-}
-
-// TODO(bep) it would be dandy to put this one into the partials package, but
-// we have some package cycle issues to solve first.
-func TestPartialCached(t *testing.T) {
-	t.Parallel()
-
-	c := qt.New(t)
-
-	partial := `Now: {{ now.UnixNano }}`
-	name := "testing"
-
-	var data struct{}
-
-	v := newTestConfig()
-
-	config := newDepsConfig(v)
-
-	config.WithTemplate = func(templ tpl.TemplateManager) error {
-		err := templ.AddTemplate("partials/"+name, partial)
-		if err != nil {
-			return err
-		}
-
-		return nil
-	}
-
-	de, err := deps.New(config)
-	c.Assert(err, qt.IsNil)
-	defer de.Close()
-	c.Assert(de.LoadResources(), qt.IsNil)
-
-	ns := partials.New(de)
 
-	res1, err := ns.IncludeCached(context.Background(), name, &data)
-	c.Assert(err, qt.IsNil)
+	files += fmt.Sprintf("-- layouts/_default/single.html --\n%s\n", strings.Join(templates, "\n"))
+	b = hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			NeedsOsFS:   true,
+		},
+	).Build()
 
-	for j := 0; j < 10; j++ {
-		time.Sleep(2 * time.Nanosecond)
-		res2, err := ns.IncludeCached(context.Background(), name, &data)
-		c.Assert(err, qt.IsNil)
-
-		if !reflect.DeepEqual(res1, res2) {
-			t.Fatalf("cache mismatch")
-		}
-
-		res3, err := ns.IncludeCached(context.Background(), name, &data, fmt.Sprintf("variant%d", j))
-		c.Assert(err, qt.IsNil)
-
-		if reflect.DeepEqual(res1, res3) {
-			t.Fatalf("cache mismatch")
-		}
-	}
-}
-
-func BenchmarkPartial(b *testing.B) {
-	doBenchmarkPartial(b, func(ns *partials.Namespace) error {
-		_, err := ns.Include(context.Background(), "bench1")
-		return err
-	})
-}
-
-func BenchmarkPartialCached(b *testing.B) {
-	doBenchmarkPartial(b, func(ns *partials.Namespace) error {
-		_, err := ns.IncludeCached(context.Background(), "bench1", nil)
-		return err
-	})
-}
-
-func doBenchmarkPartial(b *testing.B, f func(ns *partials.Namespace) error) {
-	c := qt.New(b)
-	config := newDepsConfig(config.New())
-	config.WithTemplate = func(templ tpl.TemplateManager) error {
-		err := templ.AddTemplate("partials/bench1", `{{ shuffle (seq 1 10) }}`)
-		if err != nil {
-			return err
-		}
-
-		return nil
-	}
-
-	de, err := deps.New(config)
-	c.Assert(err, qt.IsNil)
-	defer de.Close()
-	c.Assert(de.LoadResources(), qt.IsNil)
-
-	ns := partials.New(de)
-
-	b.ResetTimer()
-	b.RunParallel(func(pb *testing.PB) {
-		for pb.Next() {
-			if err := f(ns); err != nil {
-				b.Fatalf("error executing template: %s", err)
-			}
-		}
-	})
+	b.AssertFileContent("public/blog/hugo-rocks/index.html", expected...)
 }
diff --git a/tpl/tplimpl/template_info_test.go b/tpl/tplimpl/template_info_test.go
deleted file mode 100644
index eaf57166a63..00000000000
--- a/tpl/tplimpl/template_info_test.go
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package tplimpl
-
-import (
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/hugofs"
-	"github.com/gohugoio/hugo/tpl"
-)
-
-func TestTemplateInfoShortcode(t *testing.T) {
-	c := qt.New(t)
-	d := newD(c)
-	defer d.Close()
-	h := d.Tmpl().(*templateExec)
-
-	c.Assert(h.AddTemplate("shortcodes/mytemplate.html", `
-{{ .Inner }}
-`), qt.IsNil)
-
-	c.Assert(h.postTransform(), qt.IsNil)
-
-	tt, found, _ := d.Tmpl().LookupVariant("mytemplate", tpl.TemplateVariants{})
-
-	c.Assert(found, qt.Equals, true)
-	tti, ok := tt.(tpl.Info)
-	c.Assert(ok, qt.Equals, true)
-	c.Assert(tti.ParseInfo().IsInner, qt.Equals, true)
-}
-
-// TODO(bep) move and use in other places
-func newD(c *qt.C) *deps.Deps {
-	v := newTestConfig()
-	fs := hugofs.NewMem(v)
-
-	depsCfg := newDepsConfig(v)
-	depsCfg.Fs = fs
-	d, err := deps.New(depsCfg)
-	c.Assert(err, qt.IsNil)
-
-	provider := DefaultTemplateProvider
-	provider.Update(d)
-
-	return d
-}
diff --git a/tpl/transform/init_test.go b/tpl/transform/init_test.go
deleted file mode 100644
index ec3c358974a..00000000000
--- a/tpl/transform/init_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package transform
-
-import (
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/htesting/hqt"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/transform/remarshal_test.go b/tpl/transform/remarshal_test.go
index 8e94ef6bf14..22548593b27 100644
--- a/tpl/transform/remarshal_test.go
+++ b/tpl/transform/remarshal_test.go
@@ -11,13 +11,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package transform
+package transform_test
 
 import (
 	"testing"
 
-	"github.com/gohugoio/hugo/config"
 	"github.com/gohugoio/hugo/htesting"
+	"github.com/gohugoio/hugo/hugolib"
+	"github.com/gohugoio/hugo/tpl/transform"
 
 	qt "github.com/frankban/quicktest"
 )
@@ -25,13 +26,14 @@ import (
 func TestRemarshal(t *testing.T) {
 	t.Parallel()
 
-	v := config.New()
-	v.Set("contentDir", "content")
-	ns := New(newDeps(v))
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{T: t},
+	).Build()
+
+	ns := transform.New(b.H.Deps)
 	c := qt.New(t)
 
 	c.Run("Roundtrip variants", func(c *qt.C) {
-
 		tomlExample := `title = 'Test Metadata'
 		
 [[resources]]
@@ -129,7 +131,6 @@ title: Test Metadata
 
 			}
 		}
-
 	})
 
 	c.Run("Comments", func(c *qt.C) {
diff --git a/tpl/transform/transform_test.go b/tpl/transform/transform_test.go
index 260de5f8314..3ccf1a2700a 100644
--- a/tpl/transform/transform_test.go
+++ b/tpl/transform/transform_test.go
@@ -11,13 +11,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package transform
+package transform_test
 
 import (
 	"html/template"
 	"testing"
 
 	"github.com/gohugoio/hugo/common/loggers"
+	"github.com/gohugoio/hugo/hugolib"
+	"github.com/gohugoio/hugo/tpl/transform"
 	"github.com/spf13/afero"
 
 	qt "github.com/frankban/quicktest"
@@ -32,10 +34,11 @@ type tstNoStringer struct{}
 
 func TestEmojify(t *testing.T) {
 	t.Parallel()
-	c := qt.New(t)
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{T: t},
+	).Build()
 
-	v := config.New()
-	ns := New(newDeps(v))
+	ns := transform.New(b.H.Deps)
 
 	for _, test := range []struct {
 		s      interface{}
@@ -49,23 +52,23 @@ func TestEmojify(t *testing.T) {
 
 		result, err := ns.Emojify(test.s)
 
-		if b, ok := test.expect.(bool); ok && !b {
-			c.Assert(err, qt.Not(qt.IsNil))
+		if bb, ok := test.expect.(bool); ok && !bb {
+			b.Assert(err, qt.Not(qt.IsNil))
 			continue
 		}
 
-		c.Assert(err, qt.IsNil)
-		c.Assert(result, qt.Equals, test.expect)
+		b.Assert(err, qt.IsNil)
+		b.Assert(result, qt.Equals, test.expect)
 	}
 }
 
 func TestHighlight(t *testing.T) {
 	t.Parallel()
-	c := qt.New(t)
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{T: t},
+	).Build()
 
-	v := config.New()
-	v.Set("contentDir", "content")
-	ns := New(newDeps(v))
+	ns := transform.New(b.H.Deps)
 
 	for _, test := range []struct {
 		s      interface{}
@@ -82,23 +85,23 @@ func TestHighlight(t *testing.T) {
 
 		result, err := ns.Highlight(test.s, test.lang, test.opts)
 
-		if b, ok := test.expect.(bool); ok && !b {
-			c.Assert(err, qt.Not(qt.IsNil))
+		if bb, ok := test.expect.(bool); ok && !bb {
+			b.Assert(err, qt.Not(qt.IsNil))
 			continue
 		}
 
-		c.Assert(err, qt.IsNil)
-		c.Assert(string(result), qt.Contains, test.expect.(string))
+		b.Assert(err, qt.IsNil)
+		b.Assert(string(result), qt.Contains, test.expect.(string))
 	}
 }
 
 func TestHTMLEscape(t *testing.T) {
 	t.Parallel()
-	c := qt.New(t)
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{T: t},
+	).Build()
 
-	v := config.New()
-	v.Set("contentDir", "content")
-	ns := New(newDeps(v))
+	ns := transform.New(b.H.Deps)
 
 	for _, test := range []struct {
 		s      interface{}
@@ -112,23 +115,23 @@ func TestHTMLEscape(t *testing.T) {
 
 		result, err := ns.HTMLEscape(test.s)
 
-		if b, ok := test.expect.(bool); ok && !b {
-			c.Assert(err, qt.Not(qt.IsNil))
+		if bb, ok := test.expect.(bool); ok && !bb {
+			b.Assert(err, qt.Not(qt.IsNil))
 			continue
 		}
 
-		c.Assert(err, qt.IsNil)
-		c.Assert(result, qt.Equals, test.expect)
+		b.Assert(err, qt.IsNil)
+		b.Assert(result, qt.Equals, test.expect)
 	}
 }
 
 func TestHTMLUnescape(t *testing.T) {
 	t.Parallel()
-	c := qt.New(t)
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{T: t},
+	).Build()
 
-	v := config.New()
-	v.Set("contentDir", "content")
-	ns := New(newDeps(v))
+	ns := transform.New(b.H.Deps)
 
 	for _, test := range []struct {
 		s      interface{}
@@ -142,23 +145,23 @@ func TestHTMLUnescape(t *testing.T) {
 
 		result, err := ns.HTMLUnescape(test.s)
 
-		if b, ok := test.expect.(bool); ok && !b {
-			c.Assert(err, qt.Not(qt.IsNil))
+		if bb, ok := test.expect.(bool); ok && !bb {
+			b.Assert(err, qt.Not(qt.IsNil))
 			continue
 		}
 
-		c.Assert(err, qt.IsNil)
-		c.Assert(result, qt.Equals, test.expect)
+		b.Assert(err, qt.IsNil)
+		b.Assert(result, qt.Equals, test.expect)
 	}
 }
 
 func TestMarkdownify(t *testing.T) {
 	t.Parallel()
-	c := qt.New(t)
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{T: t},
+	).Build()
 
-	v := config.New()
-	v.Set("contentDir", "content")
-	ns := New(newDeps(v))
+	ns := transform.New(b.H.Deps)
 
 	for _, test := range []struct {
 		s      interface{}
@@ -171,23 +174,24 @@ func TestMarkdownify(t *testing.T) {
 
 		result, err := ns.Markdownify(test.s)
 
-		if b, ok := test.expect.(bool); ok && !b {
-			c.Assert(err, qt.Not(qt.IsNil))
+		if bb, ok := test.expect.(bool); ok && !bb {
+			b.Assert(err, qt.Not(qt.IsNil))
 			continue
 		}
 
-		c.Assert(err, qt.IsNil)
-		c.Assert(result, qt.Equals, test.expect)
+		b.Assert(err, qt.IsNil)
+		b.Assert(result, qt.Equals, test.expect)
 	}
 }
 
 // Issue #3040
 func TestMarkdownifyBlocksOfText(t *testing.T) {
 	t.Parallel()
-	c := qt.New(t)
-	v := config.New()
-	v.Set("contentDir", "content")
-	ns := New(newDeps(v))
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{T: t},
+	).Build()
+
+	ns := transform.New(b.H.Deps)
 
 	text := `
 #First 
@@ -202,17 +206,18 @@ And then some.
 `
 
 	result, err := ns.Markdownify(text)
-	c.Assert(err, qt.IsNil)
-	c.Assert(result, qt.Equals, template.HTML(
+	b.Assert(err, qt.IsNil)
+	b.Assert(result, qt.Equals, template.HTML(
 		"<p>#First</p>\n<p>This is some <em>bold</em> text.</p>\n<h2 id=\"second\">Second</h2>\n<p>This is some more text.</p>\n<p>And then some.</p>\n"))
 }
 
 func TestPlainify(t *testing.T) {
 	t.Parallel()
-	c := qt.New(t)
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{T: t},
+	).Build()
 
-	v := config.New()
-	ns := New(newDeps(v))
+	ns := transform.New(b.H.Deps)
 
 	for _, test := range []struct {
 		s      interface{}
@@ -225,13 +230,13 @@ func TestPlainify(t *testing.T) {
 
 		result, err := ns.Plainify(test.s)
 
-		if b, ok := test.expect.(bool); ok && !b {
-			c.Assert(err, qt.Not(qt.IsNil))
+		if bb, ok := test.expect.(bool); ok && !bb {
+			b.Assert(err, qt.Not(qt.IsNil))
 			continue
 		}
 
-		c.Assert(err, qt.IsNil)
-		c.Assert(result, qt.Equals, test.expect)
+		b.Assert(err, qt.IsNil)
+		b.Assert(result, qt.Equals, test.expect)
 	}
 }
 
diff --git a/tpl/transform/unmarshal_test.go b/tpl/transform/unmarshal_test.go
index fb0e446c338..2b14282ece4 100644
--- a/tpl/transform/unmarshal_test.go
+++ b/tpl/transform/unmarshal_test.go
@@ -11,7 +11,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package transform
+package transform_test
 
 import (
 	"fmt"
@@ -19,7 +19,8 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/gohugoio/hugo/config"
+	"github.com/gohugoio/hugo/hugolib"
+	"github.com/gohugoio/hugo/tpl/transform"
 
 	"github.com/gohugoio/hugo/common/hugio"
 	"github.com/gohugoio/hugo/resources/resource"
@@ -80,12 +81,14 @@ func (t testContentResource) Key() string {
 }
 
 func TestUnmarshal(t *testing.T) {
-	v := config.New()
-	ns := New(newDeps(v))
-	c := qt.New(t)
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{T: t},
+	).Build()
+
+	ns := transform.New(b.H.Deps)
 
 	assertSlogan := func(m map[string]interface{}) {
-		c.Assert(m["slogan"], qt.Equals, "Hugo Rocks!")
+		b.Assert(m["slogan"], qt.Equals, "Hugo Rocks!")
 	}
 
 	for _, test := range []struct {
@@ -116,24 +119,24 @@ func TestUnmarshal(t *testing.T) {
 		}},
 		{testContentResource{key: "r1", content: `1997,Ford,E350,"ac, abs, moon",3000.00
 1999,Chevy,"Venture ""Extended Edition""","",4900.00`, mime: media.CSVType}, nil, func(r [][]string) {
-			c.Assert(len(r), qt.Equals, 2)
+			b.Assert(len(r), qt.Equals, 2)
 			first := r[0]
-			c.Assert(len(first), qt.Equals, 5)
-			c.Assert(first[1], qt.Equals, "Ford")
+			b.Assert(len(first), qt.Equals, 5)
+			b.Assert(first[1], qt.Equals, "Ford")
 		}},
 		{testContentResource{key: "r1", content: `a;b;c`, mime: media.CSVType}, map[string]interface{}{"delimiter": ";"}, func(r [][]string) {
-			c.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
+			b.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
 		}},
 		{"a,b,c", nil, func(r [][]string) {
-			c.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
+			b.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
 		}},
 		{"a;b;c", map[string]interface{}{"delimiter": ";"}, func(r [][]string) {
-			c.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
+			b.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
 		}},
 		{testContentResource{key: "r1", content: `
 % This is a comment
 a;b;c`, mime: media.CSVType}, map[string]interface{}{"DElimiter": ";", "Comment": "%"}, func(r [][]string) {
-			c.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
+			b.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
 		}},
 		// errors
 		{"thisisnotavaliddataformat", nil, false},
@@ -144,7 +147,7 @@ a;b;c`, mime: media.CSVType}, map[string]interface{}{"DElimiter": ";", "Comment"
 		{tstNoStringer{}, nil, false},
 	} {
 
-		ns.cache.Clear()
+		ns.Reset()
 
 		var args []interface{}
 
@@ -156,29 +159,32 @@ a;b;c`, mime: media.CSVType}, map[string]interface{}{"DElimiter": ";", "Comment"
 
 		result, err := ns.Unmarshal(args...)
 
-		if b, ok := test.expect.(bool); ok && !b {
-			c.Assert(err, qt.Not(qt.IsNil))
+		if bb, ok := test.expect.(bool); ok && !bb {
+			b.Assert(err, qt.Not(qt.IsNil))
 		} else if fn, ok := test.expect.(func(m map[string]interface{})); ok {
-			c.Assert(err, qt.IsNil)
+			b.Assert(err, qt.IsNil)
 			m, ok := result.(map[string]interface{})
-			c.Assert(ok, qt.Equals, true)
+			b.Assert(ok, qt.Equals, true)
 			fn(m)
 		} else if fn, ok := test.expect.(func(r [][]string)); ok {
-			c.Assert(err, qt.IsNil)
+			b.Assert(err, qt.IsNil)
 			r, ok := result.([][]string)
-			c.Assert(ok, qt.Equals, true)
+			b.Assert(ok, qt.Equals, true)
 			fn(r)
 		} else {
-			c.Assert(err, qt.IsNil)
-			c.Assert(result, qt.Equals, test.expect)
+			b.Assert(err, qt.IsNil)
+			b.Assert(result, qt.Equals, test.expect)
 		}
 
 	}
 }
 
 func BenchmarkUnmarshalString(b *testing.B) {
-	v := config.New()
-	ns := New(newDeps(v))
+	bb := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{T: b},
+	).Build()
+
+	ns := transform.New(bb.H.Deps)
 
 	const numJsons = 100
 
@@ -200,8 +206,11 @@ func BenchmarkUnmarshalString(b *testing.B) {
 }
 
 func BenchmarkUnmarshalResource(b *testing.B) {
-	v := config.New()
-	ns := New(newDeps(v))
+	bb := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{T: b},
+	).Build()
+
+	ns := transform.New(bb.H.Deps)
 
 	const numJsons = 100
 
diff --git a/tpl/urls/init_test.go b/tpl/urls/init_test.go
deleted file mode 100644
index 7e53c247a5c..00000000000
--- a/tpl/urls/init_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package urls
-
-import (
-	"testing"
-
-	"github.com/gohugoio/hugo/config"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/htesting/hqt"
-	"github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
-	c := qt.New(t)
-	var found bool
-	var ns *internal.TemplateFuncsNamespace
-
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns = nsf(&deps.Deps{Cfg: config.New()})
-		if ns.Name == name {
-			found = true
-			break
-		}
-	}
-
-	c.Assert(found, qt.Equals, true)
-	ctx, err := ns.Context()
-	c.Assert(err, qt.IsNil)
-	c.Assert(ctx, hqt.IsSameType, &Namespace{})
-
-}