Skip to content

A configurable Eleventy plugin that enables a powerful component system for building dynamic, reusable HTML components across your static site.

License

Notifications You must be signed in to change notification settings

MWDelaney/eleventy-plugin-reusable-components

Repository files navigation

Reusable Components for Eleventy Plugin

npm version Tests 🎈🐀

components-logo

A configurable Eleventy plugin that enables a powerful component system for building dynamic, reusable HTML components across your static site.

👉 Build your components once and use them anywhere.

Features

  • 🧩 Dynamic Component Rendering - Render components based on content data
  • 🎨 Template Language Agnostic - Works with Nunjucks, Liquid, WebC, Vento, and more
  • 🏗️ Flexible Configuration - Customizable directories and options
  • 🚀 Production Ready - Excludes development components from production builds
  • 🔧 Developer Friendly - Comprehensive error handling and debugging

Installation

npm install eleventy-plugin-reusable-components

Quick Start

1. Add the Plugin

Add the plugin to your Eleventy configuration file (.eleventy.js or eleventy.config.js).

View configuration code
import reusableComponents from "eleventy-plugin-reusable-components";

export default function(eleventyConfig) {
  eleventyConfig.addPlugin(reusableComponents);
}

2. Create a Component

Create a component file at src/components/callout.liquid:

View Liquid component template
---
title: Callout

# Default values
heading: Lorem ipsum dolor sit
description: A callout component to highlight important information.
links:
  - linkUrl: "#"
    linkText: Learn more
background: light
---

<div class="callout callout--{{ background }}">
  <h3 class="callout__heading">{{ heading }}</h3>
  <p class="callout__description">{{ description }}</p>
  {% if links %}
    <div class="callout__links">
      {% for link in links %}
        <a href="{{ link.linkUrl }}" class="callout__link">{{ link.linkText }}</a>
      {% endfor %}
    </div>
  {% endif %}
</div>
View Nunjucks component template
---
title: Callout

# Default values
heading: Lorem ipsum dolor sit
description: A callout component to highlight important information.
links:
  - linkUrl: "#"
    linkText: Learn more
background: light
---

<div class="callout callout--{{ background }}">
  <h3 class="callout__heading">{{ heading }}</h3>
  <p class="callout__description">{{ description }}</p>
  {% if links %}
    <div class="callout__links">
      {% for link in links %}
        <a href="{{ link.linkUrl }}" class="callout__link">{{ link.linkText }}</a>
      {% endfor %}
    </div>
  {% endif %}
</div>
View WebC component template
---
title: Callout

# Default values
heading: Lorem ipsum dolor sit
description: A callout component to highlight important information.
links:
  - linkUrl: "#"
    linkText: Learn more
background: light
---

<div :class="`callout callout--${background}`">
  <h3 class="callout__heading" @text="heading"></h3>
  <p class="callout__description" @text="description"></p>
  <div class="callout__links" webc:if="links">
    <a 
      :href="link.linkUrl" 
      class="callout__link" 
      @text="link.linkText"
      webc:for="link of links">
    </a>
  </div>
</div>
View Vento component template
---
title: Callout

# Default values
heading: Lorem ipsum dolor sit
description: A callout component to highlight important information.
links:
  - linkUrl: "#"
    linkText: Learn more
background: light
---

<div class="callout callout--{{ background }}">
  <h3 class="callout__heading">{{ heading }}</h3>
  <p class="callout__description">{{ description }}</p>
  {{ if links }}
    <div class="callout__links">
      {{ for link of links }}
        <a href="{{ link.linkUrl }}" class="callout__link">{{ link.linkText }}</a>
      {{ /for }}
    </div>
  {{ /if }}
</div>

3. Use the Component

In any template, use the renderComponent filter:

View Liquid usage example
{{-
  {
    "type": "callout",
    "heading": "Help organize the 11ty Meetup!",
    "description": "A callout component to highlight important information.",
    "links": [
      {
        "linkUrl": "https://11tymeetup.dev/",
        "linkText": "Join the 11ty Meetup!"
      }
    ],
    "background": "warning"
  } | renderComponent
-}}

Or from frontmatter:

---
title: My Page

# Callout component data
callout:
  type: callout
  heading: Help organize the 11ty Meetup!
  description: A callout component to highlight important information.
  links:
    - linkUrl: https://11tymeetup.dev/
      linkText: Join the 11ty Meetup!
  background: warning
---

{{ callout | renderComponent }}
View Nunjucks usage example
{{-
  {
    "type": "callout",
    "heading": "Help organize the 11ty Meetup!",
    "description": "A callout component to highlight important information.",
    "links": [
      {
        "linkUrl": "https://11tymeetup.dev/",
        "linkText": "Join the 11ty Meetup!"
      }
    ],
    "background": "warning"
  } | renderComponent | safe
-}}

