Have you read the Contributing Guidelines on issues?
Prerequisites
Description
With future.faster enabled, a Sass-generated UTF-8 BOM ends up in the middle of the concatenated CSS bundle, and LightningCssMinimizerRspackPlugin silently deletes the rule that follows it. The build exits 0 with no warning or error, and the asset referenced by the deleted rule is still emitted, so nothing looks wrong in build/.
On my site the casualty was the upright @font-face of a self-hosted code font. Only the font-style: italic face survived, and since CSS font matching falls back to an italic face when the family has no normal one, every code block on the production site rendered in italic. It shipped that way for two weeks, because the site builds clean and the .woff2 is sitting right there in build/assets/fonts/.
The chain, each step verified on my site:
- Dart Sass prepends a UTF-8 BOM to compressed output whenever it contains a non-ASCII character. Mine has one
content: "▸" in a ::before, and that alone is enough — sass --style=compressed emits ef bb bf before the first rule, --no-charset does not, and replacing the ▸ with an ASCII character makes the BOM disappear.
- Until postcss 8.5.23 a postcss pass silently stripped that BOM. postcss 8.5.24 changed to "Preserve the BOM after the processing", so it now survives into the bundle. My site was on postcss 8.5.10 and fine; a lockfile migration pulled 8.5.25 and the code font went italic that day.
- The Sass chunk is concatenated after the Infima CSS, so the BOM lands mid-file rather than at offset 0. In my pre-minification bundle it sits at byte 79316, as
7d 0a 0a ef bb bf 40 66 6f 6e 74 2d 66 61 63 65 — }\n\n<BOM>@font-face.
- The minifier drops that
@font-face and the BOM together. The shipped CSS contains neither. A --no-minify build of the same commit contains both rules, which is what identifies the minifier as the step responsible.
Step 3 is where valid inputs turn into invalid output, and it is not really a Sass problem. Per CSS Syntax §3.2 a BOM is removed during decoding, so a standalone stylesheet that starts with one is perfectly valid. Concatenating that same stylesheet into position 79316 of a bundle is what makes the BOM meaningful to the tokenizer — it stops being an encoding marker and becomes a U+FEFF ident character sitting in front of an at-rule. The CSS extractor (CssExtractRspackPlugin on the faster path, mini-css-extract-plugin otherwise) concatenates module contents verbatim, so two valid modules produce one invalid bundle. I could not find an existing BOM issue on either repo.
The related parse-side report is parcel-bundler/lightningcss#338 (open since 2022), though the fix proposed there — stripping a leading BOM — would not help this case, since the BOM is not at offset 0.
What makes it a Docusaurus-side problem is the failure mode rather than the parse itself. Running the JS lightningcss package (1.33.0) over my exact pre-minification bundle throws:
Unexpected token AtKeyword("font-face") { line: 3097, column: 2 }
A thrown error is survivable — someone sees it and fixes it. But future.faster selects the Rspack bundler, so the minimizer is rspack.LightningCssMinimizerRspackPlugin (packages/bundler/src/minification.ts), not that JS package. On identical input it neither throws nor warns. It removes the rule and the build reports success. I confirmed the JS package is never called: I patched lightningcss.transform via NODE_OPTIONS=--require for a full build and it was not invoked once, while the rule still vanished from the output.
So the two Lightning CSS integrations Docusaurus ships disagree on the same input, and the default one (faster) is the silent one. A site owner gets a clean build and a wrong page, with nothing connecting the two.
This is not docusaurus-plugin-sass doing something unusual. Any site whose compiled CSS contains one non-ASCII character is exposed once faster is on, and the trigger was a transitive postcss bump, not a change to anyone's stylesheet.
Reproducible demo
No hosted repro — it needs a production build to observe. The recipe below reproduces from a clean create-docusaurus in a couple of minutes.
Steps to reproduce
npx create-docusaurus@latest my-site classic --typescript
npm i -D docusaurus-plugin-sass sass, add "docusaurus-plugin-sass" to plugins, set future: {faster: true}.
- Rename
src/css/custom.css to custom.scss, point theme.customCss at it, and make its first rules a font family, with a non-ASCII glyph anywhere in the file:
@font-face {
font-family: 'Test Font';
src: url('/fonts/test.woff2') format('woff2');
font-style: normal;
}
@font-face {
font-family: 'Test Font';
src: url('/fonts/test-italic.woff2') format('woff2');
font-style: italic;
}
.some-class::before { content: "▸"; } // any non-ASCII char — this is the trigger
npm run build
grep -o "@font-face{[^}]*}" build/assets/css/*.css — only the italic one is there.
npm run build -- --no-minify and grep again — both are there.
Expected behavior
Both @font-face rules survive minification. Failing that, a build that cannot parse part of its own CSS says so, instead of dropping a rule and reporting success.
Actual behavior
Only the second @font-face is in the built CSS:
@font-face{font-family:Test Font;src:url(/assets/fonts/test-italic-<hash>.woff2)format("woff2");font-style:italic}
Exit code 0, no warnings. build/assets/fonts/ still contains both files; only the url() reference to the first is gone.
Suggested fix: strip from CSS assets before they reach the minimizer, e.g. a compilation.hooks.processAssets pass at a stage before minimize. A leading-only strip is not enough — the BOM has to be removed wherever it lands after concatenation. This is bundler-agnostic, so it covers both the faster and the webpack path, and it also covers a hand-authored .css file that happens to be saved with a BOM.
I am deliberately not suggesting sassOptions: {charset: false} as a project default. It is what I used on my own site — ["docusaurus-plugin-sass", {sassOptions: {charset: false}}], which works because the plugin forwards options straight to sass-loader — but sass-loader strongly discourages it, and it is only safe for me because my host sends content-type: text/css; charset=utf-8. It is a workaround for a site owner who has already diagnosed the problem, not a default for everyone. I only found it by diffing a --no-minify build against a normal one.
I searched open and closed issues for charset, lightningcss, font-face, and minify css and did not find this reported.
Your environment
- Docusaurus 3.10.2,
future: {faster: true, v4: true}
@docusaurus/faster 3.10.2, @rspack/core 1.7.12, lightningcss (JS) 1.33.0
docusaurus-plugin-sass 0.2.6, sass-loader 16.0.8, sass 1.102.0
- postcss 8.5.25
- Node 24.14.0, bun 1.3.14, macOS (darwin arm64)
Self-service
Have you read the Contributing Guidelines on issues?
Prerequisites
npm run clearoryarn clearcommand.rm -rf node_modules yarn.lock package-lock.jsonand re-installing packages.Description
With
future.fasterenabled, a Sass-generated UTF-8 BOM ends up in the middle of the concatenated CSS bundle, andLightningCssMinimizerRspackPluginsilently deletes the rule that follows it. The build exits 0 with no warning or error, and the asset referenced by the deleted rule is still emitted, so nothing looks wrong inbuild/.On my site the casualty was the upright
@font-faceof a self-hosted code font. Only thefont-style: italicface survived, and since CSS font matching falls back to an italic face when the family has no normal one, every code block on the production site rendered in italic. It shipped that way for two weeks, because the site builds clean and the.woff2is sitting right there inbuild/assets/fonts/.The chain, each step verified on my site:
content: "▸"in a::before, and that alone is enough —sass --style=compressedemitsef bb bfbefore the first rule,--no-charsetdoes not, and replacing the▸with an ASCII character makes the BOM disappear.7d 0a 0a ef bb bf 40 66 6f 6e 74 2d 66 61 63 65—}\n\n<BOM>@font-face.@font-faceand the BOM together. The shipped CSS contains neither. A--no-minifybuild of the same commit contains both rules, which is what identifies the minifier as the step responsible.Step 3 is where valid inputs turn into invalid output, and it is not really a Sass problem. Per CSS Syntax §3.2 a BOM is removed during decoding, so a standalone stylesheet that starts with one is perfectly valid. Concatenating that same stylesheet into position 79316 of a bundle is what makes the BOM meaningful to the tokenizer — it stops being an encoding marker and becomes a U+FEFF ident character sitting in front of an at-rule. The CSS extractor (
CssExtractRspackPluginon thefasterpath,mini-css-extract-pluginotherwise) concatenates module contents verbatim, so two valid modules produce one invalid bundle. I could not find an existing BOM issue on either repo.The related parse-side report is parcel-bundler/lightningcss#338 (open since 2022), though the fix proposed there — stripping a leading BOM — would not help this case, since the BOM is not at offset 0.
What makes it a Docusaurus-side problem is the failure mode rather than the parse itself. Running the JS
lightningcsspackage (1.33.0) over my exact pre-minification bundle throws:A thrown error is survivable — someone sees it and fixes it. But
future.fasterselects the Rspack bundler, so the minimizer isrspack.LightningCssMinimizerRspackPlugin(packages/bundler/src/minification.ts), not that JS package. On identical input it neither throws nor warns. It removes the rule and the build reports success. I confirmed the JS package is never called: I patchedlightningcss.transformviaNODE_OPTIONS=--requirefor a full build and it was not invoked once, while the rule still vanished from the output.So the two Lightning CSS integrations Docusaurus ships disagree on the same input, and the default one (
faster) is the silent one. A site owner gets a clean build and a wrong page, with nothing connecting the two.This is not
docusaurus-plugin-sassdoing something unusual. Any site whose compiled CSS contains one non-ASCII character is exposed oncefasteris on, and the trigger was a transitive postcss bump, not a change to anyone's stylesheet.Reproducible demo
No hosted repro — it needs a production build to observe. The recipe below reproduces from a clean
create-docusaurusin a couple of minutes.Steps to reproduce
npx create-docusaurus@latest my-site classic --typescriptnpm i -D docusaurus-plugin-sass sass, add"docusaurus-plugin-sass"toplugins, setfuture: {faster: true}.src/css/custom.csstocustom.scss, pointtheme.customCssat it, and make its first rules a font family, with a non-ASCII glyph anywhere in the file:npm run buildgrep -o "@font-face{[^}]*}" build/assets/css/*.css— only the italic one is there.npm run build -- --no-minifyand grep again — both are there.Expected behavior
Both
@font-facerules survive minification. Failing that, a build that cannot parse part of its own CSS says so, instead of dropping a rule and reporting success.Actual behavior
Only the second
@font-faceis in the built CSS:Exit code 0, no warnings.
build/assets/fonts/still contains both files; only theurl()reference to the first is gone.Suggested fix: strip
from CSS assets before they reach the minimizer, e.g. acompilation.hooks.processAssetspass at a stage beforeminimize. A leading-only strip is not enough — the BOM has to be removed wherever it lands after concatenation. This is bundler-agnostic, so it covers both thefasterand the webpack path, and it also covers a hand-authored.cssfile that happens to be saved with a BOM.I am deliberately not suggesting
sassOptions: {charset: false}as a project default. It is what I used on my own site —["docusaurus-plugin-sass", {sassOptions: {charset: false}}], which works because the plugin forwards options straight to sass-loader — but sass-loader strongly discourages it, and it is only safe for me because my host sendscontent-type: text/css; charset=utf-8. It is a workaround for a site owner who has already diagnosed the problem, not a default for everyone. I only found it by diffing a--no-minifybuild against a normal one.I searched open and closed issues for
charset,lightningcss,font-face, andminify cssand did not find this reported.Your environment
future: {faster: true, v4: true}@docusaurus/faster3.10.2,@rspack/core1.7.12,lightningcss(JS) 1.33.0docusaurus-plugin-sass0.2.6, sass-loader 16.0.8, sass 1.102.0Self-service