Repository: gohugoio/hugo (language: go)

Issue:
Create render hooks for inline and block passthrough snippets
There is zero demand for this at the moment, but I wanted to describe utility while it's fresh in my head.

```text
layotus/
└── _default/
    └── _markup/
        ├── render-passthrough-block.html
        └── render-passthrough-inline.html
```

How would these be useful?

1. Wrap passthrough snippets in an element (e.g., `span`, `figure`). In addition to custom presentation, this would provide a targeted way to handle the [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content) exhibited when using [MathJax](https://www.mathjax.org/) or [KaTex](https://katex.org/).
2. This would allow you to set a `.Page.Store` value (e.g., `hasMath`) to trigger JS inclusion, instead of having to set a site and/or front matter parameter.
3. It would set us up for server-side rendering in the future (e.g., `{{ transform.ToMathML .Inner }}`) with something like <https://git.sr.ht/~mekyt/latex2mathml> (native Go, currently very buggy). Or passing `.Inner` to a `resources.GetRemote` call to the free [Math API](https://math.vercel.app/) service to embed an SVG.

References:

- https://gohugo.io/content-management/mathematics/
- https://github.com/gohugoio/hugo-goldmark-extensions?tab=readme-ov-file#passthrough-extension
- https://github.com/mathjax/MathJax/issues/131
- https://github.com/gohugoio/hugo/issues/10044
- https://git.sr.ht/~mekyt/latex2mathml

cc: @j2kun 
Support GitHub's Alert Markdown extension
I am currently immersed in the development of a new Hugo theme, which aims to provide users with a seamless experience when writing Hugo blogs using Obsidian. However, when it comes to Callout blocks like this:

> [!important]
> This is a Callout.

Things get stuck. While utilizing shortcodes and regex can be a workaround, it tends to be error-prone and lacks elegance.

Thus, I am curious whether the esteemed Hugo development team would consider enhancing the Markdown parsing engine to natively support GitHub-style Callouts.

Yours sincerely.



Discussion / hints:
I'm interested in rendering math server-side! Sounds like implementing these render hooks would pave the road. How can I help?
@Xeyler I'm not aware of any native Go LaTeX-to-MathML or LaTeX-to-SVG tools that are accessible and ready for prime time. https://git.sr.ht/~mekyt/latex2mathml was not even close to ready when I tried it a few months ago. Server-side rendering isn't going to happen unless there's a native Go package (i.e., CGO and Node.js approaches are not acceptable).
What is the reasoning behind only using native Go packages? And (assuming CGO refers to [Cairo Go](https://github.com/tdewolff/canvas)) what makes CGO unacceptable?

About server-side Latex rendering, MathML is supported by "only" [94% of web browsers](https://caniuse.com/mathml) at the time of this writing. SVG, on the other hand, is supported by [more than 98% of web browsers](https://caniuse.com/svg).

Since 2019, MathJax (which is recommended for use [in the Hugo docs](https://gohugo.io/content-management/mathematics/)) hasn't supported MathML output. It does support SVG or CommonHTML output, [preferring CommonHTML](https://docs.mathjax.org/en/latest/output/html.html), which if I understand correctly, is specific to MathJax. CommonHTML is useful because it uses web-based fonts so that users can either load math fonts from a CDN or use a locally-installed font. SVG is potentially useful because the presentation doesn't vary between web browsers.

Now, with respect to Hugo and server-side rendering, the Latex -> MathML or Latex -> SVG tooling isn't quite what it needs to be in Go yet, apparently. latex2mathml is a good start, but it could probably use more unit tests and test-driven development to stabilize the output. Otherwise, a new "latex2svg" go library to convert Latex to SVGs would require more work, but support more web browsers.

Sounds like a lot of work either way, but do I understand the issue and most useful/probable solutions?

CGO = https://pkg.go.dev/cmd/cgo

Our two CGO implementations (libsass and WebP encoding) had a very high development cost; we're not doing that again unless there's no other choice for some critical feature. And server-side LaTeX rendering is not critical.

I outlined the probable solutions in my previous comment.
>Our two CGO implementations (libsass and WebP encoding) had a very high development cost;

The development/integration/maintenance cost (significant) is one thing, but but it also makes the build process more complex -- currently we only support the extended (CGO) version of Hugo on Linux, MacOS and Windows.
I understand. It really comes down to a lack of native latex-mathml/svg tooling for Go, then. Maybe I'll investigate latex2mathml and see if there's anything I can do to help development.

Meanwhile, these passthrough render blocks _do_ fall within the scope of Hugo itself. Honestly, I don't have a lot of time right now. I'll have more after the end of the semester in a couple weeks. However, I've used Hugo's render hooks and I'm familiar with how they work. It's been two months since this issue was posted and it looks like it hasn't become anyone else's priority, so I'd like to try my hand at it.

What sort of context would this render hook provide? Something like `.PassthroughContent`, holding the plaintext content of the block that was "passed through"? What else would be necessary?
> it looks like it hasn't become anyone else's priority

It hasn't been accepted either. It's still in the proposal stage, without any demand. I wouldn't spend any time on this right now.
When this was proposed/implemented at GitHub, I suggested they used markdown attributes instead (which is certainly more standard and something Hugo could easily support). 

They chose not to listen to me. But you could, e.g:

````
```callout {level="warning" }
This is a warning.
```
````

And add a code render hook for it.
> When this was proposed/implemented at GitHub, I suggested they used markdown attributes instead (which is certainly more standard and something Hugo could easily support).
> 
> They chose not to listen to me. But you could, e.g:
> 
> ```
> ```callout {level="warning" }
> This is a warning.
> ```
> ```
> 
> And add a code render hook for it.

Thank you for your kind reply, @bep. Using markdown attributes is indeed a more Markdown-standard solution, and I will try it in my development. I might also come up with another solution for our Obsidian users to write with ease.

I won't close this issue, and if there are any breakthroughs, I will update it.

Have a day as amazing as you are :)
GitHub's documentation for their Alert Markdown extension:
<https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts> 

GitHub's implementation is OK if you speak English. But for the rest of the planet, not so much.

Using English words in the alert designator (e.g., `[!NOTE]`)  is fine, but the _rendered_ alert type is always English. For example:

```text
> [!NOTE]  
> Das ist meine Notiz.
```

> [!NOTE]  
> Das ist meine Notiz.

The above is just... rude.

We could support this monolignual syntax with a Goldmark extension, or (preferably) provide multilingual support via a blockquote render hook. 

The blockquote render hook would parse the first line, looking for an alert designator (e.g., `[!something]`), and provide this context to the render hook template:

- `.Attributes` -- The Markdown attributes, if any.
- `.Text` -- The blockquote text.
- `.PlainText` -- The blockquote text as plain text.
- `.Page` -- A reference to the current page.
- `.Ordinal` -- The zero-based ordinal of the blockquote on the page.
- `.Position` -- The position of the blockquote within the page content.
- `.IsAlert` -- Returns `true` if the blockquote has an alert designator.
- `.AlertType` -- The alert type converted to lowercase.
- `.AlertText` -- The blockquote text, excluding the alert designator.
- `.AlertPlainText` -- The blockquote text as plain text, excluding the alert designator.

Ordinal and position are desired but not required. The ordinal is handy for assigning element ids, and the position is handy for error reporting.  Example render hook:

```text
{{ $emojis := dict
  "caution" ":exclamation:"
  "important" ":information_source:"
  "note" ":information_source:"
  "tip" ":bulb:"
  "warning" ":information_source:"
}}

{{ if .IsAlert }}
  <blockquote class="alert alert-{{ .AlertType }}">
    <p class="alert-heading">
      {{ transform.Emojify (index $emojis .AlertType) }}
      {{ or (i18n .AlertType) (title .AlertType) }}
    </p>
    {{ .AlertText | safeHTML }}
  </blockquote>
{{ else }}
  <blockquote>
    {{ .Text | safeHTML }}
  </blockquote>
{{ end }}
```

This render hook implementation would also allow you to do things like:

```text
[markup.goldmark.parser.attribute]
block = true
```

```text
> Some text
{cite="https://gohugo.io" caption="Some caption"}
```

```text
<figure>
  <blockquote {{ with .Attributes.cite }}cite="{{ . }}"{{ end }}>
    {{ .Text | safeHTML }}
  </blockquote>
  {{ with .Attributes.caption }}
    <figcaption class="blockquote-caption">
      {{ . | safeHTML }}
    </figcaption>
  {{ end }}
</figure>
```


Have there been any breakthroughs here? I wrote a Python script for an All-in-One regex and S3 image upload, and it works well for me for now. However, I'm still hoping for native support for GitHub alerts. This Markdown syntax does have its quirks, but it's currently the most widely used format. :)

PS: My hastily-written Python regex
```py
alert_pattern = re.compile(r'^>\s*\[!(.*?)\]\s*\n((?:>.*\n)*)', re.MULTILINE | re.DOTALL)
def replace_alert(match):
    alert_type = match.group(1).strip()
    content = match.group(2).replace('>', '').strip()
    return f'{{{{< alert "{alert_type}" >}}}}\n{content}\n{{{{< /alert >}}}}'
    
content = alert_pattern.sub(replace_alert, content)
```

Fix patch:
diff --git a/go.mod b/go.mod
index c38aac9eb7a..8932d934745 100644
--- a/go.mod
+++ b/go.mod
@@ -40,7 +40,7 @@ require (
 	github.com/gohugoio/hashstructure v0.1.0
 	github.com/gohugoio/httpcache v0.7.0
 	github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0
-	github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0
+	github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0
 	github.com/gohugoio/locales v0.14.0
 	github.com/gohugoio/localescompressed v1.0.1
 	github.com/gohugoio/testmodBuilder/mods v0.0.0-20190520184928-c56af20f2e95
diff --git a/go.sum b/go.sum
index cfcf2a12024..6e584cfad2e 100644
--- a/go.sum
+++ b/go.sum
@@ -235,6 +235,8 @@ github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0 h1:MNdY6hYCTQEekY0oAf
 github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0/go.mod h1:oBdBVuiZ0fv9xd8xflUgt53QxW5jOCb1S+xntcN4SKo=
 github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0 h1:PCtO5l++psZf48yen2LxQ3JiOXxaRC6v0594NeHvGZg=
 github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0/go.mod h1:g9CCh+Ci2IMbPUrVJuXbBTrA+rIIx5+hDQ4EXYaQDoM=
+github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0 h1:7PY5PIJ2mck7v6R52yCFvvYHvsPMEbulgRviw3I9lP4=
+github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0/go.mod h1:r8g5S7bHfdj0+9ShBog864ufCsVODKQZNjYYY8OnJpM=
 github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc=
 github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4=
 github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo=
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
index 6dffd18a579..59cb574dfe0 100644
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -440,8 +440,8 @@ func (pco *pageContentOutput) initRenderHooks() error {
 			var offset int
 
 			switch v := ctx.(type) {
-			case hooks.CodeblockContext:
-				offset = bytes.Index(source, []byte(v.Inner()))
+			case hooks.PositionerSourceTargetProvider:
+				offset = bytes.Index(source, v.PositionerSourceTarget())
 			}
 
 			pos := pco.po.p.posFromInput(source, offset)
@@ -476,6 +476,16 @@ func (pco *pageContentOutput) initRenderHooks() error {
 				layoutDescriptor.Kind = "render-image"
 			case hooks.HeadingRendererType:
 				layoutDescriptor.Kind = "render-heading"
+			case hooks.PassthroughRendererType:
+				layoutDescriptor.Kind = "render-passthrough"
+				if id != nil {
+					layoutDescriptor.KindVariants = id.(string)
+				}
+			case hooks.BlockquoteRendererType:
+				layoutDescriptor.Kind = "render-blockquote"
+				if id != nil {
+					layoutDescriptor.KindVariants = id.(string)
+				}
 			case hooks.CodeBlockRendererType:
 				layoutDescriptor.Kind = "render-codeblock"
 				if id != nil {
diff --git a/hugolib/site.go b/hugolib/site.go
index 2113c4f2086..bbf110d1d8c 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -915,6 +915,14 @@ func (hr hookRendererTemplate) RenderCodeblock(cctx context.Context, w hugio.Fle
 	return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
 }
 
+func (hr hookRendererTemplate) RenderPassthrough(cctx context.Context, w io.Writer, ctx hooks.PassthroughContext) error {
+	return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
+}
+
+func (hr hookRendererTemplate) RenderBlockquote(cctx context.Context, w hugio.FlexiWriter, ctx hooks.BlockquoteContext) error {
+	return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
+}
+
 func (hr hookRendererTemplate) ResolvePosition(ctx any) text.Position {
 	return hr.resolvePosition(ctx)
 }
diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go
index 54babd32086..29e848d80cc 100644
--- a/markup/converter/hooks/hooks.go
+++ b/markup/converter/hooks/hooks.go
@@ -78,6 +78,49 @@ type CodeblockContext interface {
 	Ordinal() int
 }
 
+// BlockquoteContext is the context passed to a blockquote render hook.
+type BlockquoteContext interface {
+	AttributesProvider
+	text.Positioner
+	PageProvider
+
+	// Zero-based ordinal for all block quotes in the current document.
+	Ordinal() int
+
+	// The blockquote text.
+	// If type is "alert", this will be the alert text.
+	Text() hstring.RenderedString
+
+	/// Returns the blockquote type, one of "regular" and "alert".
+	// Type "alert" indicates that this is a GitHub type alert.
+	Type() string
+
+	// The GitHub alert type converted to lowercase, e.g. "note".
+	// Only set if Type is "alert".
+	AlertType() string
+}
+
+type PositionerSourceTargetProvider interface {
+	// For internal use.
+	PositionerSourceTarget() []byte
+}
+
+// PassThroughContext is the context passed to a passthrough render hook.
+type PassthroughContext interface {
+	AttributesProvider
+	text.Positioner
+	PageProvider
+
+	// Currently one of "inline" or "block".
+	Type() string
+
+	// The inner content of the passthrough element, excluding the delimiters.
+	Inner() string
+
+	// Zero-based ordinal for all passthrough elements in the document.
+	Ordinal() int
+}
+
 type AttributesOptionsSliceProvider interface {
 	AttributesSlice() []attributes.Attribute
 	OptionsSlice() []attributes.Attribute
@@ -91,6 +134,14 @@ type CodeBlockRenderer interface {
 	RenderCodeblock(cctx context.Context, w hugio.FlexiWriter, ctx CodeblockContext) error
 }
 
+type BlockquoteRenderer interface {
+	RenderBlockquote(cctx context.Context, w hugio.FlexiWriter, ctx BlockquoteContext) error
+}
+
+type PassthroughRenderer interface {
+	RenderPassthrough(cctx context.Context, w io.Writer, ctx PassthroughContext) error
+}
+
 type IsDefaultCodeBlockRendererProvider interface {
 	IsDefaultCodeBlockRenderer() bool
 }
@@ -143,6 +194,8 @@ const (
 	ImageRendererType
 	HeadingRendererType
 	CodeBlockRendererType
+	PassthroughRendererType
+	BlockquoteRendererType
 )
 
 type GetRendererFunc func(t RendererType, id any) any
diff --git a/markup/goldmark/blockquotes/blockquotes.go b/markup/goldmark/blockquotes/blockquotes.go
new file mode 100644
index 00000000000..f4c908e0ba9
--- /dev/null
+++ b/markup/goldmark/blockquotes/blockquotes.go
@@ -0,0 +1,248 @@
+// Copyright 2024 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 blockquotes
+
+import (
+	"regexp"
+	"strings"
+	"sync"
+
+	"github.com/gohugoio/hugo/common/herrors"
+	htext "github.com/gohugoio/hugo/common/text"
+	"github.com/gohugoio/hugo/common/types/hstring"
+	"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/renderer"
+	"github.com/yuin/goldmark/renderer/html"
+	"github.com/yuin/goldmark/util"
+)
+
+type (
+	blockquotesExtension struct{}
+	htmlRenderer         struct{}
+)
+
+func New() goldmark.Extender {
+	return &blockquotesExtension{}
+}
+
+func (e *blockquotesExtension) Extend(m goldmark.Markdown) {
+	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(ast.KindBlockquote, r.renderBlockquote)
+}
+
+const (
+	typeRegular = "regular"
+	typeAlert   = "alert"
+)
+
+func (r *htmlRenderer) renderBlockquote(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	ctx := w.(*render.Context)
+
+	n := node.(*ast.Blockquote)
+
+	if entering {
+		// Store the current pos so we can capture the rendered text.
+		ctx.PushPos(ctx.Buffer.Len())
+		return ast.WalkContinue, nil
+	}
+
+	pos := ctx.PopPos()
+	text := ctx.Buffer.Bytes()[pos:]
+	ctx.Buffer.Truncate(pos)
+
+	// Extract a source sample to use for position information.
+	nn := n.FirstChild()
+	var start, stop int
+	for i := 0; i < nn.Lines().Len() && i < 2; i++ {
+		line := nn.Lines().At(i)
+		if i == 0 {
+			start = line.Start
+		}
+		stop = line.Stop
+	}
+
+	// We do not mutate the source, so this is safe.
+	sourceRef := src[start:stop]
+
+	ordinal := ctx.GetAndIncrementOrdinal(ast.KindBlockquote)
+
+	texts := string(text)
+	typ := typeRegular
+	alertType := resolveGitHubAlert(texts)
+	if alertType != "" {
+		typ = typeAlert
+	}
+
+	renderer := ctx.RenderContext().GetRenderer(hooks.BlockquoteRendererType, typ)
+	if renderer == nil {
+		return r.renderBlockquoteDefault(w, n, texts)
+	}
+
+	if typ == typeAlert {
+		// Trim preamble: <p>[!NOTE]<br>\n but preserve leading paragraph.
+		// We could possibly complicate this by moving this to the parser, but
+		// keep it simple for now.
+		texts = "<p>" + texts[strings.Index(texts, "\n")+1:]
+	}
+
+	bqctx := &blockquoteContext{
+		page:             ctx.DocumentContext().Document,
+		pageInner:        r.getPageInner(ctx),
+		typ:              typ,
+		alertType:        alertType,
+		text:             hstring.RenderedString(texts),
+		sourceRef:        sourceRef,
+		ordinal:          ordinal,
+		AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
+	}
+
+	bqctx.createPos = func() htext.Position {
+		if resolver, ok := renderer.(hooks.ElementPositionResolver); ok {
+			return resolver.ResolvePosition(bqctx)
+		}
+
+		return htext.Position{
+			Filename:     ctx.DocumentContext().Filename,
+			LineNumber:   1,
+			ColumnNumber: 1,
+		}
+	}
+
+	cr := renderer.(hooks.BlockquoteRenderer)
+
+	err := cr.RenderBlockquote(
+		ctx.RenderContext().Ctx,
+		w,
+		bqctx,
+	)
+	if err != nil {
+		return ast.WalkContinue, herrors.NewFileErrorFromPos(err, bqctx.createPos())
+	}
+
+	return ast.WalkContinue, nil
+}
+
+func (r *htmlRenderer) getPageInner(rctx *render.Context) any {
+	pid := rctx.PeekPid()
+	if pid > 0 {
+		if lookup := rctx.DocumentContext().DocumentLookup; lookup != nil {
+			if v := rctx.DocumentContext().DocumentLookup(pid); v != nil {
+				return v
+			}
+		}
+	}
+	return rctx.DocumentContext().Document
+}
+
+// Code borrowed from goldmark's html renderer.
+func (r *htmlRenderer) renderBlockquoteDefault(
+	w util.BufWriter, n ast.Node, text string,
+) (ast.WalkStatus, error) {
+	if n.Attributes() != nil {
+		_, _ = w.WriteString("<blockquote")
+		html.RenderAttributes(w, n, html.BlockquoteAttributeFilter)
+		_ = w.WriteByte('>')
+	} else {
+		_, _ = w.WriteString("<blockquote>\n")
+	}
+
+	_, _ = w.WriteString(text)
+
+	_, _ = w.WriteString("</blockquote>\n")
+	return ast.WalkContinue, nil
+}
+
+type blockquoteContext struct {
+	page      any
+	pageInner any
+	text      hstring.RenderedString
+	typ       string
+	sourceRef []byte
+	alertType string
+	ordinal   int
+
+	// This is only used in error situations and is expensive to create,
+	// so delay creation until needed.
+	pos       htext.Position
+	posInit   sync.Once
+	createPos func() htext.Position
+
+	*attributes.AttributesHolder
+}
+
+func (c *blockquoteContext) Type() string {
+	return c.typ
+}
+
+func (c *blockquoteContext) AlertType() string {
+	return c.alertType
+}
+
+func (c *blockquoteContext) Page() any {
+	return c.page
+}
+
+func (c *blockquoteContext) PageInner() any {
+	return c.pageInner
+}
+
+func (c *blockquoteContext) Text() hstring.RenderedString {
+	return c.text
+}
+
+func (c *blockquoteContext) Ordinal() int {
+	return c.ordinal
+}
+
+func (c *blockquoteContext) Position() htext.Position {
+	c.posInit.Do(func() {
+		c.pos = c.createPos()
+	})
+	return c.pos
+}
+
+func (c *blockquoteContext) PositionerSourceTarget() []byte {
+	return c.sourceRef
+}
+
+var _ hooks.PositionerSourceTargetProvider = (*blockquoteContext)(nil)
+
+// https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts
+// Five types:
+// [!NOTE], [!TIP], [!WARNING], [!IMPORTANT], [!CAUTION]
+var gitHubAlertRe = regexp.MustCompile(`^<p>\[!(NOTE|TIP|WARNING|IMPORTANT|CAUTION)\]`)
+
+// resolveGitHubAlert returns one of note, tip, warning, important or caution.
+// An empty string if no match.
+func resolveGitHubAlert(s string) string {
+	m := gitHubAlertRe.FindStringSubmatch(s)
+	if len(m) == 2 {
+		return strings.ToLower(m[1])
+	}
+	return ""
+}
diff --git a/markup/goldmark/codeblocks/render.go b/markup/goldmark/codeblocks/render.go
index 5dfa8262fed..fad3ac45809 100644
--- a/markup/goldmark/codeblocks/render.go
+++ b/markup/goldmark/codeblocks/render.go
@@ -44,11 +44,6 @@ func New() goldmark.Extender {
 }
 
 func (e *codeBlocksExtension) Extend(m goldmark.Markdown) {
-	m.Parser().AddOptions(
-		parser.WithASTTransformers(
-			util.Prioritized(&Transformer{}, 100),
-		),
-	)
 	m.Renderer().AddOptions(renderer.WithNodeRenderers(
 		util.Prioritized(newHTMLRenderer(), 100),
 	))
@@ -60,7 +55,7 @@ func newHTMLRenderer() renderer.NodeRenderer {
 }
 
 func (r *htmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
-	reg.Register(KindCodeBlock, r.renderCodeBlock)
+	reg.Register(ast.KindFencedCodeBlock, r.renderCodeBlock)
 }
 
 func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
@@ -70,28 +65,29 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
 		return ast.WalkContinue, nil
 	}
 
-	n := node.(*codeBlock)
-	lang := getLang(n.b, src)
+	n := node.(*ast.FencedCodeBlock)
+
+	lang := getLang(n, src)
 	renderer := ctx.RenderContext().GetRenderer(hooks.CodeBlockRendererType, lang)
 	if renderer == nil {
 		return ast.WalkStop, fmt.Errorf("no code renderer found for %q", lang)
 	}
 
-	ordinal := n.ordinal
+	ordinal := ctx.GetAndIncrementOrdinal(ast.KindFencedCodeBlock)
 
 	var buff bytes.Buffer
 
-	l := n.b.Lines().Len()
+	l := n.Lines().Len()
 	for i := 0; i < l; i++ {
-		line := n.b.Lines().At(i)
+		line := n.Lines().At(i)
 		buff.Write(line.Value(src))
 	}
 
 	s := htext.Chomp(buff.String())
 
 	var info []byte
-	if n.b.Info != nil {
-		info = n.b.Info.Segment.Value(src)
+	if n.Info != nil {
+		info = n.Info.Segment.Value(src)
 	}
 
 	attrtp := attributes.AttributesOwnerCodeBlockCustom
@@ -101,8 +97,7 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
 		attrtp = attributes.AttributesOwnerCodeBlockChroma
 	}
 
-	// IsDefaultCodeBlockRendererProvider
-	attrs, attrStr, err := getAttributes(n.b, info)
+	attrs, attrStr, err := getAttributes(n, info)
 	if err != nil {
 		return ast.WalkStop, &herrors.TextSegmentError{Err: err, Segment: attrStr}
 	}
@@ -152,6 +147,8 @@ func (r *htmlRenderer) getPageInner(rctx *render.Context) any {
 	return rctx.DocumentContext().Document
 }
 
+var _ hooks.PositionerSourceTargetProvider = (*codeBlockContext)(nil)
+
 type codeBlockContext struct {
 	page      any
 	pageInner any
@@ -160,7 +157,7 @@ type codeBlockContext struct {
 	ordinal   int
 
 	// This is only used in error situations and is expensive to create,
-	// to delay creation until needed.
+	// so delay creation until needed.
 	pos       htext.Position
 	posInit   sync.Once
 	createPos func() htext.Position
@@ -195,6 +192,11 @@ func (c *codeBlockContext) Position() htext.Position {
 	return c.pos
 }
 
+// For internal use.
+func (c *codeBlockContext) PositionerSourceTarget() []byte {
+	return []byte(c.code)
+}
+
 func getLang(node *ast.FencedCodeBlock, src []byte) string {
 	langWithAttributes := string(node.Language(src))
 	lang, _, _ := strings.Cut(langWithAttributes, "{")
diff --git a/markup/goldmark/codeblocks/transform.go b/markup/goldmark/codeblocks/transform.go
deleted file mode 100644
index 829dfcc190d..00000000000
--- a/markup/goldmark/codeblocks/transform.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package codeblocks
-
-import (
-	"github.com/yuin/goldmark/ast"
-	"github.com/yuin/goldmark/parser"
-	"github.com/yuin/goldmark/text"
-)
-
-// KindCodeBlock 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 1c0d228edfb..357be732860 100644
--- a/markup/goldmark/convert.go
+++ b/markup/goldmark/convert.go
@@ -18,15 +18,15 @@ import (
 	"bytes"
 
 	"github.com/gohugoio/hugo-goldmark-extensions/extras"
-	"github.com/gohugoio/hugo-goldmark-extensions/passthrough"
-	"github.com/gohugoio/hugo/markup/goldmark/hugocontext"
-	"github.com/yuin/goldmark/util"
-
+	"github.com/gohugoio/hugo/markup/goldmark/blockquotes"
 	"github.com/gohugoio/hugo/markup/goldmark/codeblocks"
 	"github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
+	"github.com/gohugoio/hugo/markup/goldmark/hugocontext"
 	"github.com/gohugoio/hugo/markup/goldmark/images"
 	"github.com/gohugoio/hugo/markup/goldmark/internal/extensions/attributes"
 	"github.com/gohugoio/hugo/markup/goldmark/internal/render"
+	"github.com/gohugoio/hugo/markup/goldmark/passthrough"
+	"github.com/yuin/goldmark/util"
 
 	"github.com/yuin/goldmark"
 	emoji "github.com/yuin/goldmark-emoji"
@@ -108,6 +108,7 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
 			hugocontext.New(),
 			newLinks(cfg),
 			newTocExtension(tocRendererOptions),
+			blockquotes.New(),
 		}
 		parserOptions []parser.Option
 	)
@@ -177,32 +178,7 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
 	}
 
 	if cfg.Extensions.Passthrough.Enable {
-		configuredInlines := cfg.Extensions.Passthrough.Delimiters.Inline
-		configuredBlocks := cfg.Extensions.Passthrough.Delimiters.Block
-
-		inlineDelimiters := make([]passthrough.Delimiters, len(configuredInlines))
-		blockDelimiters := make([]passthrough.Delimiters, len(configuredBlocks))
-
-		for i, d := range configuredInlines {
-			inlineDelimiters[i] = passthrough.Delimiters{
-				Open:  d[0],
-				Close: d[1],
-			}
-		}
-
-		for i, d := range configuredBlocks {
-			blockDelimiters[i] = passthrough.Delimiters{
-				Open:  d[0],
-				Close: d[1],
-			}
-		}
-
-		extensions = append(extensions, passthrough.New(
-			passthrough.Config{
-				InlineDelimiters: inlineDelimiters,
-				BlockDelimiters:  blockDelimiters,
-			},
-		))
+		extensions = append(extensions, passthrough.New(cfg.Extensions.Passthrough))
 	}
 
 	if pcfg.Conf.EnableEmoji() {
diff --git a/markup/goldmark/internal/render/context.go b/markup/goldmark/internal/render/context.go
index 306d26748b1..712e1f0531b 100644
--- a/markup/goldmark/internal/render/context.go
+++ b/markup/goldmark/internal/render/context.go
@@ -18,6 +18,7 @@ import (
 	"math/bits"
 
 	"github.com/gohugoio/hugo/markup/converter"
+	"github.com/yuin/goldmark/ast"
 )
 
 type BufWriter struct {
@@ -40,9 +41,19 @@ func (b *BufWriter) Flush() error {
 
 type Context struct {
 	*BufWriter
+	ContextData
 	positions []int
 	pids      []uint64
-	ContextData
+	ordinals  map[ast.NodeKind]int
+}
+
+func (ctx *Context) GetAndIncrementOrdinal(kind ast.NodeKind) int {
+	if ctx.ordinals == nil {
+		ctx.ordinals = make(map[ast.NodeKind]int)
+	}
+	i := ctx.ordinals[kind]
+	ctx.ordinals[kind]++
+	return i
 }
 
 func (ctx *Context) PushPos(n int) {
diff --git a/markup/goldmark/passthrough/passthrough.go b/markup/goldmark/passthrough/passthrough.go
new file mode 100644
index 00000000000..aafb1544b94
--- /dev/null
+++ b/markup/goldmark/passthrough/passthrough.go
@@ -0,0 +1,226 @@
+// Copyright 2024 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 passthrough
+
+import (
+	"bytes"
+	"sync"
+
+	htext "github.com/gohugoio/hugo/common/text"
+
+	"github.com/gohugoio/hugo-goldmark-extensions/passthrough"
+	"github.com/gohugoio/hugo/markup/converter/hooks"
+	"github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
+	"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/renderer"
+	"github.com/yuin/goldmark/util"
+)
+
+func New(cfg goldmark_config.Passthrough) goldmark.Extender {
+	if !cfg.Enable {
+		return nil
+	}
+	return &passthroughExtension{cfg: cfg}
+}
+
+type (
+	passthroughExtension struct {
+		cfg goldmark_config.Passthrough
+	}
+	htmlRenderer struct{}
+)
+
+func (e *passthroughExtension) Extend(m goldmark.Markdown) {
+	configuredInlines := e.cfg.Delimiters.Inline
+	configuredBlocks := e.cfg.Delimiters.Block
+
+	inlineDelimiters := make([]passthrough.Delimiters, len(configuredInlines))
+	blockDelimiters := make([]passthrough.Delimiters, len(configuredBlocks))
+
+	for i, d := range configuredInlines {
+		inlineDelimiters[i] = passthrough.Delimiters{
+			Open:  d[0],
+			Close: d[1],
+		}
+	}
+
+	for i, d := range configuredBlocks {
+		blockDelimiters[i] = passthrough.Delimiters{
+			Open:  d[0],
+			Close: d[1],
+		}
+	}
+
+	pse := passthrough.New(
+		passthrough.Config{
+			InlineDelimiters: inlineDelimiters,
+			BlockDelimiters:  blockDelimiters,
+		},
+	)
+
+	pse.Extend(m)
+
+	// Set up render hooks if configured.
+	// Upstream passthrough inline = 101
+	// Upstream passthrough block = 99
+	m.Renderer().AddOptions(renderer.WithNodeRenderers(
+		util.Prioritized(newHTMLRenderer(), 90),
+	))
+}
+
+func newHTMLRenderer() renderer.NodeRenderer {
+	r := &htmlRenderer{}
+	return r
+}
+
+func (r *htmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+	reg.Register(passthrough.KindPassthroughBlock, r.renderPassthroughBlock)
+	reg.Register(passthrough.KindPassthroughInline, r.renderPassthroughBlock)
+}
+
+func (r *htmlRenderer) renderPassthroughBlock(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	ctx := w.(*render.Context)
+
+	if entering {
+		return ast.WalkContinue, nil
+	}
+
+	var (
+		s      string
+		typ    string
+		delims *passthrough.Delimiters
+	)
+
+	switch nn := node.(type) {
+	case *passthrough.PassthroughInline:
+		s = string(nn.Text(src))
+		typ = "inline"
+		delims = nn.Delimiters
+	case (*passthrough.PassthroughBlock):
+		l := nn.Lines().Len()
+		var buff bytes.Buffer
+		for i := 0; i < l; i++ {
+			line := nn.Lines().At(i)
+			buff.Write(line.Value(src))
+		}
+		s = buff.String()
+		typ = "block"
+		delims = nn.Delimiters
+	}
+
+	renderer := ctx.RenderContext().GetRenderer(hooks.PassthroughRendererType, typ)
+	if renderer == nil {
+		// Write the raw content if no renderer is found.
+		ctx.WriteString(s)
+		return ast.WalkContinue, nil
+	}
+
+	// Inline and block passthroughs share the same ordinal counter.
+	ordinal := ctx.GetAndIncrementOrdinal(passthrough.KindPassthroughBlock)
+
+	// Trim the delimiters.
+	s = s[len(delims.Open) : len(s)-len(delims.Close)]
+
+	pctx := &passthroughContext{
+		ordinal:          ordinal,
+		page:             ctx.DocumentContext().Document,
+		pageInner:        r.getPageInner(ctx),
+		inner:            s,
+		typ:              typ,
+		AttributesHolder: attributes.New(node.Attributes(), attributes.AttributesOwnerGeneral),
+	}
+
+	pctx.createPos = func() htext.Position {
+		if resolver, ok := renderer.(hooks.ElementPositionResolver); ok {
+			return resolver.ResolvePosition(pctx)
+		}
+		return htext.Position{
+			Filename:     ctx.DocumentContext().Filename,
+			LineNumber:   1,
+			ColumnNumber: 1,
+		}
+	}
+
+	pr := renderer.(hooks.PassthroughRenderer)
+
+	if err := pr.RenderPassthrough(ctx.RenderContext().Ctx, w, pctx); err != nil {
+		return ast.WalkStop, err
+	}
+
+	return ast.WalkContinue, nil
+}
+
+func (r *htmlRenderer) getPageInner(rctx *render.Context) any {
+	pid := rctx.PeekPid()
+	if pid > 0 {
+		if lookup := rctx.DocumentContext().DocumentLookup; lookup != nil {
+			if v := rctx.DocumentContext().DocumentLookup(pid); v != nil {
+				return v
+			}
+		}
+	}
+	return rctx.DocumentContext().Document
+}
+
+type passthroughContext struct {
+	page      any
+	pageInner any
+	typ       string // inner or block
+	inner     string
+	ordinal   int
+
+	// This is only used in error situations and is expensive to create,
+	// so delay creation until needed.
+	pos       htext.Position
+	posInit   sync.Once
+	createPos func() htext.Position
+	*attributes.AttributesHolder
+}
+
+func (p *passthroughContext) Page() any {
+	return p.page
+}
+
+func (p *passthroughContext) PageInner() any {
+	return p.pageInner
+}
+
+func (p *passthroughContext) Type() string {
+	return p.typ
+}
+
+func (p *passthroughContext) Inner() string {
+	return p.inner
+}
+
+func (p *passthroughContext) Ordinal() int {
+	return p.ordinal
+}
+
+func (p *passthroughContext) Position() htext.Position {
+	p.posInit.Do(func() {
+		p.pos = p.createPos()
+	})
+	return p.pos
+}
+
+// For internal use.
+func (p *passthroughContext) PositionerSourceTarget() []byte {
+	return []byte(p.inner)
+}
+
+var _ hooks.PositionerSourceTargetProvider = (*passthroughContext)(nil)


Test patch:
diff --git a/markup/goldmark/blockquotes/blockquotes_integration_test.go b/markup/goldmark/blockquotes/blockquotes_integration_test.go
new file mode 100644
index 00000000000..45c7caa0fc2
--- /dev/null
+++ b/markup/goldmark/blockquotes/blockquotes_integration_test.go
@@ -0,0 +1,83 @@
+// Copyright 2024 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 blockquotes_test
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/gohugoio/hugo/hugolib"
+)
+
+func TestBlockquoteHook(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+[markup]
+  [markup.goldmark]
+    [markup.goldmark.parser]
+      [markup.goldmark.parser.attribute]
+        block = true
+        title = true
+-- layouts/_default/_markup/render-blockquote.html --
+Blockquote: |{{ .Text | safeHTML }}|{{ .Type }}|
+-- layouts/_default/_markup/render-blockquote-alert.html --
+{{ $text := .Text | safeHTML }}
+Blockquote Alert: |{{ $text }}|{{ .Type }}|
+Blockquote Alert Attributes: |{{ $text }}|{{ .Attributes }}|
+Blockquote Alert Page: |{{ $text }}|{{ .Page.Title }}|{{ .PageInner.Title }}|
+{{ if .Attributes.showpos }}
+Blockquote Alert Position: |{{ $text }}|{{ .Position | safeHTML }}|
+{{ end }}
+-- layouts/_default/single.html --
+Content: {{ .Content }}
+-- content/p1.md --
+---
+title: "p1"
+---
+
+> [!NOTE]  
+> This is a note with some whitespace after the alert type.
+
+
+> [!TIP]
+> This is a tip.
+
+>   [!CAUTION]
+> This is a caution with some whitespace before the alert type.
+
+> A regular blockquote.
+
+> [!TIP]
+> This is a tip with attributes.
+{class="foo bar" id="baz"}
+
+> [!NOTE]  
+> Note triggering showing the position.
+{showpos="true"}
+
+`
+
+	b := hugolib.Test(t, files)
+	b.AssertFileContent("public/p1/index.html",
+		"Blockquote Alert: |<p>This is a note with some whitespace after the alert type.</p>\n|alert|",
+		"Blockquote Alert: |<p>This is a tip.</p>",
+		"Blockquote Alert: |<p>This is a caution with some whitespace before the alert type.</p>\n|alert|",
+		"Blockquote: |<p>A regular blockquote.</p>\n|regular|",
+		"Blockquote Alert Attributes: |<p>This is a tip with attributes.</p>\n|map[class:foo bar id:baz]|",
+		filepath.FromSlash("/content/p1.md:20:3"),
+		"Blockquote Alert Page: |<p>This is a tip with attributes.</p>\n|p1|p1|",
+	)
+}
diff --git a/markup/goldmark/blockquotes/blockquotes_test.go b/markup/goldmark/blockquotes/blockquotes_test.go
new file mode 100644
index 00000000000..5b2680a0dbd
--- /dev/null
+++ b/markup/goldmark/blockquotes/blockquotes_test.go
@@ -0,0 +1,60 @@
+// Copyright 2024 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 blockquotes
+
+import (
+	"testing"
+
+	qt "github.com/frankban/quicktest"
+)
+
+func TestResolveGitHubAlert(t *testing.T) {
+	t.Parallel()
+
+	c := qt.New(t)
+
+	tests := []struct {
+		input    string
+		expected string
+	}{
+		{
+			input:    "[!NOTE]",
+			expected: "note",
+		},
+		{
+			input:    "[!WARNING]",
+			expected: "warning",
+		},
+		{
+			input:    "[!TIP]",
+			expected: "tip",
+		},
+		{
+			input:    "[!IMPORTANT]",
+			expected: "important",
+		},
+		{
+			input:    "[!CAUTION]",
+			expected: "caution",
+		},
+		{
+			input:    "[!FOO]",
+			expected: "",
+		},
+	}
+
+	for _, test := range tests {
+		c.Assert(resolveGitHubAlert("<p>"+test.input), qt.Equals, test.expected)
+	}
+}
diff --git a/markup/goldmark/passthrough/passthrough_integration_test.go b/markup/goldmark/passthrough/passthrough_integration_test.go
new file mode 100644
index 00000000000..2d51c596180
--- /dev/null
+++ b/markup/goldmark/passthrough/passthrough_integration_test.go
@@ -0,0 +1,62 @@
+// Copyright 2024 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 passthrough_test
+
+import (
+	"testing"
+
+	"github.com/gohugoio/hugo/hugolib"
+)
+
+func TestPassthroughRenderHook(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- config.toml --
+[markup.goldmark.extensions.passthrough]
+enable = true
+[markup.goldmark.extensions.passthrough.delimiters]
+block = [['$$', '$$']]
+inline = [['$', '$']]
+-- content/p1.md --
+---
+title: "p1"
+---
+## LaTeX test
+
+Some inline LaTeX 1: $a^*=x-b^*$.
+
+Block equation that would be mangled by default parser:
+
+$$a^*=x-b^*$$
+
+Some inline LaTeX 2: $a^*=x-b^*$.
+
+-- layouts/_default/single.html --
+{{ .Content }}
+-- layouts/_default/_markup/render-passthrough-block.html --
+Passthrough block: {{ .Inner | safeHTML }}|{{ .Type }}|{{ .Ordinal }}:END
+-- layouts/_default/_markup/render-passthrough-inline.html --
+Passthrough inline: {{ .Inner | safeHTML }}|{{ .Type }}|{{ .Ordinal }}:END
+
+`
+
+	b := hugolib.Test(t, files)
+	b.AssertFileContent("public/p1/index.html", `
+		Some inline LaTeX 1: Passthrough inline: a^*=x-b^*|inline|0:END
+		Passthrough block: a^*=x-b^*|block|1:END
+		Some inline LaTeX 2: Passthrough inline: a^*=x-b^*|inline|2:END
+
+	`)
+}