Or from frontmatter:

---
title: My Page

# Callout component data
callout:
  type: callout
  heading: Help organize the 11ty Meetup!
  description: A callout component to highlight important information.
  links:
    - linkUrl: https://11tymeetup.dev/
      linkText: Join the 11ty Meetup!
  background: warning
---

{{ callout | renderComponent | safe }}
View WebC usage example
<template @html="{
  'type': 'callout',
  'heading': 'Help organize the 11ty Meetup!',
  'description': 'A callout component to highlight important information.',
  'links': [
    {
      'linkUrl': 'https://11tymeetup.dev/',
      'linkText': 'Join the 11ty Meetup!'
    }
  ],
  'background': 'warning'
} | renderComponent"></template>

Or from frontmatter:

---
title: My Page

# Callout component data
callout:
  type: callout
  heading: Help organize the 11ty Meetup!
  description: A callout component to highlight important information.
  links:
    - linkUrl: https://11tymeetup.dev/
      linkText: Join the 11ty Meetup!
  background: warning
---

<template @html="callout | renderComponent"></template>
View Vento usage example
{{-
  {
    "type": "callout",
    "heading": "Help organize the 11ty Meetup!",
    "description": "A callout component to highlight important information.",
    "links": [
      {
        "linkUrl": "https://11tymeetup.dev/",
        "linkText": "Join the 11ty Meetup!"
      }
    ],
    "background": "warning"
  } |> renderComponent |> safe
-}}

Or from frontmatter:

---
title: My Page

# Callout component data
callout:
  type: callout
  heading: Help organize the 11ty Meetup!
  description: A callout component to highlight important information.
  links:
    - linkUrl: https://11tymeetup.dev/
      linkText: Join the 11ty Meetup!
  background: warning
---

{{ callout |> renderComponent |> safe }}

4. Render Multiple Components

You can render multiple components by passing an array of component data:

View Liquid multiple components example
---
title: My Page

# Mixed component data
components:
  - type: callout
    heading: Help organize the 11ty Meetup!
    description: A callout component to highlight important information.
    links:
      - linkUrl: https://11tymeetup.dev/
        linkText: Join the 11ty Meetup!
    background: warning

  - type: text-and-image
    heading: About Our Community
    description: Join thousands of developers building amazing things with Eleventy.
    image: /assets/images/community.jpg
    imageAlt: Community members collaborating
    layout: image-right
---

{{ components | renderComponent }}
View Nunjucks multiple components example
---
title: My Page

# Mixed component data
components:
  - type: callout
    heading: Help organize the 11ty Meetup!
    description: A callout component to highlight important information.
    links:
      - linkUrl: https://11tymeetup.dev/
        linkText: Join the 11ty Meetup!
    background: warning

  - type: text-and-image
    heading: About Our Community
    description: Join thousands of developers building amazing things with Eleventy.
    image: /assets/images/community.jpg
    imageAlt: Community members collaborating
    layout: image-right
---

{{ components | renderComponent | safe }}
View WebC multiple components example
---
title: My Page

# Mixed component data
components:
  - type: callout
    heading: Help organize the 11ty Meetup!
    description: A callout component to highlight important information.
    links:
      - linkUrl: https://11tymeetup.dev/
        linkText: Join the 11ty Meetup!
    background: warning

  - type: text-and-image
    heading: About Our Community
    description: Join thousands of developers building amazing things with Eleventy.
    image: /assets/images/community.jpg
    imageAlt: Community members collaborating
    layout: image-right
---

<template @html="components | renderComponent"></template>
View Vento multiple components example
---
title: My Page

# Mixed component data
components:
  - type: callout
    heading: Help organize the 11ty Meetup!
    description: A callout component to highlight important information.
    links:
      - linkUrl: https://11tymeetup.dev/
        linkText: Join the 11ty Meetup!
    background: warning

  - type: text-and-image
    heading: About Our Community
    description: Join thousands of developers building amazing things with Eleventy.
    image: /assets/images/community.jpg
    imageAlt: Community members collaborating
    layout: image-right
---

{{ components |> renderComponent |> safe }}

Note: The renderComponent filter accepts a template language parameter ("njk", "liquid", "webc", "vto", etc.) and can process both single components and arrays of components. The filter automatically merges component default values with your provided data - any missing fields will use the defaults from the component file.

Configuration

Default Options

View default options
const defaultOptions = {
  componentsDir: "src/components/*.*",
  collectionName: "components",
  enableRenderPlugin: true,
  excludeFromProduction: true
};

Usage Examples

Method 1: From Frontmatter

Define components directly in your page's frontmatter:

View Liquid frontmatter example
---
title: My Page
components:
  - type: callout
    heading: Welcome!
    description: Thanks for visiting our site
    background: primary

  - type: text-and-image
    heading: About Us
    description: Learn more about our company
    image: /assets/images/about.jpg
    imageAlt: About our company
