Skip to content

Grav Markdown & Shortcodes — Extras, Custom Shortcodes and Formatting

DodaTech Updated 2026-06-27 7 min read

In this tutorial, you'll learn Grav's Markdown extras and shortcode system — how to enable Markdown Extra features, create custom shortcodes, format content, and extend Markdown with Twig-based shortcodes.

What You'll Learn

  • Grav's Markdown Extra features: tables, footnotes, definition lists, abbreviations
  • How shortcodes work in Grav (WordPress-style shortcodes)
  • Built-in shortcodes: safe-email, random, markdown-fallback
  • Creating custom shortcodes with Twig and plugins
  • Shortcode parameters, nesting, and escaping
  • Combining shortcodes with page content

Why It Matters

Standard Markdown covers bold, italic, links, and images. But real-world content needs more: tables, footnotes, collapsible sections, alerts, and reusable components. In WordPress, you use Gutenberg blocks or shortcode plugins. In Grav, you enable Markdown Extra for extended syntax and build shortcodes for anything the content editors need. Shortcodes let you embed complex functionality (forms, galleries, code samples) with a simple tag like {{< gallery >}}.

Real-World Use

A documentation site needs a consistent "callout" box for warnings, tips, and information. Rather than writing raw HTML every time, the team creates a {{< callout "type" "message" >}} shortcode. Content editors use it throughout pages. When the design of callout boxes changes, only the shortcode template is updated — all 200 callouts update automatically.

Learning Path

