# How to Implement Custom Post‑Renderers for HTML Output in Quarkdown

> Learn to implement custom post renderers for HTML output in Quarkdown. Extend the PostRenderer interface, provide custom resources, and register using a RendererFactory for tailored results.

- Repository: [Giorgio Garofalo/quarkdown](https://github.com/iamgio/quarkdown)
- Tags: how-to-guide
- Published: 2026-04-29

---

**To implement custom post‑renderers for HTML output in Quarkdown, create a class that implements the `PostRenderer` interface (or delegates to `HtmlPostRenderer`), supply custom `PostRendererResource` implementations for additional assets, and register the renderer via a `RendererFactory` extension.**

Quarkdown separates **rendering** (converting the AST to plain HTML) from **post‑rendering** (wrapping that HTML in a complete document structure and aggregating auxiliary resources). This architecture, defined in the `iamgio/quarkdown` repository, allows you to customize the final HTML output without modifying the core rendering logic. By extending the post‑renderer pipeline, you can inject custom meta tags, add footer fragments, bundle additional scripts, or modify the document wrapper while retaining Quarkdown’s built‑in media handling and theming capabilities.

## Understanding the Post‑Rendering Architecture

The post‑rendering stage is governed by the `PostRenderer` interface located in [`quarkdown-core/src/main/kotlin/com/quarkdown/core/rendering/PostRenderer.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-core/src/main/kotlin/com/quarkdown/core/rendering/PostRenderer.kt). This contract defines three core operations: `wrap()` (envelops rendered HTML in a full document), `generateResources()` (produces auxiliary output files), and `wrapResources()` (groups resources for final output).

Quarkdown provides two primary implementations in the `quarkdown-html` module:

- **`HtmlOnlyPostRenderer`** ([`quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/HtmlOnlyPostRenderer.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/HtmlOnlyPostRenderer.kt)): A minimal wrapper that generates only the main [`index.html`](https://github.com/iamgio/quarkdown/blob/main/index.html) without extra assets.
- **`HtmlPostRenderer`** ([`quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/HtmlPostRenderer.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/HtmlPostRenderer.kt)): The full‑featured default that delegates document wrapping to `HtmlOnlyPostRenderer` and aggregates a configurable set of `PostRendererResource` objects (themes, scripts, static assets).

The `PostRendererResource` interface ([`quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/resources/PostRendererResource.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/resources/PostRendererResource.kt)) defines pluggable resources that know how to copy files or generate content. These resources are automatically collected by `HtmlPostRenderer` during the `generateResources()` phase.

Finally, the `RenderingComponents` class ([`quarkdown-core/src/main/kotlin/com/quarkdown/core/rendering/RenderingComponents.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-core/src/main/kotlin/com/quarkdown/core/rendering/RenderingComponents.kt)) bundles the node renderer (AST → HTML) with the post‑renderer, created by the factory extension in [`HtmlRendererExtension.kt`](https://github.com/iamgio/quarkdown/blob/main/HtmlRendererExtension.kt).

## Creating a Custom Post‑Renderer Resource

To inject additional files into the output directory—such as a custom footer HTML snippet—implement the `PostRendererResource` sealed interface. Each resource receives a mutable `Set<OutputResource>` collector and the rendered HTML content, allowing conditional generation based on the document.

Here is an implementation that generates a static footer file:

```kotlin
// src/main/kotlin/com/example/quarkdown/CustomFooterResource.kt
package com.example.quarkdown

import com.quarkdown.rendering.html.post.resources.PostRendererResource
import com.quarkdown.core.pipeline.output.OutputResource
import com.quarkdown.core.pipeline.output.TextOutputArtifact
import com.quarkdown.core.pipeline.output.ArtifactType

class CustomFooterResource : PostRendererResource {
    // Include this resource even in preview mode
    override val runsInPreviewMode: Boolean = true

    override fun includeTo(
        collector: MutableSet<OutputResource>,
        rendered: CharSequence
    ) {
        val footer = """
            <footer class="qd-custom-footer">
                <p>Generated by a custom Quarkdown post‑renderer.</p>
            </footer>
        """.trimIndent()

        collector += TextOutputArtifact(
            name = "footer.html",
            content = footer,
            type = ArtifactType.HTML
        )
    }
}

```

This resource creates [`footer.html`](https://github.com/iamgio/quarkdown/blob/main/footer.html) in the output directory alongside Quarkdown’s standard assets.

## Implementing the Custom Post‑Renderer

Rather than implementing `PostRenderer` from scratch, the recommended pattern is **delegation**. Create a class that delegates to `HtmlPostRenderer` but overrides the `resourcesProvider` lambda to include your custom resources.

The following example wraps the standard behavior and adds `CustomFooterResource` to the resource set:

```kotlin
// src/main/kotlin/com/example/quarkdown/CustomHtmlPostRenderer.kt
package com.example.quarkdown

import com.quarkdown.core.context.Context
import com.quarkdown.rendering.html.post.HtmlOnlyPostRenderer
import com.quarkdown.rendering.html.post.HtmlPostRenderer
import com.quarkdown.core.rendering.PostRenderer
import com.quarkdown.rendering.html.post.resources.PostRendererResource

class CustomHtmlPostRenderer(
    context: Context,
    resourcesLayout: com.quarkdown.installlayout.InstallLayout.Html? = null,
    relativePathToRoot: String = "."
) : PostRenderer by HtmlPostRenderer(
    context = context,
    resourcesLayout = resourcesLayout,
    relativePathToRoot = relativePathToRoot,
    base = HtmlOnlyPostRenderer(context, relativePathToRoot = relativePathToRoot),
    resourcesProvider = {
        // Retain default resources and append custom ones
        HtmlPostRenderer(context, resourcesLayout, relativePathToRoot).resourcesProvider()
            .plus(CustomFooterResource())
    }
)

```

**Key implementation details:**
- The `base` parameter receives `HtmlOnlyPostRenderer` to handle standard document wrapping.
- The `resourcesProvider` lambda returns the union of default resources and your custom implementations.
- By using Kotlin delegation (`by`), all other `PostRenderer` methods (`wrap`, `generateResources`, `wrapResources`) retain their default behavior.

## Wiring the Renderer into the Pipeline

To activate your custom post‑renderer, extend `RendererFactory` with a new factory function that returns `RenderingComponents` configured with your implementation. This integrates your renderer into the Quarkdown pipeline while preserving the standard AST-to-HTML node renderer.

```kotlin
// src/main/kotlin/com/example/quarkdown/Extension.kt
package com.example.quarkdown

import com.quarkdown.core.context.Context
import com.quarkdown.core.rendering.RenderingComponents
import com.quarkdown.core.flavor.RendererFactory
import com.quarkdown.rendering.html.HtmlExportOptions

/**
 * Registers a custom HTML renderer using [CustomHtmlPostRenderer].
 */
fun RendererFactory.customHtml(
    context: Context,
    options: HtmlExportOptions = HtmlExportOptions()
) = RenderingComponents(
    // Standard AST visitor for HTML generation
    nodeRenderer = accept(com.quarkdown.rendering.html.HtmlRendererFactoryVisitor(context)),
    // Custom post‑renderer injection
    postRenderer = CustomHtmlPostRenderer(context, options.resourcesLayout)
)

```

If you are building a CLI tool, override the default renderer selection in your pipeline initialization:

```kotlin
// src/main/kotlin/com/example/quarkdown/CLIWrapper.kt
package com.example.quarkdown

import com.quarkdown.cli.PipelineInitialization
import com.quarkdown.core.flavor.RendererFactory

fun main(args: Array<String>) {
    PipelineInitialization(
        rendererProvider = { factory: RendererFactory, ctx -> factory.customHtml(ctx) }
    ).run(args)
}

```

Execute your custom pipeline with Gradle:

```bash
./gradlew :quarkdown-cli:installDist
./quarkdown-cli/build/install/quarkdown-cli/bin/quarkdown-cli \
    --renderer customHtml \
    -i document.qd -o output/

```

The generated [`output/index.html`](https://github.com/iamgio/quarkdown/blob/main/output/index.html) will contain the standard Quarkdown content plus the resources defined in your custom post‑renderer.

## Summary

- **Post‑rendering is pluggable** via the `PostRenderer` interface in `quarkdown-core`, allowing customization of the final HTML document and its auxiliary files.
- **Delegate to existing implementations** like `HtmlOnlyPostRenderer` and `HtmlPostRenderer` to retain standard document structures while extending functionality.
- **Resources are modular**—implement `PostRendererResource` to generate or copy additional files (scripts, styles, HTML fragments) into the output directory.
- **Registration occurs through `RendererFactory` extensions**, which produce `RenderingComponents` pairing your custom post‑renderer with Quarkdown’s standard node renderer.

## Frequently Asked Questions

### How do I modify the HTML `<head>` section when using a custom post‑renderer?

Extend `HtmlOnlyPostRenderer` and override the `wrap()` method. This method receives the rendered body HTML and returns the complete document string, allowing you to inject custom `<meta>` tags, `<link>` elements, or CDN scripts before returning the final HTML. Delegate to `super.wrap()` first to obtain the base document, then use string manipulation or a template engine to insert your elements.

### Can I conditionally include resources based on document content?

Yes. The `PostRendererResource.includeTo()` method receives the fully rendered HTML as a `CharSequence` parameter. You can parse this content—checking for specific CSS classes, data attributes, or Quarkdown-specific markers—and conditionally add resources to the collector set. For example, you might only include a math library script if the rendered content contains LaTeX delimiters.

### What is the difference between `HtmlOnlyPostRenderer` and `HtmlPostRenderer`?

`HtmlOnlyPostRenderer` ([`quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/HtmlOnlyPostRenderer.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/HtmlOnlyPostRenderer.kt)) is a minimal implementation that only wraps the rendered HTML in a basic `<html>` structure and outputs a single [`index.html`](https://github.com/iamgio/quarkdown/blob/main/index.html) file. `HtmlPostRenderer` ([`quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/HtmlPostRenderer.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/HtmlPostRenderer.kt)) extends this functionality by managing a collection of `PostRendererResource` objects, enabling automatic generation of theme files, script bundles, media directories, and other auxiliary assets alongside the main document.

### How do I prevent my custom resource from being included in preview mode?

Set the `runsInPreviewMode` property to `false` in your `PostRendererResource` implementation. When this property is `false`, Quarkdown’s pipeline excludes the resource during live preview operations, reducing overhead while developing. The resource will still be generated during full export builds.