---

<main>
  {{ components | renderComponent }}
</main>
View Nunjucks frontmatter example
---
title: My Page
components:
  - type: callout
    heading: Welcome!
    description: Thanks for visiting our site
    background: primary

  - type: text-and-image
    heading: About Us
    description: Learn more about our company
    image: /assets/images/about.jpg
    imageAlt: About our company
---

<main>
  {{ components | renderComponent | safe }}
</main>
View WebC frontmatter example
---
title: My Page
components:
  - type: callout
    heading: Welcome!
    description: Thanks for visiting our site
    background: primary

  - type: text-and-image
    heading: About Us
    description: Learn more about our company
    image: /assets/images/about.jpg
    imageAlt: About our company
---

<main>
  <template @html="components | renderComponent"></template>
</main>
View Vento frontmatter example
---
title: My Page
components:
  - type: callout
    heading: Welcome!
    description: Thanks for visiting our site
    background: primary

  - type: text-and-image
    heading: About Us
    description: Learn more about our company
    image: /assets/images/about.jpg
    imageAlt: About our company
---

<main>
  {{ components |> renderComponent |> safe }}
</main>

Method 2: Inline

Define components inline within your template:

View Liquid inline definition example
{% assign heroComponent = {
  type: "hero",
  heading: "Welcome to Our Site",
  description: "Thanks for visiting! We're excited to share our content with you.",
  background: "primary"
} %}

{% assign features = [
  {
    type: "callout",
    heading: "Fast Performance",
    description: "Built for speed and efficiency.",
    background: "success"
  },
  {
    type: "callout", 
    heading: "Easy to Use",
    description: "Simple and intuitive interface.",
    background: "info"
  }
] %}

<main>
  {{ heroComponent | renderComponent }}
  
  <section class="features">
  {{ features | renderComponent }}
  </section>
</main>
View Nunjucks inline definition example
{%- set heroComponent = {
  type: "hero",
  heading: "Welcome to Our Site",
  description: "Thanks for visiting! We're excited to share our content with you.",
  background: "primary"
} -%}

{%- set features = [
  {
    type: "callout",
    heading: "Fast Performance",
    description: "Built for speed and efficiency.",
    background: "success"
  },
  {
    type: "callout",
    heading: "Easy to Use", 
    description: "Simple and intuitive interface.",
    background: "info"
  }
] -%}

<main>
  {{ heroComponent | renderComponent | safe }}
  
  <section class="features">
  {{ features | renderComponent | safe }}
  </section>
</main>
View WebC inline definition example
<script webc:setup>
const heroComponent = {
  type: "hero",
  heading: "Welcome to Our Site",
  description: "Thanks for visiting! We're excited to share our content with you.",
  background: "primary"
};

const features = [
  {
    type: "callout",
    heading: "Fast Performance",
    description: "Built for speed and efficiency.",
    background: "success"
  },
  {
    type: "callout",
    heading: "Easy to Use",
    description: "Simple and intuitive interface.",
    background: "info"
  }
];
</script>

<main>
  <template @html="heroComponent | renderComponent"></template>
  
  <section class="features">
    <template @html="features | renderComponent"></template>
  </section>
</main>
View Vento inline definition example
{{ set heroComponent = {
  type: "hero",
  heading: "Welcome to Our Site",
  description: "Thanks for visiting! We're excited to share our content with you.",
  background: "primary"
} }}

{{ set features = [
  {
    type: "callout",
    heading: "Fast Performance",
    description: "Built for speed and efficiency.",
    background: "success"
  },
  {
    type: "callout",
    heading: "Easy to Use",
    description: "Simple and intuitive interface.",
    background: "info"
  }
] }}

<main>
  {{ heroComponent |> renderComponent |> safe }}
  
  <section class="features">
  {{ features |> renderComponent |> safe }}
  </section>
</main>

Method 3: From Data Files

Store component data in separate JSON files for better organization:

Data File: src/_data/homepage.json

View data file example
{
  "hero": {
    "type": "hero",
    "heading": "Welcome to Our Site",
    "subheading": "Building amazing experiences",
    "image": "/assets/images/hero-bg.jpg",
    "ctaText": "Learn More",
    "ctaUrl": "/about/"
  },
  "sections": [
    {
      "type": "text-and-image",
      "heading": "Our Mission",
      "description": "We strive to create exceptional digital experiences that make a difference.",
      "image": "/assets/images/mission.jpg",
      "imageAlt": "Our mission in action",
      "layout": "image-right"
    },
    {
      "type": "callout",
      "heading": "Ready to Get Started?",
      "description": "Join thousands of satisfied customers today.",
      "background": "primary",
      "links": [
        {
          "linkText": "Sign Up Now",
          "linkUrl": "/signup/"
        },
        {
          "linkText": "Learn More",
          "linkUrl": "/features/"
        }
      ]
    },
    {
      "type": "stats-grid",
      "stats": [
        { "number": "10k+", "label": "Happy Customers" },
        { "number": "99.9%", "label": "Uptime" },
        { "number": "24/7", "label": "Support" },
        { "number": "50+", "label": "Countries" }
      ]
    }
  ]
}

