daun / statamic-bard-mutators
A collection of plugins for the Statamic Bard Mutator addon
Requires
- php: ^8.1
- jacksleight/statamic-bard-mutator: ^3.0
- statamic/cms: ^5.0 || ^6.0
Requires (Dev)
- larastan/larastan: ^2.9
- laravel/pint: ^1.14
- mockery/mockery: ^1.6
- orchestra/testbench: ^9.5
- pestphp/pest: ^3.5
- pestphp/pest-plugin-laravel: ^3.0
This package is auto-updated.
Last update: 2026-05-07 18:59:50 UTC
README
A collection of mutators for transforming Statamic Bard content.
Mutators are implemented as plugins for Jack Sleight's Bard Mutator Addon.
Mutators
- Lazy Load Images — add
loading=lazyanddecoding=asyncto images - Generate Heading IDs — add
idto headings - Insert Heading Permalinks — insert permalinks into headings
- Normalize Heading Levels — close gaps in the heading hierarchy
- Shift Heading Levels — shift or clamp heading levels
- Mark External Links — add
targetandrelto external links - Mark Asset Links — add
downloadto asset links - Semantic Blockquotes — wrap blockquotes in
figurewithfigcaption - Wrap Tables — move tables into a horizontally scrollable container
- Remove List Item Paragraphs — remove
pwrappers aroundlitext
See the full list of mutators →
Installation
Install the package via composer:
composer require daun/statamic-bard-mutators
Registration
Register any mutators you want to use from the Mutator facade. Options can be passed as arguments
to the constructor. You can read more about
class-based mutator plugins
in the addon readme.
use JackSleight\StatamicBardMutator\Facades\Mutator; use Daun\BardMutators\MarkExternalLinks; Mutator::plugin(new MarkExternalLinks());
All Mutators
Mark External Links
Mark external links with target="_blank" and rel="external".
<!-- Before --> <a href="https://example.com">External link</a> <!-- After --> <a href="https://example.com" target="_blank" rel="external">External link</a>
new MarkExternalLinks(); // Optionally customize the `target` and `rel` attributes new MarkExternalLinks( target: '_blank', rel: 'noopener noreferrer' );
Mark Asset Links
Mark links to assets with download="filename.ext".
<!-- Before --> <a href="/assets/video.mp4">Download video</a> <!-- After --> <a href="/assets/video.mp4" download="video.mp4">Download video</a>
new MarkAssetLinks(); // Use original filename as download filename hint // Requires `daun/statamic-original-filename` package new MarkAssetLinks( useOriginalFilename: true );
Generate Heading IDs
Adds an id attribute to headings based on their content.
<!-- Before --> <h2>Heading</h2> <!-- After --> <h2 id="heading">Heading</h2>
new GenerateHeadingIds(); // Customize heading levels to generate IDs for and add a prefix to generated IDs new GenerateHeadingIds( levels: [2, 3], prefix: 'section-' );
Insert Heading Permalinks
Insert a permalink inside each heading pointing to its own id. Permalinks are
only added to headings that already have an id — register GenerateHeadingIds
beforehand if you want every heading to get a permalink.
<!-- Before --> <h2 id="introduction">Introduction</h2> <!-- After --> <h2 id="introduction"> <a href="#introduction" aria-label="Permalink to Introduction"> <span aria-hidden="true">#</span> </a> Introduction </h2>
The icon is wrapped in <span aria-hidden="true"> so a screen reader announces
only the link's aria-label, not the icon.
// Register GenerateHeadingIds first so headings get an id to link to. Mutator::plugin(new GenerateHeadingIds()); Mutator::plugin(new InsertHeadingPermalinks()); // Append the permalink instead of prepending it. new InsertHeadingPermalinks(behavior: 'append'); // Customize the icon (text, emoji, or raw HTML for an inline SVG). new InsertHeadingPermalinks(icon: '🔗'); new InsertHeadingPermalinks(icon: '<svg viewBox="0 0 16 16"><path d="…"/></svg>'); // Customize the accessible label. Use `{text}` as a placeholder for the // resolved heading text. new InsertHeadingPermalinks(label: 'Jump to {text}'); // Limit which heading levels get a permalink, add a class. new InsertHeadingPermalinks( levels: [2, 3], class: 'heading-permalink', );
Semantic Blockquotes
Wraps blockquotes in a figure element and moves the author/source into a figcaption element.
<!-- Before --> <blockquote> <p>Quote</p> <p>— Author</p> </blockquote> <!-- After --> <figure> <blockquote> <p>Quote</p> </blockquote> <figcaption> Author </figcaption> </figure>
new SemanticBlockquotes(); // Optionally add a class to the figure element new SemanticBlockquotes( class: 'quote' );
Wrap Tables
Wraps tables in a div element to allow for horizontal scrolling on smaller screens.
<!-- Before --> <table>...</table> <!-- After --> <div class="table-wrapper"> <table>...</table> </div>
new WrapTables(); // Optionally use a custom tag or add a class to the wrapper element new WrapTables( tag: 'section', class: 'table' );
Normalize Heading Levels
Close skip-level gaps in the heading hierarchy by pulling deep headings up
(e.g. <h2> followed by <h4> becomes <h2> followed by <h3>). The first
heading is left at whatever level it starts, and going back up to a shallower
level is always allowed.
<!-- Before --> <h2>Section</h2> <h4>Subsection</h4> <!-- After --> <h2>Section</h2> <h3>Subsection</h3>
new NormalizeHeadingLevels();
This pairs naturally with ShiftHeadingLevels — register NormalizeHeadingLevels
first to clean the hierarchy, then ShiftHeadingLevels to position the cleaned
tree (e.g. min: 2 to keep <h1> reserved for the page title).
Shift Heading Levels
Shift heading levels up or down. Useful when a Bard field is rendered under a page
<h1> and headings inside the field should start lower in the outline.
<!-- Before --> <h1>Section</h1> <h2>Subsection</h2> <!-- After: shift: 1 --> <h2>Section</h2> <h3>Subsection</h3>
// Shift every heading down (or up, with negative values). Clamped to h6. new ShiftHeadingLevels(shift: 1); // Clamp every heading to be at least h2 (e.g. to keep h1 reserved for the page title). new ShiftHeadingLevels(min: 2); // Shift the entire document so its shallowest heading becomes h2, // preserving relative hierarchy. Mutually exclusive with `shift`. new ShiftHeadingLevels(start: 2); // Combine: shift down, then clamp at h2. new ShiftHeadingLevels(shift: 1, min: 2);
Lazy Load Images
Add loading="lazy" and decoding="async" to images for better page performance.
<!-- Before --> <img src="photo.jpg" alt="A photo"> <!-- After --> <img src="photo.jpg" alt="A photo" loading="lazy" decoding="async">
new LazyLoadImages(); // Skip lazy loading on the first image if it sits at the top of the document // (i.e. is the first node, or appears in the first paragraph or figure). // Useful for avoiding lazy loading on a likely LCP image. new LazyLoadImages( skipFirst: true ); // Switch to lazysizes.js markup. The image gets a `lazyload` class, its `src` // is moved to `data-src`, and the native `loading`/`decoding` attributes are // not added. Pass an optional class name to override `lazyload`. (new LazyLoadImages())->usingLazysizes(); (new LazyLoadImages())->usingLazysizes('lazy-load'); // Combine with skipFirst: the LCP image is left untouched (no lazyload class, // no data-src swap) so the browser loads it eagerly. (new LazyLoadImages(skipFirst: true))->usingLazysizes();
<!-- After: usingLazysizes() --> <img class="lazyload" data-src="photo.jpg" alt="A photo">
Remove List Item Paragraphs
Remove the paragraphs that tiptap automatically adds inside list items.
<!-- Before --> <li> <p>List item</p> </li> <!-- After --> <li> List item </li>
new RemoveListItemParagraphs();