@anydigital/eleventy-bricks 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,1064 @@
1
+ # eleventy-bricks
2
+
3
+ A collection of helpful utilities and filters for Eleventy (11ty).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @anydigital/eleventy-bricks
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ You can use this library in two ways:
14
+
15
+ ### Option 1: As a Plugin
16
+
17
+ Import and use the entire plugin. You can configure which helpers to enable using the options parameter:
18
+
19
+ **ES Modules:**
20
+
21
+ ```javascript
22
+ import eleventyBricks from "@anydigital/eleventy-bricks";
23
+
24
+ export default function (eleventyConfig) {
25
+ eleventyConfig.addPlugin(eleventyBricks, {
26
+ mdAutoRawTags: true,
27
+ mdAutoNl2br: true,
28
+ mdAutoLinkFavicons: true,
29
+ siteData: true,
30
+ filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat"],
31
+ });
32
+
33
+ // Your other configuration...
34
+ }
35
+ ```
36
+
37
+ **CommonJS:**
38
+
39
+ ```javascript
40
+ const eleventyBricks = require("@anydigital/eleventy-bricks");
41
+
42
+ module.exports = function (eleventyConfig) {
43
+ eleventyConfig.addPlugin(eleventyBricks, {
44
+ mdAutoRawTags: true,
45
+ mdAutoNl2br: true,
46
+ mdAutoLinkFavicons: true,
47
+ siteData: true,
48
+ filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat"],
49
+ });
50
+
51
+ // Your other configuration...
52
+ };
53
+ ```
54
+
55
+ > **Note:** The CommonJS wrapper uses dynamic imports internally and returns async functions. Eleventy's `addPlugin()` method handles this automatically.
56
+
57
+ ### Option 2: Import Individual Helpers (Recommended)
58
+
59
+ Import only the specific helpers you need without using the plugin:
60
+
61
+ **ES Modules:**
62
+
63
+ ```javascript
64
+ import {
65
+ mdAutoRawTags,
66
+ mdAutoNl2br,
67
+ mdAutoLinkFavicons,
68
+ setAttrFilter,
69
+ whereInFilter,
70
+ mergeFilter,
71
+ removeTagFilter,
72
+ ifFilter,
73
+ attrConcatFilter,
74
+ siteData,
75
+ } from "@anydigital/eleventy-bricks";
76
+
77
+ export default function (eleventyConfig) {
78
+ mdAutoRawTags(eleventyConfig);
79
+ mdAutoNl2br(eleventyConfig);
80
+ mdAutoLinkFavicons(eleventyConfig);
81
+ setAttrFilter(eleventyConfig);
82
+ whereInFilter(eleventyConfig);
83
+ mergeFilter(eleventyConfig);
84
+ removeTagFilter(eleventyConfig);
85
+ ifFilter(eleventyConfig);
86
+ attrConcatFilter(eleventyConfig);
87
+ siteData(eleventyConfig);
88
+
89
+ // Your other configuration...
90
+ }
91
+ ```
92
+
93
+ **CommonJS:**
94
+
95
+ ```javascript
96
+ const {
97
+ mdAutoRawTags,
98
+ mdAutoNl2br,
99
+ mdAutoLinkFavicons,
100
+ setAttrFilter,
101
+ whereInFilter,
102
+ mergeFilter,
103
+ removeTagFilter,
104
+ ifFilter,
105
+ attrConcatFilter,
106
+ siteData,
107
+ } = require("@anydigital/eleventy-bricks");
108
+
109
+ module.exports = async function (eleventyConfig) {
110
+ await mdAutoRawTags(eleventyConfig);
111
+ await mdAutoNl2br(eleventyConfig);
112
+ await mdAutoLinkFavicons(eleventyConfig);
113
+ await setAttrFilter(eleventyConfig);
114
+ await whereInFilter(eleventyConfig);
115
+ await mergeFilter(eleventyConfig);
116
+ await removeTagFilter(eleventyConfig);
117
+ await ifFilter(eleventyConfig);
118
+ await attrConcatFilter(eleventyConfig);
119
+ await siteData(eleventyConfig);
120
+
121
+ // Your other configuration...
122
+ };
123
+ ```
124
+
125
+ > **Note:** When using CommonJS with individual helpers, the config function must be `async` and each helper must be `await`ed, as the CommonJS wrapper uses dynamic imports internally.
126
+
127
+ ## Configuration Options
128
+
129
+ When using the plugin (Option 1), you can configure which helpers to enable:
130
+
131
+ | Option | Type | Default | Description |
132
+ | -------------------- | --------------- | ------- | ---------------------------------------------------------------- |
133
+ | `mdAutoRawTags` | boolean | `false` | Enable the mdAutoRawTags preprocessor for Markdown files |
134
+ | `mdAutoNl2br` | boolean | `false` | Enable the mdAutoNl2br preprocessor to convert \n to `<br>` tags |
135
+ | `mdAutoLinkFavicons` | boolean | `false` | Enable the mdAutoLinkFavicons transform to add favicons to links |
136
+ | `siteData` | boolean | `false` | Enable site.year and site.prod global data |
137
+ | `filters` | array of string | `[]` | Array of filter names to enable (see Available Filters section) |
138
+
139
+ **Available filter names for the `filters` array:**
140
+
141
+ - `'attr'` - Override object attributes
142
+ - `'where_in'` - Filter collections by attribute values
143
+ - `'merge'` - Merge arrays or objects
144
+ - `'remove_tag'` - Remove HTML elements from content
145
+ - `'if'` - Inline conditional/ternary operator
146
+ - `'attr_concat'` - Concatenate values to an attribute array
147
+
148
+ **Example:**
149
+
150
+ ```javascript
151
+ eleventyConfig.addPlugin(eleventyBricks, {
152
+ mdAutoRawTags: true,
153
+ mdAutoNl2br: true,
154
+ mdAutoLinkFavicons: true,
155
+ siteData: true,
156
+ filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat"],
157
+ });
158
+ ```
159
+
160
+ ## Available 11ty Helpers
161
+
162
+ ### mdAutoRawTags
163
+
164
+ Prevents Nunjucks syntax from being processed in Markdown files by automatically wrapping `{{`, `}}`, `{%`, and `%}` with `{% raw %}` tags.
165
+
166
+ **Why use this?**
167
+
168
+ When writing documentation or tutorials about templating in Markdown files, you often want to show Nunjucks/Liquid syntax as literal text. This preprocessor automatically escapes these special characters so they display as-is instead of being processed by the template engine.
169
+
170
+ **Usage:**
171
+
172
+ 1. Enable `mdAutoRawTags` in your Eleventy config:
173
+
174
+ ```javascript
175
+ import { mdAutoRawTags } from "@anydigital/eleventy-bricks";
176
+
177
+ export default function (eleventyConfig) {
178
+ mdAutoRawTags(eleventyConfig);
179
+ // Or use as plugin:
180
+ // eleventyConfig.addPlugin(eleventyBricks, { mdAutoRawTags: true });
181
+ }
182
+ ```
183
+
184
+ **Example:**
185
+
186
+ Before `mdAutoRawTags`, writing this in Markdown:
187
+
188
+ ```markdown
189
+ Use {{ variable }} to output variables.
190
+ ```
191
+
192
+ Would try to process `{{ variable }}` as a template variable. With `mdAutoRawTags`, it displays exactly as written.
193
+
194
+ ### mdAutoNl2br
195
+
196
+ Automatically converts `\n` sequences to `<br>` tags in Markdown content. This is particularly useful for adding line breaks inside Markdown tables where standard newlines don't work.
197
+
198
+ **Why use this?**
199
+
200
+ Markdown tables don't support multi-line content in cells. By using `\n` in your content, this preprocessor will convert it to `<br>` tags, allowing you to display line breaks within table cells and other content.
201
+
202
+ **Usage:**
203
+
204
+ 1. Enable `mdAutoNl2br` in your Eleventy config:
205
+
206
+ ```javascript
207
+ import { mdAutoNl2br } from "@anydigital/eleventy-bricks";
208
+
209
+ export default function (eleventyConfig) {
210
+ mdAutoNl2br(eleventyConfig);
211
+ // Or use as plugin:
212
+ // eleventyConfig.addPlugin(eleventyBricks, { mdAutoNl2br: true });
213
+ }
214
+ ```
215
+
216
+ **Example:**
217
+
218
+ In your Markdown file:
219
+
220
+ ```markdown
221
+ | Column 1 | Column 2 |
222
+ | ---------------------- | --------------------------------- |
223
+ | Line 1\nLine 2\nLine 3 | Another cell\nWith multiple lines |
224
+ ```
225
+
226
+ Will render as:
227
+
228
+ ```html
229
+ <td>Line 1<br />Line 2<br />Line 3</td>
230
+ <td>Another cell<br />With multiple lines</td>
231
+ ```
232
+
233
+ **Note:** This processes literal `\n` sequences (backslash followed by 'n'), not actual newline characters. Type `\n` in your source files where you want line breaks.
234
+
235
+ ### mdAutoLinkFavicons
236
+
237
+ Automatically adds favicon images from Google's favicon service to links that display plain URLs or domain names. This transform processes all HTML output files and adds inline favicon images next to link text that appears to be a plain URL.
238
+
239
+ **Why use this?**
240
+
241
+ When you have links in your content that display raw URLs or domain names (like `https://example.com/page`), adding favicons provides a visual indicator of the external site. This transform automatically detects these plain-text URL links and enhances them with favicon images, making them more visually appealing and easier to recognize.
242
+
243
+ **Usage:**
244
+
245
+ 1. Enable `mdAutoLinkFavicons` in your Eleventy config:
246
+
247
+ ```javascript
248
+ import { mdAutoLinkFavicons } from "@anydigital/eleventy-bricks";
249
+
250
+ export default function (eleventyConfig) {
251
+ mdAutoLinkFavicons(eleventyConfig);
252
+ // Or use as plugin:
253
+ // eleventyConfig.addPlugin(eleventyBricks, { mdAutoLinkFavicons: true });
254
+ }
255
+ ```
256
+
257
+ **How it works:**
258
+
259
+ The transform:
260
+
261
+ 1. Scans all HTML output files for `<a>` tags
262
+ 2. Checks if the link text appears to be a plain URL or domain
263
+ 3. Extracts the domain from the URL
264
+ 4. Removes the domain from the link text (keeping only the path)
265
+ 5. Adds a favicon image from Google's favicon service inline with the remaining text
266
+
267
+ **Example:**
268
+
269
+ Before transformation:
270
+
271
+ ```html
272
+ <a href="https://github.com/anydigital/eleventy-bricks">https://github.com/anydigital/eleventy-bricks</a>
273
+ ```
274
+
275
+ After transformation:
276
+
277
+ ```html
278
+ <a href="https://github.com/anydigital/eleventy-bricks" target="_blank">
279
+ <i><img src="https://www.google.com/s2/favicons?domain=github.com&sz=32" /></i>
280
+ /anydigital/eleventy-bricks
281
+ </a>
282
+ ```
283
+
284
+ **Rules:**
285
+
286
+ - Only applies to links where the text looks like a plain URL (contains the domain or starts with `http://`/`https://`)
287
+ - Removes the protocol and domain from the display text
288
+ - Removes the trailing slash from the display text
289
+ - Only applies if at least 3 characters remain after removing the domain (to avoid showing favicons for bare domain links)
290
+ - Uses Google's favicon service at `https://www.google.com/s2/favicons?domain=DOMAIN&sz=32`
291
+ - Adds `target="_blank"` to the transformed links
292
+ - The favicon is wrapped in an `<i>` tag for easy styling
293
+
294
+ **Styling:**
295
+
296
+ You can style the favicon icons with CSS:
297
+
298
+ ```css
299
+ /* Style the favicon wrapper */
300
+ a i {
301
+ display: inline-block;
302
+ margin-right: 0.25em;
303
+ }
304
+
305
+ /* Style the favicon image */
306
+ a i img {
307
+ width: 16px;
308
+ height: 16px;
309
+ vertical-align: middle;
310
+ }
311
+ ```
312
+
313
+ **Note:** This transform only processes HTML output files (those ending in `.html`). It does not modify the original content files.
314
+
315
+ ### attr
316
+
317
+ A filter that creates a new object with an overridden attribute value. This is useful for modifying data objects in templates without mutating the original.
318
+
319
+ **Why use this?**
320
+
321
+ When working with Eleventy data, you sometimes need to modify an object's properties for a specific use case. The `attr` filter provides a clean way to create a modified copy of an object without affecting the original.
322
+
323
+ **Usage:**
324
+
325
+ 1. Enable the `attr` filter in your Eleventy config:
326
+
327
+ ```javascript
328
+ import { setAttrFilter } from "@anydigital/eleventy-bricks";
329
+
330
+ export default function (eleventyConfig) {
331
+ setAttrFilter(eleventyConfig);
332
+ // Or use as plugin:
333
+ // eleventyConfig.addPlugin(eleventyBricks, { filters: ['attr'] });
334
+ }
335
+ ```
336
+
337
+ 2. Use the filter in your templates:
338
+
339
+ ```njk
340
+ {# Create a modified version of a page object #}
341
+ {% set modifiedPage = page | attr('title', 'New Title') %}
342
+
343
+ <h1>{{ modifiedPage.title }}</h1>
344
+ <p>Original title: {{ page.title }}</p>
345
+ ```
346
+
347
+ **Parameters:**
348
+
349
+ - `obj`: The object to modify
350
+ - `key`: The attribute name to set (string)
351
+ - `value`: The value to set for the attribute (any type)
352
+
353
+ **Returns:**
354
+
355
+ A new object with the specified attribute set to the given value. The original object is not modified.
356
+
357
+ **Features:**
358
+
359
+ - Non-mutating: Creates a new object, leaving the original unchanged
360
+ - Works with any object type
361
+ - Supports any attribute name and value type
362
+ - Can be chained with other filters
363
+
364
+ **Examples:**
365
+
366
+ ```njk
367
+ {# Override a single attribute #}
368
+ {% set updatedPost = post | attr('featured', true) %}
369
+
370
+ {# Chain multiple attr filters #}
371
+ {% set modifiedPost = post
372
+ | attr('category', 'blog')
373
+ | attr('priority', 1)
374
+ %}
375
+
376
+ {# Use in loops #}
377
+ {% for item in collection %}
378
+ {% set enhancedItem = item | attr('processed', true) %}
379
+ {# ... use enhancedItem ... #}
380
+ {% endfor %}
381
+ ```
382
+
383
+ ### where_in
384
+
385
+ A filter that filters collection items by attribute value. It checks if an item's attribute matches a target value. If the attribute is an array, it checks if the array includes the target value. Supports nested attribute names using dot notation.
386
+
387
+ **Why use this?**
388
+
389
+ When working with Eleventy collections, you often need to filter items based on front matter data. The `where_in` filter provides a flexible way to filter by any attribute, with special handling for array attributes (like tags) and support for nested properties using dot notation.
390
+
391
+ **Usage:**
392
+
393
+ 1. Enable the `where_in` filter in your Eleventy config:
394
+
395
+ ```javascript
396
+ import { whereInFilter } from "@anydigital/eleventy-bricks";
397
+
398
+ export default function (eleventyConfig) {
399
+ whereInFilter(eleventyConfig);
400
+ // Or use as plugin:
401
+ // eleventyConfig.addPlugin(eleventyBricks, { filters: ['where_in'] });
402
+ }
403
+ ```
404
+
405
+ 2. Use the filter in your templates:
406
+
407
+ **Filter by exact attribute match:**
408
+
409
+ ```njk
410
+ {# Get all posts with category 'blog' #}
411
+ {% set blogPosts = collections.all | where_in('data.category', 'blog') %}
412
+
413
+ {% for post in blogPosts %}
414
+ <h2>{{ post.data.title }}</h2>
415
+ {% endfor %}
416
+ ```
417
+
418
+ **Filter by array attribute (tags):**
419
+
420
+ ```njk
421
+ {# Get all posts that include 'javascript' tag #}
422
+ {% set jsPosts = collections.all | where_in('data.tags', 'javascript') %}
423
+
424
+ {% for post in jsPosts %}
425
+ <h2>{{ post.data.title }}</h2>
426
+ {% endfor %}
427
+ ```
428
+
429
+ **Parameters:**
430
+
431
+ - `collection`: The collection to filter (array of items)
432
+ - `attrName`: The attribute name to check (string, supports dot notation for nested properties)
433
+ - `targetValue`: The value to match against (any type)
434
+
435
+ **Features:**
436
+
437
+ - Works with any attribute in front matter
438
+ - Supports dot notation for nested properties (e.g., `'data.tags'`, `'data.author.name'`)
439
+ - Special handling for array attributes (uses `includes()` check)
440
+ - Returns empty array if collection is invalid
441
+ - Filters out items without the specified attribute
442
+
443
+ **Examples:**
444
+
445
+ Front matter:
446
+
447
+ ```yaml
448
+ ---
449
+ title: My Post
450
+ category: blog
451
+ tags: [javascript, tutorial, beginner]
452
+ priority: 1
453
+ ---
454
+ ```
455
+
456
+ Template usage:
457
+
458
+ ```njk
459
+ {# Filter by category (using dot notation for nested properties) #}
460
+ {% set blogPosts = collections.all | where_in('data.category', 'blog') %}
461
+
462
+ {# Filter by tag (array) #}
463
+ {% set jsTutorials = collections.all | where_in('data.tags', 'javascript') %}
464
+
465
+ {# Filter by numeric value #}
466
+ {% set highPriority = collections.all | where_in('data.priority', 1) %}
467
+
468
+ {# Chain filters #}
469
+ {% set recentBlogPosts = collections.all | where_in('data.category', 'blog') | reverse | limit(5) %}
470
+ ```
471
+
472
+ ### merge
473
+
474
+ A filter that merges arrays or objects together, similar to Twig's merge filter. For arrays, it concatenates them. For objects, it performs a shallow merge where later values override earlier ones.
475
+
476
+ **Why use this?**
477
+
478
+ When working with data in templates, you often need to combine multiple arrays or objects. The `merge` filter provides a clean way to merge data structures without writing custom JavaScript, making it easy to combine collections, merge configuration objects, or aggregate data from multiple sources.
479
+
480
+ **Usage:**
481
+
482
+ 1. Enable the `merge` filter in your Eleventy config:
483
+
484
+ ```javascript
485
+ import { mergeFilter } from "@anydigital/eleventy-bricks";
486
+
487
+ export default function (eleventyConfig) {
488
+ mergeFilter(eleventyConfig);
489
+ // Or use as plugin:
490
+ // eleventyConfig.addPlugin(eleventyBricks, { filters: ['merge'] });
491
+ }
492
+ ```
493
+
494
+ 2. Use the filter in your templates:
495
+
496
+ **Merge arrays:**
497
+
498
+ ```njk
499
+ {# Combine two arrays #}
500
+ {% set allItems = featured | merge(regular) %}
501
+
502
+ {# Merge multiple arrays #}
503
+ {% set combined = array1 | merge(array2, array3, array4) %}
504
+
505
+ {% for item in allItems %}
506
+ <p>{{ item }}</p>
507
+ {% endfor %}
508
+ ```
509
+
510
+ **Merge objects:**
511
+
512
+ ```njk
513
+ {# Merge configuration objects #}
514
+ {% set defaultConfig = { theme: 'light', lang: 'en' } %}
515
+ {% set userConfig = { theme: 'dark' } %}
516
+ {% set finalConfig = defaultConfig | merge(userConfig) %}
517
+
518
+ {# Result: { theme: 'dark', lang: 'en' } #}
519
+ ```
520
+
521
+ **Parameters:**
522
+
523
+ - `first`: The first array or object (the base to merge into)
524
+ - `...rest`: One or more arrays or objects to merge in
525
+
526
+ **Features:**
527
+
528
+ - Works with both arrays and objects
529
+ - Supports merging multiple items at once
530
+ - Non-mutating: Creates new arrays/objects, leaving originals unchanged
531
+ - For objects: Later values override earlier ones (shallow merge)
532
+ - For arrays: Concatenates all arrays together
533
+ - Handles null/undefined gracefully
534
+
535
+ **Examples:**
536
+
537
+ ```njk
538
+ {# Combine featured and regular posts #}
539
+ {% set featuredPosts = collections.all | where_in('data.featured', true) %}
540
+ {% set regularPosts = collections.all | where_in('data.featured', false) %}
541
+ {% set allPosts = featuredPosts | merge(regularPosts) %}
542
+
543
+ {# Merge page metadata with defaults #}
544
+ {% set defaultMeta = {
545
+ author: 'Site Admin',
546
+ category: 'general',
547
+ comments: false
548
+ } %}
549
+ {% set pageMeta = defaultMeta | merge(page.data) %}
550
+
551
+ {# Combine arrays of tags #}
552
+ {% set commonTags = ['javascript', 'html', 'css'] %}
553
+ {% set specialTags = page.data.tags or [] %}
554
+ {% set allTags = commonTags | merge(specialTags) %}
555
+
556
+ {# Merge multiple configuration sources #}
557
+ {% set config = defaults | merge(siteConfig, pageConfig, userPrefs) %}
558
+ ```
559
+
560
+ ### remove_tag
561
+
562
+ A filter that removes a specified HTML element from provided HTML content. It removes the tag along with its content, including self-closing tags.
563
+
564
+ **Why use this?**
565
+
566
+ When working with content from external sources or user-generated content, you may need to strip certain HTML tags for security or presentation purposes. The `remove_tag` filter provides a simple way to remove unwanted tags like `<script>`, `<style>`, or any other HTML elements from your content.
567
+
568
+ **Usage:**
569
+
570
+ 1. Enable the `remove_tag` filter in your Eleventy config:
571
+
572
+ ```javascript
573
+ import { removeTagFilter } from "@anydigital/eleventy-bricks";
574
+
575
+ export default function (eleventyConfig) {
576
+ removeTagFilter(eleventyConfig);
577
+ // Or use as plugin:
578
+ // eleventyConfig.addPlugin(eleventyBricks, { filters: ['remove_tag'] });
579
+ }
580
+ ```
581
+
582
+ 2. Use the filter in your templates:
583
+
584
+ ```njk
585
+ {# Remove all script tags from content #}
586
+ {% set cleanContent = htmlContent | remove_tag('script') %}
587
+
588
+ {{ cleanContent | safe }}
589
+ ```
590
+
591
+ **Parameters:**
592
+
593
+ - `html`: The HTML content to process (string)
594
+ - `tagName`: The tag name to remove (string)
595
+
596
+ **Features:**
597
+
598
+ - Removes both opening and closing tags along with their content
599
+ - Handles self-closing tags (e.g., `<br />`, `<img />`)
600
+ - Handles tags with attributes
601
+ - Case-insensitive matching
602
+ - Non-destructive: Returns new string, doesn't modify original
603
+
604
+ **Examples:**
605
+
606
+ ```njk
607
+ {# Remove scripts from user-generated content #}
608
+ {% set userContent = '<p>Hello</p><script>alert("XSS")</script><p>World</p>' %}
609
+ {% set safeContent = userContent | remove_tag('script') %}
610
+ {# Result: '<p>Hello</p><p>World</p>' #}
611
+
612
+ {# Strip specific formatting tags #}
613
+ {% set formatted = '<div><strong>Bold</strong> and <em>italic</em> text</div>' %}
614
+ {% set noStrong = formatted | remove_tag('strong') %}
615
+ {# Result: '<div>Bold and <em>italic</em> text</div>' #}
616
+
617
+ {# Chain multiple remove_tag filters for multiple tags #}
618
+ {% set richContent = page.content %}
619
+ {% set stripped = richContent
620
+ | remove_tag('script')
621
+ | remove_tag('style')
622
+ | remove_tag('iframe')
623
+ %}
624
+
625
+ {# Remove images for text-only preview #}
626
+ {% set textOnly = htmlContent | remove_tag('img') %}
627
+ ```
628
+
629
+ **Security Note:**
630
+
631
+ While this filter can help sanitize HTML content, it should not be relied upon as the sole security measure. For critical security requirements, use a dedicated HTML sanitization library on the server side before content reaches your templates.
632
+
633
+ ### if
634
+
635
+ An inline conditional/ternary operator filter that returns one value if a condition is truthy, and another if it's falsy. Similar to Nunjucks' inline if syntax.
636
+
637
+ **Why use this?**
638
+
639
+ When you need simple conditional values in templates without verbose if/else blocks, the `if` filter provides a clean inline solution. It's especially useful for class names, attributes, or displaying alternate text based on conditions.
640
+
641
+ **Usage:**
642
+
643
+ 1. Enable the `if` filter in your Eleventy config:
644
+
645
+ ```javascript
646
+ import { ifFilter } from "@anydigital/eleventy-bricks";
647
+
648
+ export default function (eleventyConfig) {
649
+ ifFilter(eleventyConfig);
650
+ // Or use as plugin:
651
+ // eleventyConfig.addPlugin(eleventyBricks, { filters: ['if'] });
652
+ }
653
+ ```
654
+
655
+ 2. Use the filter in your templates:
656
+
657
+ ```njk
658
+ {# Basic usage #}
659
+ <div class="{{ 'active' | if: isActive, 'inactive' }}">Status</div>
660
+
661
+ {# Without falsy value (defaults to empty string) #}
662
+ <span class="{{ 'highlight' | if: shouldHighlight }}">Text</span>
663
+
664
+ {# With variable values #}
665
+ {% set status = 'Published' | if: post.published, 'Draft' %}
666
+ ```
667
+
668
+ **Parameters:**
669
+
670
+ - `trueValue`: The value to return if condition is truthy
671
+ - `condition`: The condition to evaluate
672
+ - `falseValue`: The value to return if condition is falsy (optional, defaults to empty string)
673
+
674
+ **Features:**
675
+
676
+ - Returns `trueValue` if condition is truthy, otherwise returns `falseValue`
677
+ - Treats empty objects `{}` as falsy
678
+ - Default `falseValue` is an empty string if not provided
679
+ - Works with any data type for values
680
+
681
+ **Examples:**
682
+
683
+ ```njk
684
+ {# Toggle CSS classes #}
685
+ <button class="{{ 'btn-primary' | if: isPrimary, 'btn-secondary' }}">
686
+ Click me
687
+ </button>
688
+
689
+ {# Display different text #}
690
+ <p>{{ 'Online' | if: user.isOnline, 'Offline' }}</p>
691
+
692
+ {# Use with boolean values #}
693
+ {% set isEnabled = true %}
694
+ <div>{{ 'Enabled' | if: isEnabled, 'Disabled' }}</div>
695
+
696
+ {# Conditional attribute values #}
697
+ <input type="checkbox" {{ 'checked' | if: isChecked }}>
698
+
699
+ {# With numeric values #}
700
+ <span class="{{ 'has-items' | if: items.length }}">
701
+ {{ items.length }} items
702
+ </span>
703
+
704
+ {# Chain with other filters #}
705
+ {% set cssClass = 'featured' | if: post.featured | upper %}
706
+ ```
707
+
708
+ ### attr_concat
709
+
710
+ A filter that concatenates values to an attribute array, returning a new object with the combined array. Useful for adding items to arrays like tags, classes, or other list-based attributes.
711
+
712
+ **Why use this?**
713
+
714
+ When working with objects that have array attributes (like tags), you often need to add additional values without mutating the original object. The `attr_concat` filter provides a clean way to combine existing array values with new ones, automatically handling duplicates.
715
+
716
+ **Usage:**
717
+
718
+ 1. Enable the `attr_concat` filter in your Eleventy config:
719
+
720
+ ```javascript
721
+ import { attrConcatFilter } from "@anydigital/eleventy-bricks";
722
+
723
+ export default function (eleventyConfig) {
724
+ attrConcatFilter(eleventyConfig);
725
+ // Or use as plugin:
726
+ // eleventyConfig.addPlugin(eleventyBricks, { filters: ['attr_concat'] });
727
+ }
728
+ ```
729
+
730
+ 2. Use the filter in your templates:
731
+
732
+ ```njk
733
+ {# Add tags to a post object #}
734
+ {% set enhancedPost = post | attr_concat('tags', ['featured', 'popular']) %}
735
+
736
+ {# Add a single value #}
737
+ {% set updatedPost = post | attr_concat('tags', 'important') %}
738
+
739
+ {# Add values from a JSON string #}
740
+ {% set modifiedPost = post | attr_concat('tags', '["new", "trending"]') %}
741
+ ```
742
+
743
+ **Parameters:**
744
+
745
+ - `obj`: The object to modify
746
+ - `attr`: The attribute name (must be an array or will be treated as one)
747
+ - `values`: Values to concatenate (can be an array, JSON string array, or single value)
748
+
749
+ **Returns:**
750
+
751
+ A new object with the specified attribute containing the combined unique array. The original object is not modified.
752
+
753
+ **Features:**
754
+
755
+ - Non-mutating: Creates a new object, leaving the original unchanged
756
+ - Automatically removes duplicates using Set
757
+ - Handles multiple input types: arrays, JSON string arrays, or single values
758
+ - Creates the attribute as an empty array if it doesn't exist
759
+ - Logs an error if the existing attribute is not an array
760
+
761
+ **Examples:**
762
+
763
+ ```njk
764
+ {# Add multiple tags #}
765
+ {% set post = { title: 'My Post', tags: ['javascript'] } %}
766
+ {% set enhancedPost = post | attr_concat('tags', ['tutorial', 'beginner']) %}
767
+ {# Result: { title: 'My Post', tags: ['javascript', 'tutorial', 'beginner'] } #}
768
+
769
+ {# Add single value #}
770
+ {% set updatedPost = post | attr_concat('tags', 'featured') %}
771
+ {# Result: { title: 'My Post', tags: ['javascript', 'tutorial', 'beginner', 'featured'] } #}
772
+
773
+ {# No duplicates #}
774
+ {% set deduped = post | attr_concat('tags', ['javascript', 'advanced']) %}
775
+ {# Result: Only 'advanced' is added, 'javascript' already exists #}
776
+
777
+ {# Chain multiple attr_concat filters #}
778
+ {% set finalPost = post
779
+ | attr_concat('tags', 'popular')
780
+ | attr_concat('categories', ['tech', 'programming'])
781
+ %}
782
+
783
+ {# Use in loops to enhance collection items #}
784
+ {% for item in collections.posts %}
785
+ {% set enhancedItem = item | attr_concat('data.tags', 'blog') %}
786
+ {# ... use enhancedItem ... #}
787
+ {% endfor %}
788
+ ```
789
+
790
+ ### siteData
791
+
792
+ Adds global site data to your Eleventy project, providing commonly needed values that can be accessed in all templates.
793
+
794
+ **Why use this?**
795
+
796
+ Many websites need access to the current year (for copyright notices) and environment information (to conditionally enable features based on production vs development). This helper provides these as global `site` data without manually setting them up.
797
+
798
+ **Usage:**
799
+
800
+ 1. Enable `siteData` in your Eleventy config:
801
+
802
+ ```javascript
803
+ import { siteData } from "@anydigital/eleventy-bricks";
804
+
805
+ export default function (eleventyConfig) {
806
+ siteData(eleventyConfig);
807
+ // Or use as plugin:
808
+ // eleventyConfig.addPlugin(eleventyBricks, { siteData: true });
809
+ }
810
+ ```
811
+
812
+ 2. Use the global data in your templates:
813
+
814
+ **Current Year:**
815
+
816
+ ```njk
817
+ <footer>
818
+ <p>&copy; {{ site.year }} Your Company Name. All rights reserved.</p>
819
+ </footer>
820
+ ```
821
+
822
+ **Environment Check:**
823
+
824
+ ```njk
825
+ {% if site.prod %}
826
+ <!-- Production-only features -->
827
+ <script async src="https://www.googletagmanager.com/gtag/js?id=GA_TRACKING_ID"></script>
828
+ {% else %}
829
+ <!-- Development-only features -->
830
+ <div class="dev-toolbar">Development Mode</div>
831
+ {% endif %}
832
+ ```
833
+
834
+ **Available Data:**
835
+
836
+ - `site.year`: The current year as a number (e.g., `2026`)
837
+ - `site.prod`: Boolean indicating if running in production mode (`true` for `eleventy build`, `false` for `eleventy serve`)
838
+
839
+ **Features:**
840
+
841
+ - Automatically updates the year value
842
+ - Detects production vs development mode based on `ELEVENTY_RUN_MODE` environment variable
843
+ - Available globally in all templates without manual setup
844
+ - No configuration required
845
+
846
+ **Examples:**
847
+
848
+ ```njk
849
+ {# Copyright notice #}
850
+ <p>Copyright &copy; {{ site.year }} My Site</p>
851
+
852
+ {# Conditional loading of analytics #}
853
+ {% if site.prod %}
854
+ <script src="/analytics.js"></script>
855
+ {% endif %}
856
+
857
+ {# Different behavior in dev vs prod #}
858
+ {% if site.prod %}
859
+ <link rel="stylesheet" href="/css/styles.min.css">
860
+ {% else %}
861
+ <link rel="stylesheet" href="/css/styles.css">
862
+ <script src="/live-reload.js"></script>
863
+ {% endif %}
864
+ ```
865
+
866
+ ### Additional Exports
867
+
868
+ The plugin also exports the following utility functions for advanced usage:
869
+
870
+ - `transformAutoRaw(content)`: The transform function used by `mdAutoRawTags` preprocessor. Can be used programmatically to wrap Nunjucks syntax with raw tags.
871
+ - `transformNl2br(content)`: The transform function used by `mdAutoNl2br` preprocessor. Can be used programmatically to convert `\n` sequences to `<br>` tags.
872
+ - `isPlainUrlText(linkText, domain)`: Helper function that checks if link text looks like a plain URL or domain.
873
+ - `cleanLinkText(linkText, domain)`: Helper function that cleans link text by removing protocol, domain, and leading slash.
874
+ - `buildFaviconLink(attrs, domain, text)`: Helper function that builds HTML for a link with favicon.
875
+ - `transformLink(match, attrs, url, linkText)`: The transform function used by `mdAutoLinkFavicons` that transforms a single link to include a favicon.
876
+ - `merge(first, ...rest)`: The core merge function used by the `merge` filter. Can be used programmatically to merge arrays or objects.
877
+ - `removeTag(html, tagName)`: The core function used by the `remove_tag` filter. Can be used programmatically to remove HTML tags from content.
878
+ - `iff(trueValue, condition, falseValue)`: The core conditional function used by the `if` filter. Can be used programmatically as a ternary operator.
879
+ - `attrConcat(obj, attr, values)`: The core function used by the `attr_concat` filter. Can be used programmatically to concatenate values to an attribute array.
880
+
881
+ ## Starter Configuration Files
882
+
883
+ The package includes pre-configured starter files in `node_modules/@anydigital/eleventy-bricks/src/` that you can symlink to your project for quick setup:
884
+
885
+ ### Available Starter Files
886
+
887
+ #### eleventy.config.js
888
+
889
+ A fully-configured Eleventy config file with:
890
+
891
+ - All eleventy-bricks plugins enabled
892
+ - Eleventy Navigation plugin
893
+ - Markdown-it with anchors and attributes
894
+ - YAML data support
895
+ - CLI input directory support
896
+ - Symlink support for development
897
+
898
+ **Required dependencies:**
899
+
900
+ ```bash
901
+ npm install @11ty/eleventy-navigation markdown-it markdown-it-anchor markdown-it-attrs js-yaml minimist
902
+ ```
903
+
904
+ **Symlink to your project:**
905
+
906
+ ```bash
907
+ ln -s node_modules/@anydigital/eleventy-bricks/src/eleventy.config.js eleventy.config.js
908
+ ```
909
+
910
+ #### admin/index.html
911
+
912
+ A ready-to-use Sveltia CMS admin interface for content management.
913
+
914
+ **Symlink to your project:**
915
+
916
+ ```bash
917
+ mkdir -p admin
918
+ ln -s ../node_modules/@anydigital/eleventy-bricks/src/admin/index.html admin/index.html
919
+ ```
920
+
921
+ ### Benefits of Symlinking
922
+
923
+ - **Always up-to-date**: Configuration automatically updates when you upgrade the package
924
+ - **Less maintenance**: No need to manually sync configuration changes
925
+ - **Quick setup**: Get started immediately with best-practice configurations
926
+ - **Easy customization**: Override specific settings by creating your own config that imports from the symlinked version
927
+
928
+ ### Alternative: Copy Files
929
+
930
+ If you prefer to customize the configurations extensively, you can copy the files instead:
931
+
932
+ ```bash
933
+ cp node_modules/@anydigital/eleventy-bricks/src/eleventy.config.js .
934
+ mkdir -p admin
935
+ cp node_modules/@anydigital/eleventy-bricks/src/admin/index.html admin/
936
+ ```
937
+
938
+ ## Development Workflow Setup
939
+
940
+ ### Using the `do` Folder Pattern
941
+
942
+ This package provides a pre-configured `do` folder setup that helps organize your development workflow using npm workspaces. The `do` folder contains scripts for building and running your Eleventy project.
943
+
944
+ **Setup:**
945
+
946
+ 1. Create a `do` folder in your project root:
947
+
948
+ ```bash
949
+ mkdir do
950
+ ```
951
+
952
+ 2. Symlink the package.json from the eleventy-bricks package:
953
+
954
+ ```bash
955
+ ln -s node_modules/@anydigital/eleventy-bricks/src/do/package.json do/package.json
956
+ ```
957
+
958
+ 3. Configure your root `package.json` to use npm workspaces:
959
+
960
+ ```json
961
+ {
962
+ "name": "my-project",
963
+ "workspaces": ["do"],
964
+ "scripts": {
965
+ "build": "npm -w do run build",
966
+ "start": "npm -w do run start"
967
+ }
968
+ }
969
+ ```
970
+
971
+ **Usage:**
972
+
973
+ Run your Eleventy project:
974
+
975
+ ```bash
976
+ npm start
977
+ ```
978
+
979
+ Build for production:
980
+
981
+ ```bash
982
+ npm run build
983
+ ```
984
+
985
+ **Available Scripts in `do` folder:**
986
+
987
+ - `build` - Build the site with Eleventy and minify CSS with Tailwind
988
+ - `start` - Start Eleventy dev server with live reload and Tailwind watch mode
989
+ - `stage` - Clean build and serve locally for preview
990
+ - `11ty` - Run Eleventy commands directly
991
+ - `11ty:clean` - Remove the `_site` output directory
992
+ - `tw` - Run Tailwind CSS commands
993
+
994
+ **Benefits:**
995
+
996
+ - **Clean separation**: Keep build scripts separate from project configuration
997
+ - **Reusable workflows**: Update scripts by upgrading the package
998
+ - **Workspace isolation**: Scripts run in their own workspace context
999
+ - **Easy maintenance**: No need to manually maintain build scripts
1000
+
1001
+ ## CLI Helper Commands
1002
+
1003
+ After installing this package, the `download-files` command becomes available:
1004
+
1005
+ ### download-files
1006
+
1007
+ A CLI command that downloads external files to your project based on URLs specified in your `package.json`.
1008
+
1009
+ **Usage:**
1010
+
1011
+ 1. Add a `_downloadFiles` field to your project's `package.json` with URL-to-path mappings:
1012
+
1013
+ ```json
1014
+ {
1015
+ "_downloadFiles": {
1016
+ "https://example.com/library.js": "src/vendor/library.js",
1017
+ "https://cdn.example.com/styles.css": "public/css/external.css"
1018
+ }
1019
+ }
1020
+ ```
1021
+
1022
+ 2. Run the download command:
1023
+
1024
+ ```bash
1025
+ npx download-files
1026
+ ```
1027
+
1028
+ **Options:**
1029
+
1030
+ - `-o, --output <dir>`: Specify an output directory where all files will be downloaded (relative paths in `_downloadFiles` will be resolved relative to this directory)
1031
+
1032
+ ```bash
1033
+ # Download all files to a specific directory
1034
+ npx download-files --output public
1035
+ ```
1036
+
1037
+ **Features:**
1038
+
1039
+ - Downloads multiple files from external URLs
1040
+ - Automatically creates directories if they don't exist
1041
+ - Overwrites existing files
1042
+ - Continues downloading remaining files even if some fail
1043
+ - Provides clear progress and error messages
1044
+ - Returns appropriate exit codes for CI/CD integration
1045
+
1046
+ **Use Cases:**
1047
+
1048
+ - Download third-party libraries and assets
1049
+ - Fetch external resources during build processes
1050
+ - Keep vendored files up to date
1051
+ - Automate dependency downloads that aren't available via npm
1052
+
1053
+ ## Requirements
1054
+
1055
+ - Node.js >= 18.0.0
1056
+ - Eleventy >= 3.0.0
1057
+
1058
+ ## License
1059
+
1060
+ MIT
1061
+
1062
+ ## Contributing
1063
+
1064
+ Contributions are welcome! Please feel free to submit a Pull Request.