A standard-library of filters, transforms, and shortcodes for 11ty. One install, one plugin line, done.
dashly is the standard-library helpers every 11ty site eventually rewrites from scratch: date formatting, RSS sanitization, HTML minification, URL validation, reading-time & excerpt generation, locale-aware number/currency/percent formatting, & more. All of it ships in a single tree-shakeable package.
npm install --save-dev @allons-y/dashlyHeavy dependencies (prettier, html-minifier-terser, posthtml, markdown-it, postcss) are declared as optional peers. Install only the ones you use.
Full documentation lives in docs & is built with yarn docs:build.
// eleventy.config.js
import dashly from "@allons-y/dashly";
export default function (config) {
config.addPlugin(dashly, {
dateLocale: "en-GB",
baseUrl: "https://example.com",
});
}Templates can immediately use:
{{ post.date | longDate }} {# 15 January 2026 #}
{{ post.date | shortDate }} {# Jan 2026 #}
{{ post.date | year }} {# 2026 #}
{{ name | firstWord }} {# "Allons-y" → "Allons-y" #}
{{ phone | digitsOnly }} {# "(555) 123-4567" → "5551234567" #}
{{ "example.com" | validateURL }} {# https://example.com #}
{{ post.content | cleanForRSS }} {# safe HTML for feeds #}
{{ post.content | wordCount }} {# 842 #}
{{ post.content | readingTime }} {# 4 #}
{{ post.content | excerpt(40) }} {# first 40 words, ellipsised #}
<pre>{{ page | dump }}</pre> {# pretty-printed JSON, circular-safe #}
{{ page.url | absoluteUrl }} {# https://example.com/posts/hello/ #}
{{ post.date | timeAgo }} {# "2 days ago" #}
{{ product.price | currency }} {# "$1,299.00" #}
{{ survey.rate | percent }} {# "25%": input 0.25 #}
© {% currentYear %} Acme Corp.import { filters, transforms } from "@allons-y/dashly";
config.addFilter("long", filters.longDate);
config.addFilter("rss", filters.cleanForRSS);
config.addTransform("prettier", transforms.createPrettierTransform());Or import from the narrowest subpath so unused dependencies never enter your bundle:
import { longDate } from "@allons-y/dashly/filters/dates";
import { validateURL } from "@allons-y/dashly/filters/urls";| Name | Purpose | Peer dep |
|---|---|---|
toISOString |
ISO-8601 string for <time> & JSON-LD |
– |
year |
Four-digit year | – |
dateFormat |
Pass-through Intl.DateTimeFormat |
– |
longDate |
15 January 2026 |
– |
shortDate |
Jan 2026 |
– |
firstWord, lastWord |
Pull first/last word from a string | – |
trim, stripWhitespace, digitsOnly |
Common string sanitizers | – |
keys |
Safe Object.keys for templates |
– |
validateURL |
Force https:// on bare hostnames; pass mailto:/tel:/relative URLs through |
– |
cleanForRSS |
Strip scripts, embeds, permalinks, custom elements from HTML | posthtml |
renderMarkdown |
Render a markdown string (with optional \n\n paragraph splitting) |
markdown-it |
wordCount |
Count words in a string (HTML stripped) | – |
readingTime |
Estimate minutes to read (configurable wpm, default 200) |
– |
excerpt |
First/last N words or chars, ellipsised. Negative N → trailing slice | – |
dump / jsonify |
Pretty-printed JSON for in-template debugging, circular-safe | – |
first |
First element of an array, or first N when a count is given | – |
last |
Last element of an array, or last N when a count is given | – |
reverse |
Reverse an array or string (non-mutating, code-point safe) | – |
capitalize |
Uppercase first letter, lowercase rest | – |
title |
Title-case each word | – |
stripTags |
Remove every HTML tag; collapse whitespace | – |
nl2br |
Replace newlines with <br> for plain-text → HTML |
– |
absoluteUrl |
Resolve a path against baseUrl for canonical/OG/JSON-LD tags |
– |
relativeDate / timeAgo |
Intl.RelativeTimeFormat wrapper ("2 days ago") |
– |
number |
Intl.NumberFormat wrapper with locale-aware grouping |
– |
currency |
Localized currency formatting (default USD) |
– |
percent |
Localized percent formatting (fraction-aware) | – |
groupBy |
Group items by key, dot-path, or selector function | – |
sortBy |
Sort by key/dot-path/function, 'asc' or 'desc' |
– |
chunk |
Split into N-sized arrays (gallery rows, paginated grids) | – |
where |
Filter by key equality, or truthiness when no value is given | – |
pluck |
Project an array of objects onto a single key/path | – |
tags |
Strip Eleventy system tags (all/post/…) from a tag list |
– |
| Name | Purpose | Peer dep |
|---|---|---|
prettier |
Format HTML/XML/SVG output | prettier |
htmlmin |
Minify HTML (production-gated by default) | html-minifier-terser |
cleanAttrs |
Strip eleventy:ignore="" markers left on <img> inside <picture> |
– |
The postcss helper (@allons-y/dashly/transforms/postcss) is exported separately for use in addBundle or addExtension('css', …) setups.
| Name | Purpose | Peer dep |
|---|---|---|
currentYear |
Current four-digit year (for footer copyrights) | – |
interface DashlyOptions {
dateLocale?: string; // default 'en-US'; also used by relativeDate / number / currency / percent
baseUrl?: string; // origin used by `absoluteUrl`, e.g. 'https://example.com'
readingTimeWpm?: number; // default 200
tagsExclude?: string[]; // tag names stripped by the `tags` filter; default ['all', 'post', 'posts']
filters?: false | { exclude?: string[] };
transforms?:
| false
| {
exclude?: string[];
prettier?: { extensions?: string[]; prettier?: PrettierOptions };
htmlmin?: { production?: boolean; minifier?: HtmlMinifierOptions };
cleanAttrs?: { attrs?: string[] };
};
shortcodes?: false | { exclude?: string[] };
}Pass false for any category to skip it entirely. Pass exclude to omit specific names.
No truncate filter. Mechanical mid-sentence truncation ({{ content | truncate(100) }}) is a poor reading experience: it hides content with no signal about what was cut & often clips mid-word or mid-tag. Use excerpt instead, which trims on word boundaries and is intended as a summary affordance, not a clipping one. For trailing slices (last N words), pass a negative N: {{ content | excerpt(-20) }}.
Smart quotes. No dedicated filter; enable markdown-it's typographer: true on the renderer you pass to renderMarkdown, which handles ' → ’, -- → —, etc. for free.
Nunjucks-parity universals. Nunjucks & Liquid templates ship many of these filters natively (first, last, reverse, capitalize, etc.). Registering them through addFilter, dashly makes them available in Nunjucks, Liquid, 11ty.js (JavaScript) templates, & Handlebars, with consistent semantics across those engines: test/engines.test.js renders each one through all four. The exceptions are the async cleanForRSS & renderMarkdown, which Handlebars can't await (see the Handlebars note below). Other engines are untested. first and last accept an optional count argument so {{ posts | first(3) }} covers the common "limit to N" case without a separate filter name.
Handlebars. Eleventy's official Handlebars plugin turns filters & shortcodes into helpers, so add it after dashly so it sees them. Every filter works there except two: do not use cleanForRSS or renderMarkdown in Handlebars templates. They are async, & Handlebars helpers are synchronous, so {{cleanForRSS html}} renders [object Promise]. There is no template-side fix: Handlebars has no way to wait on a helper, & both filters need an async import() of an optional peer (posthtml, markdown-it).
To get their output onto a Handlebars page, run them before the template renders & hand the template a finished string:
// post.11tydata.js
import { renderMarkdown } from '@allons-y/dashly/filters/html';
export default {
eleventyComputed: {
summaryHtml: async (data) => renderMarkdown(data.summary, { splitParagraphs: false }),
},
};Then use {{{summaryHtml}}} in the template. Or write that page in Nunjucks, Liquid, or 11ty.js, which await filters.
Argument errors. A present but wrongly typed argument throws a DashlyArgumentError naming the filter, the parameter, & what arrived, such as dashly: readingTime(): "wpm" must be a finite number, but received the string "fast". Omitted arguments, nullish values, & the options object Handlebars appends to every helper call fall back to defaults.
Contributions are welcome & appreciated! Please open an issue (before submitting a pull request) if you find a bug, have a feature request, or have a question. Be sure to follow the contribution guidelines and code of conduct but most importantly, be excellent to each other. 💖
This project is licensed under the MPL-2.0 License - see the LICENSE file for details. This means you can use this freely, modify as needed; changes to MPL-licensed files should be shared back under the same license.
If you find this plugin useful and would like to buy me a coffee/beer as a small thank you, I would greatly appreciate it!