Template Usage

View Liquid template usage
---
title: Homepage
---

<main>
  {{ homepage.hero | renderComponent }}
  
  {{ homepage.sections | renderComponent }}
</main>
View Nunjucks template usage
---
title: Homepage
---

<main>
  {{ homepage.hero | renderComponent | safe }}
  
  {{ homepage.sections | renderComponent | safe }}
</main>
View WebC template usage
---
title: Homepage
---

<main>
  <template @html="homepage.hero | renderComponent"></template>
  
  <template @html="homepage.sections | renderComponent"></template>
</main>
View Vento template usage
---
title: Homepage
---

<main>
  {{ homepage.hero |> renderComponent |> safe }}
  
  {{ homepage.sections |> renderComponent |> safe }}
</main>

Component Structure

Components should follow this structure:

View component structure example
---
title: ComponentName

# Default values
heading: "default heading"
description: "default description"
---

<!-- Component template here -->
<div class="component-name">
  <h2>{{ heading }}</h2>
  <p>{{ description }}</p>
</div>

Required Frontmatter

  • title: Used for component matching (gets slugified)

Component Matching

The plugin matches components by comparing:

  • Component's title (from frontmatter) → slugified
  • Content item's type property → slugified

Examples:

  • Component: title: "Text and Image" → slug: "text-and-image"
  • Content: type: "text-and-image"Match!
  • Component: title: "Callout" → slug: "callout"
  • Content: type: "callout"Match!

Default Values & Data Merging

Components automatically merge their default values with the data you provide. This means you only need to specify the fields you want to override - any missing fields will use the defaults from the component file.

Example:

If your component has these defaults:

---
title: Callout

# Default values
heading: "Default Heading"
description: "Default description"
background: "light"
links:
  - linkUrl: "#"
    linkText: "Default Link"
---

And you use it with partial data:

{% assign myCallout = {
  type: "callout",
  heading: "Custom Heading"
} %}

{{ myCallout | renderComponent: "liquid" }}

The component will render with:

  • heading: "Custom Heading" (from your data)
  • description: "Default description" (from component default)
  • background: "light" (from component default)
  • links: Default links array (from component default)

This ensures components always have complete data to work with, even when you only provide a subset of the required fields.

Error Handling

The plugin handles errors gracefully:

  • Missing component: Returns empty string
  • Invalid data: Returns empty string
  • Missing collections: Returns empty string
  • Template errors: Logged to console, returns fallback

Troubleshooting

Component Not Rendering

  1. Check component title matches type:
View component title matching example
<!-- Component file -->
title: "My Component"  <!-- becomes "my-component" -->

<!-- Usage -->
type: "my-component"  <!-- must match! -->
  1. Verify component collection:
View component collection debug example
<!-- Debug: List all components -->
{% for component in collections.components %}
  <p>{{ component.data.title }} → {{ component.data.title | slugify }}</p>
{% endfor %}
  1. Check file location:
    • Default: src/components/*.njk
    • Custom: Set via componentsDir option

Template Language Issues

Make sure to specify the correct template language parameter for your template engine:

  • Liquid: {{ item | renderComponent }} (auto-escaped by default)
  • Nunjucks: {{ item | renderComponent | safe }}
  • WebC: <template @html="item | renderComponent"></template>
  • Vento: {{ item |> renderComponent |> safe }}

If no template language is specified, the filter will use the calling template's language by default.

API Reference

Plugin Options

Option Type Default Description
componentsDir string "src/components/*.*" Glob pattern for component files
collectionName string "components" Name of components collection
enableRenderPlugin boolean true Enable Eleventy Render Plugin
excludeFromProduction boolean true Exclude components from production

Filters

renderComponent

Matches content items to component templates and renders them with automatic default value merging.

Parameters:

  • item (Object|Array): Content item(s) with type property
  • templateLang (string): Optional. Template language ("njk", "liquid", "webc", "vto", etc.). If not specified, defaults to the calling template's language.

Returns:

  • string: Fully rendered HTML content or empty string

Behavior:

The filter automatically merges component default values with your provided data. Any fields not specified in your data will use the default values from the component's frontmatter. This ensures components always have complete data to render properly.

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests
  5. Submit a pull request

License

MIT License - see LICENSE file for details.

About

A configurable Eleventy plugin that enables a powerful component system for building dynamic, reusable HTML components across your static site.

Resources

License

Stars

Watchers

Forks