Shortcodes
Shortcodes are reusable template snippets you can use in Markdown content. Custom shortcodes are Jinja2 templates placed in templates/shortcodes/. See Template Syntax for the templating language reference.
Using Shortcodes
Two syntax patterns work in content files:
{{ shortcode_name(arg1="value", arg2="value") }}
Or explicitly:
{{ shortcode("shortcode_name", arg1="value") }}
Block Shortcode Closers
Block shortcodes support two closer styles:
-
Bare closer (simple):
{% note %}...{% end %} -
Named closer (recommended):
{% note %}...{% endnote %} {% alert(type="info") %}...{% endalert %}
We strongly recommend named closers ({% endNAME %}) for any non-trivial content. They make your Markdown much easier to read and maintain, especially when mixing multiple shortcodes or using deep nesting.
To keep shortcode syntax literal in a Markdown page outside a code fence, wrap
it in a raw block. The {% raw %} and {% endraw %} markers remain visible in
Markdown output; fenced blocks inside the region do not split it.
Built-in Shortcodes
Hwaro ships with built-in shortcodes that work out of the box, with no template files needed.
Pass parameters by name or in the shortcode's documented order. For example,
{{ youtube("VIDEO_ID") }} and {{ gist("username", "gist_id") }} use
positional arguments; quoted empty values still occupy their position.
youtube
Embed a YouTube video.
{{ youtube(id="dQw4w9WgXcQ") }}
{{ youtube(id="dQw4w9WgXcQ", width="800", height="450") }}
{{ youtube(id="dQw4w9WgXcQ", start="30") }}
| Param | Default | Description |
|---|---|---|
id |
(required) | YouTube video ID |
width |
560 |
Player width |
height |
315 |
Player height |
title |
YouTube Video |
Accessible title |
start |
Start playback at a given timestamp in seconds |
vimeo
Embed a Vimeo video.
{{ vimeo(id="123456789") }}
| Param | Default | Description |
|---|---|---|
id |
(required) | Vimeo video ID |
width |
560 |
Player width |
height |
315 |
Player height |
title |
Vimeo Video |
Accessible title |
gist
Embed a GitHub Gist.
{{ gist(user="octocat", id="abc123") }}
{{ gist(user="octocat", id="abc123", file="hello.rb") }}
| Param | Default | Description |
|---|---|---|
user |
(required) | GitHub username |
id |
(required) | Gist ID |
file |
(none) | Specific file to show |
alert / callout
Display an alert box. Use as a block shortcode to wrap content.
Both bare and named closers are supported (named closers are recommended for complex pages):
{% alert(type="warning", title="Caution") %}Be careful with this!{% end %}
{% alert(type="tip") %}Using named closer also works{% endalert %}
(We recommend the named closer style for clarity.)
| Param | Default | Description |
|---|---|---|
type |
info |
info, warning, danger, tip, success |
title |
(none) | Optional title |
figure
Image with optional caption.
{{ figure(src="/img/photo.jpg", alt="A photo", caption="My caption") }}
| Param | Default | Description |
|---|---|---|
src |
(required) | Image URL |
alt |
"" |
Alt text |
caption |
(none) | Caption below image |
width |
(none) | Image width |
height |
(none) | Image height |
tweet
Embed a tweet.
{{ tweet(user="jack", id="20") }}
| Param | Default | Description |
|---|---|---|
user |
(required) | Twitter username |
id |
(required) | Tweet ID |
codepen
Embed a CodePen.
{{ codepen(user="chriscoyier", id="gfdDu") }}
{{ codepen(user="chriscoyier", id="gfdDu", tab="css,result", height="400") }}
| Param | Default | Description |
|---|---|---|
user |
(required) | CodePen username |
id |
(required) | Pen ID |
tab |
result |
Default tab(s) |
height |
300 |
Embed height |
title |
CodePen Embed |
Accessible title |
Every parameter marked (required) above lands directly in a URL or an
src. Omitting one still renders — into an embed that points nowhere
(https://codepen.io//embed/gfdDu) — so the build warns instead of leaving you
to find the dead embed on the published page:
[WARN] Shortcode `codepen` is missing required argument `user` — it renders a broken embed. Usage: `{{ codepen(user="…", id="…") }}`.
include_code
Include a source file, or part of it, as a fenced code block. The block goes through the normal code pipeline: highlighting, fence options, the copy button and render-codeblock hooks.
{{ include_code(path="examples/site/config.toml") }}
{{ include_code(path="examples/site/config.toml", region="feeds", lines="2-8", title="config.toml", hl_lines="3") }}
| Param | Default | Description |
|---|---|---|
path |
(required) | File path, relative to the project root |
region |
(none) | Only the lines between #region <name> and #endregion <name> |
lines |
(none) | Line range a-b or a single line a (1-based, inclusive; counted inside region when both are set) |
lang |
from the file name or extension (Makefile, Dockerfile, .rs); in client highlight mode, as highlight.js names it |
Language of the code block |
title, hl_lines, hide_lines, linenos, linenostart, copy |
Passed on as fence options |
Region markers can follow any comment leader, so they stay valid code in the source file:
module App
# #region server
def run
serve
end
# #endregion server
end
//, #, --, <!--, /* and ; all work, and a bare #endregion closes the innermost open region. The markers, and any nested markers inside the selected region, are left out of the output. Without region, the file is included as it is. Common leading indentation is removed.
include_md
Render a Markdown file in place, as if its text were written at that spot.
{{ include_md(path="shared/install.md") }}
{{ include_md(path="shared/install.md", region="brew") }}
| Param | Default | Description |
|---|---|---|
path |
(required) | File path, relative to the project root |
region |
(none) | Only the lines between <!-- #region <name> --> and <!-- #endregion <name> --> |
Shortcodes inside the included file expand, and its headings take part in the page's table of contents and heading ids. Its front matter is dropped. An included file can include others, up to 8 levels deep. A cycle is an error that shows the chain.
Paths and errors
Both shortcodes read files under the project root only. Absolute paths, .., symlinks that resolve outside the project, the build output directory and .hwaro/ are refused. A missing file or region, a line range outside the file, or a refused path fails the build with an error that names the page and the file:
Error [HWARO_E_TEMPLATE]: {{ include_code(path="examples/app.cr", region="srv") }} in content/guide.md: region 'srv' not found
An included file is a dependency of the page that includes it. A warm --cache build and hwaro serve both re-render the page when the file changes, including files outside content/.
A call inside a code span, a fenced code block or a {% raw %} region stays literal.
To override any built-in shortcode, create a file with the same name in
templates/shortcodes/(e.g.,templates/shortcodes/youtube.html). User templates always take priority — and define their own contract, so the warning above does not apply to them.
Creating Custom Shortcodes
Shortcode templates live in templates/shortcodes/.
A shortcode template that fails while rendering (for example, an undefined
attribute access or an {% include %} of a missing template) fails the build
with HWARO_E_TEMPLATE (exit code 4), just like a page template. The error
names the shortcode file and line, such as templates/shortcodes/alert.html:3:5.
Under hwaro serve it appears in the error overlay instead.
A syntax error in a shortcode template is reported as a warning with the same file and location. Hwaro places a visible HTML comment at the call and keeps rendering the page.
Example: Alert Box
Create templates/shortcodes/alert.html:
{% if type and message %}
<div class="alert alert-{{ type }}">
{{ message | safe }}
</div>
{% endif %}
Use in content:
{{ alert(type="warning", message="This is important!") }}
Output:
<div class="alert alert-warning">
This is important!
</div>
Example: YouTube Embed
Create templates/shortcodes/youtube.html:
{% if id %}
<div class="video-container">
<iframe
src="https://www.youtube.com/embed/{{ id }}"
frameborder="0"
allowfullscreen>
</iframe>
</div>
{% endif %}
Use in content:
{{ youtube(id="dQw4w9WgXcQ") }}
Example: Figure with Caption
Create templates/shortcodes/figure.html:
<figure>
<img src="{{ src }}" alt="{{ alt | default(value='') }}">
{% if caption %}
<figcaption>{{ caption }}</figcaption>
{% endif %}
</figure>
Use in content:
{{ figure(src="/images/photo.jpg", alt="A photo", caption="My caption") }}
Example: Image Gallery (Asset Colocation)
You can create a gallery that automatically lists images found in the same directory as the page (Page Bundle).
Create templates/shortcodes/gallery.html:
<div class="gallery">
{% for asset in page.assets -%}
{%- if asset is matching("[.](jpg|png)$") -%}
{% set image = resize_image(path=asset, width=240, height=180) %}
<a href="{{ get_url(path=asset) }}" target="_blank">
<img src="{{ image.url }}" alt="{{ asset }}" />
</a>
{%- endif %}
{%- endfor %}
</div>
Use in content (inside a Page Bundle directory):
{{ gallery() }}
This will render a grid of all JPG and PNG images found alongside the Markdown file.
Block Shortcodes
Block shortcodes wrap content between opening and closing tags:
{% note() %}
This is the **body** content of the shortcode.
{% end %}
The body is passed to the shortcode template as the body variable. Markdown conversion is not applied automatically, so use the markdownify filter in your template if needed:
<div class="note">
{{ body | markdownify | safe }}
</div>
Or use the body as-is for raw content:
<div class="note">{{ body }}</div>
Nested Shortcodes
Block shortcodes can be nested up to 5 levels deep:
{% outer() %}
Some text with {{ inner(type="info") }} inside.
{% end %}
Argument Syntax
Named Arguments
Arguments support multiple quote styles:
{{ alert(type="warning", message="Double quotes") }}
{{ alert(type='info', message='Single quotes') }}
{{ alert(type=danger, message=Unquoted) }}
An unquoted value ends at the first space (message=No quotes here passes just No), so quote anything longer than one word.
Positional Arguments
When no key=value syntax is used, arguments are assigned as _0, _1, etc.:
{{ youtube("dQw4w9WgXcQ") }}
In the shortcode template, access via {{ _0 }}:
<iframe src="https://www.youtube.com/embed/{{ _0 }}"></iframe>
Built-in Variables
Shortcodes have access to:
- All passed arguments
siteobjectpageobject- Standard filters and functions
{# In shortcode template #}
<a href="{{ site.base_url }}/{{ url }}">{{ text }}</a>
Tips
Validate Arguments
Always check for required arguments:
{% if not url %}
<p class="error">Missing url parameter</p>
{% else %}
<a href="{{ url }}">{{ text | default(value="Link") }}</a>
{% endif %}
Use Safe Filter for HTML
When passing HTML content:
{{ content | safe }}
Organize Shortcodes
Group related shortcodes:
templates/shortcodes/
├── alert.html
├── youtube.html
├── figure.html
├── code/
│ ├── tabs.html
│ └── snippet.html
└── social/
├── twitter.html
└── github.html
See Also
- Template Syntax — Jinja2 reference for custom shortcodes
- Functions — Built-in template functions