Difference between postCSS and resources.PostCSS

Essentially for historic reasons (this was how I used to build my static sites before I migrated all of them to Hugo) I’m still using Codekit to compile my Sass files to CSS. As part of this process both purgecss and CSSO (a very efficient minifier) are applied, in sequence, by Codekit, and the result compiled by Codekit at main.css is a pretty tiny css file (about 7kb).

Also for historic reasons, I previously used Grunt to subsequently concatenate (Codekit doesn’t do concatenation out of the box) normalize.css with the main.css file compiled by Codekit, and the resulting single css file would then referenced in the site’s head.

Since Hugo does concatenation, I’ve now removed Grunt from the above workflow and generate my css — from the main.css file still compiled by Codekit — using the following code, which takes care of the concatenation, minification and fingerprinting/cache-busting:

{{ $reset := resources.Get "css/normalize.css" }}
{{ $main := resources.Get "css/main.css" }}
{{ $styles := slice $reset $main | resources.Concat "css/styles.css" | minify | fingerprint }}
<link rel="stylesheet" href="{{ $styles.RelPermalink }}" integrity="{{ $styles.Data.Integrity }}" />
{{ end }}

I’d like to take this process to its logical conclusion and use Hugo Pipes for all the workflow described above, removing the need to keep Codekit running while I’m working on the local site. This means installing the postCSS packages for purgeCSS and CSSO and referencing both of them, consecutively, in postcss.config.js, as follows:

const purgecss = require("@fullhuman/postcss-purgecss")({
  content: ["./hugo_stats.json"],
  defaultExtractor: (content) => {
    const els = JSON.parse(content).htmlElements;
    return [...(els.tags || []), ...(els.classes || []), ...(els.ids || [])];
  },
  safelist: [],
});

module.exports = {
  plugins: [
    ...(process.env.HUGO_ENVIRONMENT === "production" ? [purgecss] : []),
  ],
};

import csso from 'postcss-csso';

export const plugins = [
    csso({
        restructure: false
    })
];

The code I’m trying to use in the head to reproduce the workflow without the Codekit SASS/purgeCSS/CSSO part looks as follows:

{{ $reset := resources.Get "css/normalize.css" }}
{{ with resources.Get "scss/main.scss" | toCSS | postCSS }}
{{ $main := resources.Get "css/main.css" }}
{{ $styles := slice $reset $main | resources.Concat "css/styles.css" | minify | fingerprint }}
<link rel="stylesheet" href="{{ $styles.RelPermalink }}" integrity="{{ $styles.Data.Integrity }}" />
{{ end }}

My question is, inter alia (there probably are other mistakes in the above setup, both the postcss.config.js files and the stylesheet referenced in the head) whether I should reference {{ with resources.Get "scss/main.scss" | toCSS | postCSS }}, or replace postCSS by resources.PostCSS, which is what purgeCSS suggests on this page. I basically don’t understand the difference between those two parameters.

Any suggestions would be gratefully received.

postCSS is an alias.

https://gohugo.io/hugo-pipes/postcss/

1 Like

Ah, i should have worked that out. So my code looks good then?

Again, see documentation.

Minify, fingerprint, and post process in production environment only.

Also: https://purgecss.com/guides/hugo.html

1 Like

This reply was amended to reflect the fact the code—after taking into account jmooring’s remarks in his comment above—now works in development and production mode if CSSO is left out of postcss.config.js.

I’m updating this topic as I’ve changed the attempted setup from the one described above, which contained several errors, and while I can now get the build to work, using the revised code below, in development mode, it’s still throwing errors in production when I add CSSO to the postcss.config.js file. It would appear, in essence, that while the code referenced below for the stylesheet is working, errors in postcss.config.js are causing the production build to fail if I try to include CSSO in it.

I. New code

1. stylesheet

This integrates dart-sass, since Hugo, which uses libsass by default, was breaking modern CSS in SCSS, which I occasionally use, such as:

background-color: hsl(var(--my-purple-hsl) / 50%) ;

Using the dart transpiler, and testing that it works properly is explained very clearly here.

{{ $reset := resources.Get "css/normalize.css" }}
{{ $sass := resources.Get "sass/main.scss" }}
{{ $opts := dict "transpiler" "dartsass" "targetPath" "css/main.css" }}
{{ $main := $sass | toCSS $opts }}
{{ $css := resources.Get "css/main.css" }}
{{ $styles := slice $reset $css | resources.Concat "css/styles.css" }}
{{ if hugo.IsProduction }}
{{ $styles = $styles | postCSS | minify | fingerprint }}
{{ end }}
<link
  rel="stylesheet"
  href="{{ $styles.RelPermalink }}"
  {{ if hugo.IsProduction -}}
    integrity="{{ $styles.Data.Integrity }}"
  {{- end }}
/>

2. postcss.config.js

const purgecss = require("@fullhuman/postcss-purgecss")({
  content: ["./hugo_stats.json"],
  defaultExtractor: (content) => {
    const els = JSON.parse(content).htmlElements;
    return [...(els.tags || []), ...(els.classes || []), ...(els.ids || [])];
  },
  safelist: [],
});

