A production build
Production is the version of a site uploaded to the server for visitors. While working you want readable CSS with source maps; for publishing you want it minified and without maps. Let us put all the plugins of the course into one chain with a switch between these modes.
Project structure
blog/
├── index.html
├── css/
│ └── main.css ← build output
├── styles/
│ ├── main.css
│ ├── base.css
│ └── post-card.css
├── postcss.config.js
└── package.jsonInstall all the plugins at once:
npm install --save-dev postcss postcss-cli postcss-import postcss-preset-env cssnanoA config with a switch
The config file can export a function instead of an object. postcss-cli passes it a ctx object, where ctx.env is the value of the NODE_ENV environment variable:
// postcss.config.js
import postcssImport from 'postcss-import';
import postcssPresetEnv from 'postcss-preset-env';
import cssnano from 'cssnano';
export default (ctx) => {
const isProduction = ctx.env === 'production';
return {
map: isProduction ? false : { inline: false },
plugins: [
postcssImport(),
postcssPresetEnv(),
isProduction ? cssnano() : false,
],
};
};In development cssnano is replaced with false — PostCSS skips such list items. The source map, on the contrary, is written only in development.
Scripts
The --env flag sets NODE_ENV the same way on Windows, macOS and Linux:
{
"scripts": {
"dev": "postcss styles/main.css -o css/main.css --watch",
"build": "postcss styles/main.css -o css/main.css --env production"
},
"browserslist": ["defaults"]
}Source styles
/* styles/main.css */
@import "./base.css";
@import "./post-card.css";/* styles/base.css */
@custom-media --tablet (width >= 768px);
body {
margin: 0;
font-family: system-ui, sans-serif;
}/* styles/post-card.css */
.post-card {
padding: 16px;
border-radius: 8px;
user-select: none;
& h2 {
margin: 0;
}
@media (--tablet) {
padding: 32px;
}
}Development
npm run dev watches all three files — postcss-cli knows which of them are included through @import. css/main.css holds readable CSS and a link to the map:
body {
margin: 0;
font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans, sans-serif;
}
.post-card {
padding: 16px;
border-radius: 8px;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.post-card h2 {
margin: 0;
}
@media (min-width: 768px) {
.post-card {
padding: 32px;
}
}
/*# sourceMappingURL=main.css.map */The files are joined, nesting is unwrapped, --tablet is replaced with the condition, prefixes are added, and for older browsers a list of similar system fonts is added to system-ui.
Publishing
npm run build produces a single line without a map:
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif}.post-card{padding:16px;border-radius:8px;-webkit-user-select:none;-moz-user-select:none;user-select:none}.post-card h2{margin:0}@media(min-width:768px){.post-card{padding:32px}}You upload index.html and the css/ folder to the server. There is no need to upload styles/ or node_modules/.
In a Vite project
In Vite the chain is shorter: it handles @import and minification by itself, so postcss.config.js keeps only postcssPresetEnv(). npm run build puts the finished CSS into dist/assets/.
Common mistake
Writing NODE_ENV=production postcss ... in a script. It works on macOS and Linux, but fails in the standard Windows command prompt, so a teammate on Windows cannot build the project. Use the --env production flag — postcss-cli sets the variable itself.