Safe HTML with HTML::Blitz::Builder

Lukas Mai

2026-03-17

Bad, unsafe, terrible

my $html = "";
$html .= "<p>Hello, $user!</p>\n";
$html .= "<ul>\n";
for my $item (@list) {
    $html .= "<li>$item</li>\n";
}
$html .= "</ul>\n";
print $html;

Good, safe, lovely

use HTML::Blitz::Builder qw(to_html mk_elem);

my @html;
push @html, mk_elem('p', "Hello, $user!");
my @list;
for my $item (@list) {
    push @list, mk_elem('li', $item);
}
push @html, mk_elem('ul', @list);
print to_html(@html);

Example 1: text

use v5.36;
use HTML::Blitz::Builder qw(to_html mk_elem);

say to_html("I <3 HTML!");
I &lt;3 HTML!

Example 2: nested elements

use v5.36;
use HTML::Blitz::Builder qw(to_html mk_elem);

say to_html(
    mk_elem('p',
        "Everything is possible at ",
        mk_elem('a', { href => 'https://zombo.com/' }, "Zombocom"),
    ),
);
<p>Everything is possible at <a href=https://zombo.com/>Zombocom</a></p>

Example 3: void elements

use v5.36;
use HTML::Blitz::Builder qw(to_html mk_elem);

say to_html(
    mk_elem('p', "Hello"),
    mk_elem('hr'),
    mk_elem('img', {
        src => 'kitten.jpg',
        alt => "photo of a cute kitten",
    }),
    mk_elem('script', { src => 'main.js' }),
);
<p>Hello</p><hr><img alt="photo of a cute kitten" src=kitten.jpg><script src=main.js></script>

Example 4: non-elements

use v5.36;
use HTML::Blitz::Builder qw(to_html mk_elem mk_doctype mk_comment);

say to_html(
    mk_doctype,
    mk_comment("yay"),
    "etc",
);
<!DOCTYPE html><!--yay-->etc

Example 5: non-representable comment

use v5.36;
use HTML::Blitz::Builder qw(to_html mk_elem mk_doctype mk_comment);

say to_html(
    mk_doctype,
    mk_comment("move A --> B"),
    "etc",
);
HTML comment cannot contain '-->': 'move A --> B' at /tmp/slide-snippet-RPOK15 line 4.

Example 6: raw script

use v5.36;
use HTML::Blitz::Builder qw(to_html mk_elem);

say to_html(
    mk_elem('script', <<~'_SCRIPT_'),
        if (a < b && c > d) {
            foo();
        }
        _SCRIPT_
);
<script>if (a < b && c > d) {
    foo();
}
</script>

Example 7: non-representable script

use v5.36;
use HTML::Blitz::Builder qw(to_html mk_elem);

say to_html(
    mk_elem('script', <<~'_SCRIPT_'),
        const foo = "</SCRIPT>";
        _SCRIPT_
);
<script> tag cannot contain '</SCRIPT>' at /tmp/slide-snippet-d4AKNb line 4.

Example 8: bizarre script

use v5.36;
use HTML::Blitz::Builder qw(to_html mk_elem);

say to_html(
    mk_elem('script', <<~'_SCRIPT_'),
        /* <!-- <script> */
        const foo = "</SCRIPT>";
        _SCRIPT_
);
<script>/* <!-- <script> */
const foo = "</SCRIPT>";
</script>

Thank you!