module.exports = {
  plugins: [
    ...(process.env.HUGO_ENVIRONMENT === "production" ? [purgecss] : []),
  ],
};

Comment: this essentially returns the postCSS configuration to the one suggested here. The reason for this is that I decided, for the moment, to get the postCSS to work with just purgeCSS, omitting CSSO, because attempting to add CSSO caused an error which I haven’t been able to correct, asking me to either add the following to my package.json file:

{
  "type": "module",
}

or to rename the referenced .js file to .mjs.

Yet adding this, or renaming the csso.js in my node_modules to csso.mjs which is the alternative solution suggested, continued to cause errors. So for the sake of attempting to solve the issues sequentially, rather than all at once, I’ve deferred the inclusion of CSSO until I’ve got purgeCSS to work.

II. Testing the new code

The above setup works in development mode and also in production mode, but sadly I’ve been unable to add CSSO back into the build process.

My deleted postcss.config.js, which additionally included the code to invoke CSSO recommended here, and which I have now replaced by the one listed above which references purgeCS only, was previously as follows:

const purgecss = require("@fullhuman/postcss-purgecss")({
  content: ["./hugo_stats.json"],
  defaultExtractor: (content) => {
    const els = JSON.parse(content).htmlElements;
    return [...(els.tags || []), ...(els.classes || []), ...(els.ids || [])];
  },
  safelist: [],
});

module.exports = {
  plugins: [
    ...(process.env.HUGO_ENVIRONMENT === "production" ? [purgecss] : []),
  ],
};

import csso from 'postcss-csso';

export const plugins = [
    csso({
        restructure: false
    })
];

Comment: this referenced both purgeCSS and CSSO. The code referenced at the beginning of this edit at (1) and (2) removes the reference to CSSO. when I try to revert to this version of postcss.config.js, even if I add "type": "module" to my package.json, the build fails

I’d be most grateful if someone could suggest a way of adding CSSO to postcss.config.js, using the method suggested here, and make it work.

It seems like you want to minify (with CSSO) the CSS, then minify it again. Is that correct? If so, why?

Well of course once (or if…) I get CSSO working, I’ll remove the extra Hugo Pipes minification.

CSSO does seem to have an edge, especially as it can restructure code on top of just minifying it.

I’ll take a look at this, and let you know if I see anything. But I wonder if the effort exceeds the value (i.e., a negative ROI).

Part of the value lies in the satisfaction of having cracked a tougher-than-usual nut. Also, it annoys me that it was working flawlessly with Codekit, and producing a very small, snappy css file, and not here.

But you’re right, of course.

Starting with un-minified bootstrap.css (280812 B)…

Minifier Raw (bytes) gzipped (bytes) Build time (ms)
tdewolff/minify (built-in) 231966 30982 20
postcss-csso (restructure true) 229003 31400 1120

Just saying…

2 Likes

Yes, one of course completely sees your point. But CSSO is more than just a minifier: it performs three sort of transformations: cleaning (removing redundants), compression (replacement for the shorter forms) and restructuring (merge of declarations, rules and so on). Tdewolff/minify states explicitely that it does not do restructuring.

The speed benefit isn’t just correlated to the size of the css file, otherwise I completely agree that there would be no point in pursuing it.

I agree. I’ve added build time and gzipped (as served) file sizes to the comparison above.

Again, I’ll look at this, but I have a difficult time getting excited about it.

1 Like

This works for me:

const csso = require('postcss-csso')({
  restructure: true
});
const purgecss = require('@fullhuman/postcss-purgecss')({
  content: ['./hugo_stats.json'],
  defaultExtractor: content => {
    const els = JSON.parse(content).htmlElements;
    return [
      ...(els.tags || []),
      ...(els.classes || []),
      ...(els.ids || []),
    ];
  }
});

module.exports = {
  plugins: [
    ...(process.env.HUGO_ENVIRONMENT === 'production' ? [csso,purgecss] : [])
  ]
};

Well, that’s pretty convincing: those build-times pretty much nail it, assuming the code they correspond to wasn’t particularly atypical.

I’ve fixed the safelist locally in the mean time, so I’ll now try deploying this shiny new Hugo-only build process to Cloudflare Pages, using the same build command and environment setup you recommend for Netifly.

Hm. In Cloudflare Pages,I’m using the following build configuration, directly adapted from the one suggested for Netlify here:

npm install sass-embedded-linux-x64@${DART_SASS_VERSION} && mkdir -p /opt/build/repo/node_modules/.bin &&  cp -r /opt/build/repo/node_modules/sass-embedded-linux-x64/dart-sass-embedded/* /opt/build/repo/node_modules/.bin/ && dart-sass-embedded --version &&  hugo --gc --minify

with the following environment variables set:

DART_SASS_VERSION = 1.57.1
HUGO_VERSION = 0.112.5
NODE_VERSION = v14.19.0

The build fails when running this command:

mkdir -p /opt/build/repo/node_modules/.bin

with the following error message:

mkdir: cannot create directory ‘/opt/build/repo’: Permission denied

I’ve uploaded the complete build log here: shared.via.dj – donaldjenkins.8c68d5cd-c2d4-416e-9183-ef44f2169adb.log (10 KB).

According to this discussion page, the Cloudflare Pages build script can write to nonexistent files but only if the folder structure already exists. If that’s not the case, the commenter continues, ‘the following code can create the required folder(s):’

if (!fs.existsSync(path.replace("/index.html", ""))){
      fs.mkdirSync(path.replace("/index.html", ""), { recursive: true });
    }

I’m going to try and research this further with Cloudflare as their documentation is silent on the subject, but in the mean time any suggestions would of course be welcome.

I did find this, and a sequel, which includes a script for gatting dart-saas working with Hugo on Cloudflare Pages. I’ll be trying that out as well.

The convoluted method of installing the embedded Dart Sass transpiler when using Netlify is required due to the environment’s PATH.

See https://developers.cloudflare.com/pages/platform/language-support-and-tools/. Note that (in v2) the embedded Dart Sass transpiler is preinstalled.

image

Thanks! Reverting to just hugo --gc --minify worked, although for some reason dart-sass is trimming styles much more aggressively in the Cloudflare Pages deployment than it was, based the same postcss.config.js, in the local production build. It may possibly be connected with having set DART_SASS_VERSION = 1.57.1, rather than a more recent version, in the environment variables.

I’ll have to debug this before I can flip the switch, but will post an update once I have.

It works. I now have two sites serving identical content. The only difference between the two is the existence of postcss.config.js and the code in layouts/partials/head/styles.html:

1. the main website (repo), built using a css file compiled using dart-sass and CSSO in Codekit, followed by this stylesheet code which uses Hugo Pipes to do some concatenation:

{{ $reset := resources.Get "css/normalize.css" }}
{{ $main := resources.Get "css/main.css" }}
{{ $styles := slice $reset $main | resources.Concat "css/styles.css" | minify | fingerprint }}
<link rel="stylesheet" href="{{ $styles.RelPermalink }}" integrity="{{ $styles.Data.Integrity }}" />

2. the staging website (repo), fully built using Hugo Pipes, as follows:

2.1. stylesheet

{{ $reset := resources.Get "css/normalize.css" }}
{{ $sass := resources.Get "sass/main.scss" }}
{{ $opts := dict "transpiler" "dartsass" "targetPath" "css/main.css" }}
{{ $main := resources.Get "sass/main.scss" | toCSS $opts }}
{{ $css := resources.Get "css/main.css" }}
{{ $styles := slice $reset $css | resources.Concat "css/styles.css" }}
{{ if hugo.IsProduction }}
{{ $styles = $styles | postCSS | minify | fingerprint }}
{{ end }}
<link
  rel="stylesheet"
  href="{{ $styles.RelPermalink }}"
  {{ if hugo.IsProduction -}}
    integrity="{{ $styles.Data.Integrity }}"
  {{- end }}
/>

2.2. postcss.config.js

const csso = require('postcss-csso')({
  restructure: true
});
const purgecss = require('@fullhuman/postcss-purgecss')({
  content: ['./hugo_stats.json'],
  defaultExtractor: content => {
    const els = JSON.parse(content).htmlElements;
    return [
      ...(els.tags || []),
      ...(els.classes || []),
      ...(els.ids || []),
    ];
  },
  safelist: ['data-theme'],
});

module.exports = {
  plugins: [
    ...(process.env.HUGO_ENVIRONMENT === 'production' ? [csso, purgecss] : [])
  ]
};

The css is identical, despite being produced with two different workflows:

  • Codekit matches dart-sass to the styles in the html files;
  • Hugo Pipes matches them to hugo_stats.json.

The only discernable difference between the two was that Hugo Pipes failed to detect the dark-mode styles, which I had to add to the safelist.

And the reason dart-sass didn’t work initially was… because I had forgotten to remove hugo_stats.json from my .gitignore:roll_eyes:

According to this post, it will be possible, in Cloudflare Pages v2, now in public beta, for the

Embedded Dart Sass binary be part of that image, with the version specifiable through use of an environment variable. And it’s my pleasure to tell you that v2 will provide that option, through an EMBEDDED_DART_SASS_VERSION env var [*]

[*] This announcement about Embedded Dart Sass becoming part of the Cloudflare Pages Build is somewhat confusing, however, as the corresponding repository (whose most recent release, 1.62.1, is the one referenced as default in the Cloudflare announcement) was archived two months ago and now states:

This is no longer the repository for Embedded Dart Sass. The embedded compiler has been merged into the primary Dart Sass repository, and further releases will be included as part of Dart Sass itself. The embedded compiler can be accessed by running sass --embedded .

When I initially tried to switch the build system version to the public beta, the build failed (log), but deleting any unused npm packages and upgrading NODE_VERSION to v18.16.0 did the trick.

Anyone wishing to use Hugo Pipes to compile their SASS using dart-sass should be able to do so using the above workflow.

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.