flowchart LR
    A["Page Collections"] --> B["Markdown & Shortcodes
← You are here"]:::current B --> C["Twig Filters & Functions"] C --> D["Twig Macros"] D --> E["Twig Inheritance"] E --> F["Twig Debugging"] F --> G["Custom Twig Extensions"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

Markdown Extra

Grav supports Markdown Extra, an extension of standard Markdown with additional syntax. Enable it in user/config/system.yaml:

pages:
    markdown:
        extra: true

Tables

| Feature | Standard Markdown | Markdown Extra |
|---------|------------------|----------------|
| Tables  | No               | Yes            |
| Footnotes | No             | Yes            |
| Definition Lists | No      | Yes            |

Output:

<table>
    <thead>
        <tr>
            <th>Feature</th>
            <th>Standard Markdown</th>
            <th>Markdown Extra</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Tables</td>
            <td>No</td>
            <td>Yes</td>
        </tr>
        <tr>
            <td>Footnotes</td>
            <td>No</td>
            <td>Yes</td>
        </tr>
        <tr>
            <td>Definition Lists</td>
            <td>No</td>
            <td>Yes</td>
        </tr>
    </tbody>
</table>

Footnotes

Grav is a flat-file CMS[^1].

[^1]: A flat-file CMS stores content as files instead of a database.

The footnote appears at the bottom of the page as a linked reference.

Definition Lists

Grav CMS
:   A flat-file content management system
:   Uses Markdown files instead of a database

Twig
:   The templating engine used by Grav
:   Based on Symfony's Twig component

Output:

<dl>
    <dt>Grav CMS</dt>
    <dd>A flat-file content management system</dd>
    <dd>Uses Markdown files instead of a database</dd>
    <dt>Twig</dt>
    <dd>The templating engine used by Grav</dd>
    <dd>Based on Symfony's Twig component</dd>
</dl>

Abbreviations

*[CMS]: Content Management System
*[PHP]: PHP: Hypertext Preprocessor

Grav is a flat-file CMS written in PHP.

Shortcodes

Shortcodes are Grav's equivalent of WordPress shortcodes — reusable content snippets with parameters.

Built-in Shortcodes

safe-email:

{{< safe-email "user@example.com" >}}

Renders the email address in a way that is obfuscated from spam bots but readable to users.

random:

Random number: {{< random 100 999 >}}

Outputs a random number between 100 and 999 on each page load.

markdown-fallback:

{{< markdown-fallback >}}
**Bold text** inside a shortcode
{{< /markdown-fallback >}}

Forces Markdown processing inside shortcodes that normally escape content.

Enabling Shortcodes

Shortcodes require the shortcode-core plugin:

bin/gpm install shortcode-core

After installation, configure in user/config/plugins/shortcode-core.yaml:

enabled: true
active: true
active_admin: true
admin_pages: true
parser: regular
fontawesome:
    load: false

Custom Shortcodes with Twig

Shortcode templates live in user/themes/YOURTHEME/shortcodes/. Each shortcode is a Twig file.

Simple Shortcode

Create user/themes/mytheme/shortcodes/notice.twig:

<div class="notice notice-{{ type|default('info') }}">
    <strong>{{ type|default('Info') }}:</strong> {{ content|raw }}
</div>

Usage in Markdown:

{{< notice type="warning" >}}
This feature is deprecated in Grav 1.8.
{{< /notice >}}

Output:

<div class="notice notice-warning">
    <strong>Warning:</strong> This feature is deprecated in Grav 1.8.
</div>

Shortcode with Parameters

{# user/themes/mytheme/shortcodes/button.twig #}
<a href="{{ url }}" class="btn btn-{{ style|default('primary') }} btn-{{ size|default('md') }}">
    {{ content|default('Click Here') }}
</a>
{{< button url="/download" style="primary" size="lg" >}}Download Now{{< /button >}}

Self-Closing Shortcodes

{# user/themes/mytheme/shortcodes/youtube.twig #}
<iframe
    width="{{ width|default('560') }}"
    height="{{ height|default('315') }}"
    src="https://www.youtube.com/embed/{{ id }}"
    frameborder="0"
    allowfullscreen
></iframe>
{{< youtube id="dQw4w9WgXcQ" width="800" height="450" >}}

Nested Shortcodes

Shortcodes can be nested inside each other:

{{< notice type="tip" >}}
Check out this video: {{< youtube id="abc123" >}}
{{< /notice >}}

The inner shortcode (youtube) is processed first, then the outer shortcode (notice) wraps it.

Shortcode with Plugin Logic

For complex shortcodes, register them in a plugin:

<?php
// user/plugins/my-custom-shortcodes/my-custom-shortcodes.php
namespace Grav\Plugin;

use Grav\Common\Plugin;
use Grav\Plugin\ShortcodeCore\ShortcodeManager;

class MyCustomShortcodesPlugin extends Plugin
{
    public static function getSubscribedEvents()
    {
        return [
            'onShortcodeHandlers' => ['onShortcodeHandlers', 0],
        ];
    }

    public function onShortcodeHandlers()
    {
        $this->grav['shortcode']->registerHandler(
            'weather',
            function($attrs, $content, $tag) {
                $city = $attrs->get('city', 'London');
                // Fetch weather data from an API
                return "<div class='weather'>Weather in {$city}: Sunny, 22°C</div>";
            }
        );
    }
}

Then use in Markdown:

{{< weather city="Mumbai" >}}

Shortcode Configuration

Shortcodes can access plugin configuration:

{% set default_color = grav.config.plugins.myplugin.default_color|default('blue') %}
<div style="color: {{ color|default(default_color) }}">
    {{ content }}
</div>

Disabling Shortcodes

Shortcodes can be escaped by prefixing with &:

&{{< notice >}}This is not processed as a shortcode{{< /notice >}}

The & prefix tells the parser to output the shortcode as literal text.

Shortcode Aliases

Define aliases in shortcode-core configuration:

# user/config/plugins/shortcode-core.yaml
shortcodes:
    notice:
        enabled: true
        aliases:
            - info
            - warning
            - alert

Then {{< info >}} works the same as {{< notice type="info" >}}.

Common Mistakes

  1. Shortcode plugin not installed: Shortcodes do not work without the shortcode-core plugin. Install it with bin/gpm install shortcode-core and enable it in configuration.

  2. Twig shortcode in wrong directory: Shortcode templates go in user/themes/YOURTHEME/shortcodes/, not templates/. Grav looks for shortcode templates in the shortcodes/ subdirectory.

  3. Forgetting to close shortcodes: Wrapping shortcodes (those with content between opening and closing tags) require both {{< shortcode >}} and {{< /shortcode >}}. Self-closing shortcodes use single tags.

  4. Markdown Extra disabled: Tables, footnotes, and definition lists require markdown.extra: true in system.yaml. Without it, the syntax renders as raw text.

  5. Shortcode whitespace issues: Extra whitespace inside shortcode parameters can cause Parsing errors. Use param="value" without spaces around the = sign.

Practice Questions

  1. What configuration enables Markdown Extra features in Grav? Answer: Set pages.markdown.extra: true in user/config/system.yaml. This enables tables, footnotes, definition lists, and abbreviations.

  2. Where do custom shortcode templates live? Answer: In user/themes/YOURTHEME/shortcodes/. Each shortcode is a Twig file named after the shortcode tag (e.g., notice.twig for {{< notice >}}).

  3. How do you create a self-closing shortcode vs a wrapping shortcode? Answer: Self-closing shortcodes use a single tag: {{< shortcode param="value" >}}. Wrapping shortcodes have opening and closing tags: {{< shortcode >}}content{{< /shortcode >}}.

  4. What happens when you nest shortcodes? Answer: Inner shortcodes are processed first, then the outer shortcode wraps the result. This allows composition of shortcodes (e.g., a YouTube video inside a notice box).

  5. Challenge: Create a complete shortcode library for a documentation site. Build the following shortcodes: a callout with type (info, warning, danger, success) and icon support, a code-tabs component that displays multiple code samples with tab switching, an accordion with collapsible sections, a tooltip that shows help text on hover, a gallery with lightbox image display, and a file-download with icon, filename, and size. Each shortcode must support parameters and be nestable.

FAQ

What is the difference between Markdown Extra and standard Markdown?

Markdown Extra adds tables, footnotes, definition lists, abbreviations, and fenced code blocks with language identifiers. Standard Markdown only supports paragraphs, headings, lists, links, images, bold, and italic.

Do shortcodes work in the Admin panel?

Yes, with active_admin: true in shortcode-core.yaml configuration. Shortcodes render in the preview and work in the Markdown editor.

Can I use PHP logic inside a shortcode?

Yes, by registering the shortcode handler in a plugin (PHP file), not just in a Twig template. Plugin-registered shortcodes have full access to Grav services, databases, and external APIs.

{{< faq "How do I pass complex data structures as shortcode parameters?" "Shortcode parameters are strings. For complex data, use JSON in a single parameter: {{< gallery images='[\"img1.jpg\",\"img2.jpg\"]' >}}. Then decode it in the shortcode template." >}}

Do shortcodes affect page performance?

Each shortcode adds processing overhead. Simple Twig shortcodes are fast (microseconds). Plugin shortcodes that call APIs or query databases add latency. Cache pages with shortcodes in production.

Mini Project

Goal: Build a shortcode library for a developer documentation site.

  1. Install the shortcode-core plugin and enable Markdown Extra
  2. Create a callout shortcode with types: info, warning, danger, success
  3. Create a code shortcode that wraps content in a syntax-highlighted block with copy button
  4. Create a tabs shortcode system with tab switching for multi-language code examples
  5. Create a download shortcode that renders a file download button with icon
  6. Create an accordion shortcode for FAQ sections
  7. Create a youtube shortcode for embedded videos
  8. Create a gist shortcode that embeds GitHub gists
  9. Test nesting: callout containing tabs containing code
  10. Document the shortcode library for content editors

What's Next

Now you can extend Markdown with shortcodes. Next, learn Twig filters and functions:

Continue to Lesson 15: Twig Filters & Functions — Date formatting, translation, array functions, and built-in Twig utilities.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro