New May 11, 2026

Feature Toggles with Eleventy

More Front-end Bloggers All from Kitty Giraudel View Feature Toggles with Eleventy on kittygiraudel.com

As my website grew, I noticed I started using more and more environment conditionals, like checking whether the site is built for development or production (based on the value of the NODE_ENV environment variable). It’s not a code smell per se, and there are certainly many reasons to do so:

Ultimately, I realized I wanted proper feature toggles. Let’s see how to set them up in Eleventy.

In a nutshell

If you do not use TypeScript in your Eleventy project (more on the TS approach below), and author everything in JavaScript, then the simplest approach is to have an object in a JavaScript file somewhere (or even in your .eleventy.js file):

const PRODUCTION = process.env.NODE_ENV === 'production'

export const FEATURES = { renderDrafts: !PRODUCTION, markdownAlternative: PRODUCTION, inlineAssets: PRODUCTION, serviceWorker: PRODUCTION, minifyHTML: PRODUCTION, syntaxHighlight: true }

Then, you can read that object in your bits and bobs of JavaScript logic to toggle features appropriately. For instance:

export default function (config) {
	if (FEATURES.minifyHTML)
		config.addTransform('htmlmin', minifyHTML)

if (FEATURES.syntaxHighlight) config.addPlugin(syntaxHighlight)

if (!FEATURES.markdownAlternative) config.ignores.add('pages/blog/index-markdown.liquid') }

In data files

If you need to access these toggles in your data files, you can just import the features object, even if you defined it in your Eleventy configuration file!

import { FEATURES } from '../.eleventy.js'

// Do something with FEATURES.<yourFeatureName>

In templates

And if you need to access them in your Liquid templates (or whatever template format you use), you can expose them via a data file (e.g. features.js). Basically just re-export the configuration object so it can be consumed in the templates.

import { FEATURES } from '../.eleventy.js'

export default FEATURES

Alternatively, you can re-export only the ones you actually need in the view (and even rename them if desired):

import { FEATURES } from '../.eleventy.js'

export default { initialize_service_worker: FEATURES.serviceWorker }

And then in your templates:

{% if features.initialize_service_worker %}
<script>
if ('serviceWorker' in navigator) {
	navigator.serviceWorker.register('/service-worker.js', { scope: '/' })
}
</script>
{% endif %}

With TypeScript

If like me, you’ve converted your Eleventy configuration file to TypeScript, things are a bit dicier. That’s because Eleventy doesn’t really support TypeScript out of the box, and relies on Node.js’ type stripping feature simply not to choke on it.

The main problem is that data files cannot be authored in TypeScript: they have to use plain JavaScript (unless you want to introduce your own compilation step prior to running Eleventy). So you can’t have your feature toggles in a TypeScript file, if you need to read them in a JavaScript file.

I decided to go for an interchangeable and standardized format: JSON. Moving our toggles into a features.json file at the root of the project:

{
	"renderDrafts": ["development"],
	"markdownAlternative": ["production"],
	"inlineAssets": ["production"],
	"serviceWorker": ["production"],
	"minifyHTML": ["production"],
	"syntaxHighlight": ["development", "production"]
}

We can update our eleventy.config.ts file to consume that file:

import FEATURES from './features.json' with { type: 'json' }

const ENV = process.env.NODE_ENV

// …

export default function (config: UserConfig) { if (FEATURES.minifyHTML.includes(ENV)) config.addTransform('htmlmin', minifyHTML)

if (FEATURES.syntaxHighlight.includes(ENV)) config.addPlugin(syntaxHighlight)

if (!FEATURES.markdownAlternative.includes(ENV)) config.ignores.add('pages/blog/index-markdown.liquid') }

Stronger types

The problem with using JSON is that consuming said JSON in TypeScript results in an untyped data structure. We can fix that though! For TypeScript usage, we can wrap the JSON import into something more robust:

import features from '../features.json' with { type: 'json' }

export type FeatureEnv = 'development' | 'production' export type FeatureName = keyof typeof features export type Features = { [K in FeatureName]: [FeatureEnv?, FeatureEnv?] }

const FEATURES = features as unknown as Features

export default FEATURES

Then we can update our Eleventy configuration file (and other TypeScript files) to import from the new path, without type assertion or casting:

import FEATURES from './features.ts'

Wrapping up

Not overly complicated in the end, but I had to fiddle with it for a while before getting it to work. Also it’s worth pointing out that this is not the only approach. You can shape that data structure the way you want, as long as you have a way to know whether feature X is available in environment Y.

I hope this helps. :)

Scroll to top