Skip to content
allonsy-studioPublic

About

A standard-library of filters, transforms, and shortcodes for 11ty. One install, one plugin line, done.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

dashly

A standard-library of filters, transforms, and shortcodes for 11ty. One install, one plugin line, done.

NPM version NPM downloads Tests License Brought to you by Allons-y Studio

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.

Install

npm install --save-dev @allons-y/dashly

Heavy 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.

Quick start

// 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 #}
&copy; {% currentYear %} Acme Corp.

A la carte

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";

What's in the box

Filters

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 –

Transforms

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.

Shortcodes

Name Purpose Peer dep
currentYear Current four-digit year (for footer copyrights) –

Plugin options

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.

Design notes

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.

Contributing

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. 💖

License

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.

Funding ☕️

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!

Buy Me A Coffee

About

A standard-library of filters, transforms, and shortcodes for 11ty. One install, one plugin line, done.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages