Yet another Sass/SCSS plugin for Eleventy.
Source maps, PostCSS and revision hashes are supported.
There are several Eleventy plugins to support Sass/SCSS files, already. Even the official Eleventy website has a page which describes how to handle Sass/SCSS files with your Eleventy project.
I created the plugin, because I was not satisfied with the above solutions, for example:
-
The existing plugins did watch Sass/SCSS files and write CSS files by themselves or by using another toolkit, such as gulp.js, instead of using Eleventy's file watching and writing functionality. They might work well, but doesn't seem to be integrated enough with Eleventy.
-
The page in the official Docs is great if you only have Sass/SCSS files which do not have
@use
or@forward
rules.
If you use@use
in your Sass/SCSS files, for example, and you change a dependency Sass/SCSS file, Eleventy will compile it (if its filename doesn't start with "_"), but won't compile the dependant Sass/SCSS files, if you are following the instructions in the page of the official Docs.
In contrast to the existing solutions, eleventy-sass manages dependencies between Sass/SCSS files and makes maximum use of Eleventy's functionality. It is highly configurable, but it just works if you add it in your Eleventy config file.
For those of you who are curious about how eleventy-sass handles Sass/SCSS files, here is a brief explanation.
npm install eleventy-sass
Open up your Eleventy config file (probably .eleventy.js
) and use addPlugin()
:
const eleventySass = require("eleventy-sass");
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(eleventySass);
};
That's it. Only the above code in your .eleventy.js
, your Sass/SCSS files will be compiled to CSS and written in your output
directory.
Suppose your have the following .eleventy.js
. In this example, your input directory is "src" and your output directory is "dist" since it seems to be widely used settings in Eleventy community.
const eleventySass = require("eleventy-sass");
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(eleventySass);
return { dir: { input: "src", output: "dist" } };
};
and execute npx @11ty/eleventy
or npx @11ty/eleventy --serve
from your shell, you will get the following:
├── .eleventy.js
├── dist
│ ├── index.html
│ └── stylesheets
│ ├── accessibility
│ │ └── large-style.css
│ └── style.css
├── package-lock.json
├── package.json
└── src
├── _includes
│ └── tag-cloud.scss
├── index.md
└── stylesheets
├── accessibility
│ └── large-style.scss
├── style.scss
└── themes
└── _gruvbox.scss
As you can see, your Sass/SCSS files in your input ("src") directory, no matter how deep they are, were compiled and written in your output ("dist") directory unless they are in includes ("_includes") directory or their filenames start with "_".
Files in includes directory and files whose filenames start with "_" are not compiled directly but can be used from the other Sass/SCSS files with @use
and @forward
rules.
For example, _gruvbox.scss
file in the above example can be loaded from style.scss
file with @use "themes/gruvbox";
, and tag-cloud.scss
file can be loaded from style.scss
file with @use "tag-cloud";
. (The reason you don't have to specify it with @use "../_includes/tag-cloud";
is that includes ("_includes") directory is the default loadPaths
for eleventy-sass. I will describe loadPaths
in Sass options.)
Command samples in this documentation suppose Linux/macOS environments.
ELEVENTY_ENV=development npx @11ty/eleventy
Please replace, for example, the above command with the following:
set ELEVENTY_ENV=development & npx @11ty/eleventy
in cmd.exe
or
$env:ELEVENTY_ENV="development"; npx @11ty/eleventy
in PowerShell
.
eleventy-sass allows you to customize the behavior by options like follows:
// .eleventy.js
const eleventySass = require("eleventy-sass");
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(eleventySass, {
compileOptions: {
permalink: function(contents, inputPath) {
return (data) => data.page.filePathStem.replace(/^\/scss\//, "/css/") + ".css";
}
},
sass: {
style: "compressed",
sourceMap: false
},
rev: true
});
};
Options can be of type Object or Array.
Let's talk about passing an object as options first.
Basically, options
you pass as the second argument for addPlugin()
is used for options for eleventyConfig.addExtension()
, which eleventy-sass calls internally. The full options list is provided in the official Docs.
However, there are exceptions. Four properties, which are sass
, postcss
, rev
and when
, are not used for options for eleventyConfig.addExtension()
but used for other purposes.
sass
property is used for options for sass.compileString()
, a dart-sass API function, which eleventy-sass calls internally. For details, please read Sass options and the Sass documentation.
You can set PostCSS object to postcss
property, and eleventy-sass automatically apply the PostCSS object to the compiled CSS files.
// .eleventy.js
const eleventySass = require("eleventy-sass");
const postcss = require("postcss");
const rtlcss = require("rtlcss");
const cssnano = require("cssnano");
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(eleventySass, {
postcss: postcss([rtlcss, cssnano])
});
};
If you want to add revision hashes to the compiled CSS filenames, you can set true
to rev
property and add eleventy-plugin-rev as follows:
npm install eleventy-plugin-rev
// .eleventy.js
const pluginRev = require("eleventy-plugin-rev");
const eleventySass = require("eleventy-sass");
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(pluginRev);
eleventyConfig.addPlugin(eleventySass, {
rev: true
});
};
You can use filter functions, rev
and revvedOutput
, to add revision hashes.
Suppose you have the following project (input directory is src
and output directory is dist
):
.
├── .eleventy.js
├── dist
│ ├── blog
│ │ └── eleventy-quick-tips/index.html
│ ├── css
│ │ └── style-42df228b.css
│ └── index.html
└── src
├── index.md
├── blog
│ └── eleventy-quick-tips.md
└── scss
└── style.scss
The link tag to your CSS file will be the following:
<link rel="stylesheet" href="/css/style-42df228b.css" />
By using filters, you can write the above link tag as follows:
<link rel="stylesheet" href="{{ "/css/style.css" | rev }}" />
<link rel="stylesheet" href="{{ "/scss/style.scss" | revvedOutput }}" />
rev
and revvedOutput
filters must be relative from the output and input directories respectively and must be prefixed with /
, or a relative path from the current file.
For example, from eleventy-quick-tips.md
file, you can also write the link tag as follows:
<link rel="stylesheet" href="{{ "../../css/style.css" | rev }}" />
<link rel="stylesheet" href="{{ "../scss/style.scss" | revvedOutput }}" />
inputToRevvedOutput
filter will be deprecated. (Input paths for inputToRevvedOutput
must be relative paths from your project root, even if your input directory is not your project root.)
when
property is a special property, which is used for enabling or disabling the options object by environment/shell variables. For details, see when options.
You can set an array of objects to the second argument of addPlugin()
to configure eleventy-sass
with a finer granularity.
In this case, each element of the array is an options object.
First, eleventy-sass concatenates the default options and user-defined options.
I didn't write about it in the above Options object section, but even if you set an object as options, the plugin concatenates the default options and the user-defined options object(s) and the following process is exactly the same.
Elements of the array are options objects, and each object is enabled or disabled by its when
property. If the evaluation of the when
property is false, an options object will be disabled.
After the evaluations, eleventy-sass
merges all of the enabled options of the array, which was created from default options objects and user-defined options objects, and create a final options object.
When merging, elements at greater indexes can overwrite elements at smaller indexes. So, the default options are weakest and the last element of a user-defined options array is the strongest, and the default options can be overwritten by user-defined options.
Therefore, in general, general options should be placed at smaller indexes and more specific (conditional) options should be placed at greater indexes.
Let's take a look at an example:
// .eleventy.js
const eleventySass = require("eleventy-sass");
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(eleventySass, [
{
compileOptions: {
permalink: function(permalinkString, inputPath) {
return (data) => {
return data.page.filePathStem.replace(/^\/scss\//, "/css/") + ".css";
};
}
},
sass: {
style: "expanded",
sourceMap: true
}
}, {
rev: true,
when: { ELEVENTY_ENV: "stage" }
}, {
sass: {
style: "compressed",
sourceMap: false
},
rev: true,
when: [ { ELEVENTY_ENV: "production" }, { ELEVENTY_ENV: false } ]
}
]);
};
The first element defines permalink
, style
and sourceMap
, and it should be applied in whichever environments because there is no when
property.
The second element defines rev
, and it should be applied only when ELEVENTY_ENV
environment/shell variable is stage
.
The last element defines style
, sourceMap
and rev
, and it should be applied when ELEVENTY_ENV
is production
or ELEVENTY_ENV
is not defined (or ELEVENTY_ENV
is an empty string).
Therefore, when you run ELEVENTY_ENV=development npx @11ty/eleventy --serve
in Linux/macOS terminal for example, the result CSS files have source maps and expanded style (because of the first options object), but their filenames do not have revision hashes, because the second and the third options objects are disabled.
When you run ELEVENTY_ENV=stage npx @11ty/eleventy --serve
, the result CSS files have source maps and expanded style (because of the first options object), and their filenames have revision hashes (because of the second options object).
When you run npx @11ty/eleventy --serve
(if ELEVENTY_ENV
is not defined in .bashrc
, .zshrc
, etc.), ELEVENTY_ENV=production npx @11ty/eleventy --serve
or ELEVENTY_ENV= npx @11ty/eleventy --serve
, the result CSS files do not have source maps and have compressed style and their filenames have revision hashes (because of the last options object).
If you do not specify any options, eleventy-sass uses the default options.
// Default options
[
{
compileOptions: {
cache: true,
},
sass: {
includes: [/* The includes directory of your Eleventy project */],
style: "expended",
sourceMap: true,
sourceMapIncludeSources: true
}
}, {
sass: {
style: "compressed",
sourceMap: false
},
when: { ELEVENTY_ENV: (env) => env === undefined || "production".startsWith(env) }
}
]
The above is the default options. In short, they say eleventy-sass writes minified (compressed) CSS files without source maps if ELEVENTY_ENV
environment/shell variable is production
or not set, otherwise writes readable (expanded) CSS files with source maps.
For example, if you add the following line in your .bash_profile
, .bashrc
, .zshrc
, etc. on your local PC,
export ELEVENTY_ENV=development
the environment is development
locally, and the result CSS files will have readable style and source maps, because the second element of the default options is disabled and the only first options object is applied.
On the other hand, you will have CSS files with compressed style and without source maps, for example, in GitHub Actions by default, because there is no ELEVENTY_ENV
environment variable and the second element of the default options is enabled.
And, of course, you can specify an environment from your shell prompt like this:
ELEVENTY_ENV=production npx @11ty/eleventy --serve
For example, you have Sass/SCSS files in scss
directory in your input
directory. To output CSS files in css
directory in your output
directory, you can write a permalink function like this:
const eleventySass = require("eleventy-sass");
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(eleventySass, {
compileOptions: {
permalink: function(contents, inputPath) {
return (data) => {
return data.page.filePathStem.replace(/^\/scss\//, "/css/") + ".css";
};
}
}
});
};
For details, please refer to the compileOptions.permalink section in the official documentation.
Install PostCSS and Autoprefixer
npm install postcss autoprefixer
Configure your .eleventy.js
as follows:
const eleventySass = require("eleventy-sass");
const postcss = require("postcss");
const autoprefixer = require("autoprefixer");
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(eleventySass, {
postcss: postcss([autoprefixer])
});
};
Edit your package.json
, for example:
{
// ...
"browserslist": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version",
"last 1 ie version"
]
// ...
}
See Autoprefixer and Browserslist for more details.
You can see verbose outputs by using DEBUG
environment/shell variable (cf. DEBUG MODE).
DEBUG=Eleventy* npx @11ty/eleventy
If you want to see only the eleventy-sass's verbose outputs, change the value for DEBUG
environment/shell variable like this:
DEBUG=EleventySass* npx @11ty/eleventy
or, if you prefer more verbose outputs, like this:
DEBUG=*EleventySass* npx @11ty/eleventy
How eleventy-sass handles Sass/SCSS files
Eleventy watches files.
When Eleventy detects a file update, it emits an eleventy.beforeWatch
event, and eleventy-sass will receive the event and check if the file is a Sass/SCSS file.
If the file is a Sass/SCSS file, eleventy-sass changes the cache keys for the updated file and its dependant Sass/SCSS files, which invalidates the cached CSSes, so that Eleventy will compile all of the files affected.
When compiling Sass/SCSS files, Eleventy calls compile
function of the eleventy-sass.
The compile
function of eleventy-sass will compile Sass/SCSS files if they don't start with "_".
The actual compilation is done by sass, which is dart-sass.
By using the result of the compilation from dart-sass, eleventy-sass not only returns the compiled CSS, but also, if necessary, writes its source map file and updates the dependency map, which eleventy-sass has internally and uses for invalidating cached CSSes of dependant files as described above.