vpub-plus, since version 1.14, supports changing render engines on-the-fly – in runtime – and for this feature to work every rendering engine should comply to one source of truth: an interface.
Render engine should satisfy the following:
// Renderer is the minimal interface every rendering engine must satisfy
type Renderer interface {
// Convert turns source text into HTML (or any other one)
//
// For example, markdown -> HTML
//
// Note: wrap bool is used by vanilla renderer,
// to wrap in <p> tags the text nodes.
// You can ignore it if you so desire.
Convert(input string, wrap bool) string
Name() string
}
It is recommended that you support the image-proxy
feature, which replaces links to images with proxied ones when render engine processes raw input by prepending /image-proxy?url=
to the link.
Example using Blackfriday
Blackfriday is a markdown processor by russross. We will customize it a little bit to add support for image proxy and to make it compatible with the Renderer
interface.
Complying with the interface
All renderers should go to /syntax/renderers
folder. Let’s create blackfriday
folder and place convert.go
in there:
syntax
├── renderer.go -> defines interface for render engines
├── renderer_test.go -> unit tests for renderer.go
└── renderers -> folder where all renderers go
└── blackfriday
└── convert.go
To make our compiler happy, we need to satisfy the interface:
type BlackfridayRenderer struct{}
func (b *BlackfridayRenderer) Convert(text string, wrap bool) string {
return text
}
func (b *BlackfridayRenderer) Name() string {
return "blackfriday"
}
Let’s see how it looks…

Not bad. Ideally we need to actually display some processed markdown in HTML.
Rendering markdown
Now let us actually utilize our renderer:
func (b *BlackfridayRenderer) Convert(text string, wrap bool) string {
return string(blackfriday.Run([]byte(text)))
}

It works! Well, mostly. Our code block is for some reason an inline code instead of a block. That happens because with Blackfriday we also need to pass some extensions to support code blocks. Let’s add some:
func (b *BlackfridayRenderer) Convert(text string, wrap bool) string {
return string(blackfriday.Run(
[]byte(text),
blackfriday.WithExtensions(blackfriday.CommonExtensions),
))
}

Now that is looking good… But it is unsafe. You see, if we place sneaky <script>alert(1);</script>
along side with our markdown it will actually be executed on the client:

This is not a good thing. We don’t want our users running arbitrary JS on other people’s computers! At least from our posts on OUR forum! Let’s sanitize it. We will use bluemonday for that.
Bluemonday itself is a HTML sanitizer to scrub user generated content of XSS. Handy!
func (b *BlackfridayRenderer) Convert(text string, wrap bool) string {
unsafe := blackfriday.Run(
[]byte(text),
blackfriday.WithExtensions(blackfriday.CommonExtensions),
)
safe := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
return string(safe)
}
And now we are finally safe.
Adding support for image proxy
This is a little bit unconventional, but it can be done. Blackfriday itself allows you to pass custom renderers into it and they should comply with the following interface:
type Renderer interface {
RenderNode(w io.Writer, node *Node, entering bool) WalkStatus
RenderHeader(w io.Writer, ast *Node)
RenderFooter(w io.Writer, ast *Node)
}
We only care about RenderNode since this is the main rendering method. Let’s add another struct and implement RenderNode
:
type imageProxyRenderer struct {
blackfriday.Renderer
}
func (r *imageProxyRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
if node.Type == blackfriday.Image && entering {
originalURL := string(node.LinkData.Destination)
escapedURL := url.QueryEscape(originalURL)
node.LinkData.Destination = []byte(fmt.Sprintf("%s%s", syntax.ImageProxyURLBase, escapedURL))
}
return r.Renderer.RenderNode(w, node, entering)
}
Okay. It’s not even connected to our blackfriday instance and there’s already a lot of code. Let’s break it down a little:
- First we check if our current node type is
Blackfriday.Image
and also checking if we are about to process it (entering
bool that is passed into RenderNode)
- If we are, then we need to replace URL to an image. We get its url, escape it and then replacing current node’s
LinkData.Destination
with the proxied one.
- And we are returning
RenderNode
function with the new/old context
TDD taught me to go with small incremental steps, but I feel brave and decided to write a lot of code at the same time. Don’t be like me.
Let’s connect it to our Blackfriday instance!
type BlackfridayRenderer struct{}
func (b *BlackfridayRenderer) Name() string {
return "blackfriday"
}
type imageProxyRenderer struct {
blackfriday.Renderer
}
func (r *imageProxyRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
if node.Type == blackfriday.Image && entering {
originalURL := string(node.LinkData.Destination)
escapedURL := url.QueryEscape(originalURL)
node.LinkData.Destination = []byte(fmt.Sprintf("%s%s", syntax.ImageProxyURLBase, escapedURL))
}
return r.Renderer.RenderNode(w, node, entering)
}
func (b *BlackfridayRenderer) Convert(input string, wrap bool) string {
params := blackfriday.HTMLRendererParameters{
Flags: blackfriday.CommonHTMLFlags,
}
base := blackfriday.NewHTMLRenderer(params)
proxy := &imageProxyRenderer{Renderer: base}
unsafe := blackfriday.Run(
[]byte(input),
blackfriday.WithRenderer(proxy),
blackfriday.WithExtensions(blackfriday.CommonExtensions),
)
safe := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
return string(safe)
}
That’s a lot! But essentially we just passing our custom renderer inside of Blackfriday and running it alongside with other extensions. Does it work? Let’s add an image:

And let’s check if it works now…

And what about proxying?

And it works too!
Registering your engine in the registry
To make it appear on instance settings page you have to “register” it. Navigate to /web/handler/handler.go
, find func New
and add the following:
err := renderRegistry.Register("blackfriday", &customBlackfriday.BlackfridayRenderer{})
if err != nil {
return nil, err
}
And that’s it!