Collect HTML elements during the build to use in PurgeCSS etc.

The main use case for this is to use with resources.PostProcess and resources.PostCSS with purgecss.

You would normally set it up to extract keywords from your templates, doing it from the full /public takes forever for bigger sites.

Doing the template thing misses dynamically created class names etc., and it's hard/impossible to set up in when using themes.

You can enable this in your site config:

```toml
[build]
  writeStats = true
```

It will then write a `hugo_stats.json` file to the project root as part of the build.

If you're only using this for the production build, you should consider putting it below `config/production`.

You can then set it up with PostCSS like this:

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

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

Fixes #6999
This commit is contained in:
Bjørn Erik Pedersen
2020-03-03 12:25:03 +01:00
parent 7791a804e2
commit 095bf64c99
10 changed files with 501 additions and 29 deletions

View File

@@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2020 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -18,7 +18,8 @@ import (
"io"
"sync/atomic"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/minifiers"
@@ -68,17 +69,21 @@ type Descriptor struct {
// DestinationPublisher is the default and currently only publisher in Hugo. This
// publisher prepares and publishes an item to the defined destination, e.g. /public.
type DestinationPublisher struct {
fs afero.Fs
min minifiers.Client
fs afero.Fs
min minifiers.Client
htmlElementsCollector *htmlElementsCollector
}
// NewDestinationPublisher creates a new DestinationPublisher.
func NewDestinationPublisher(fs afero.Fs, outputFormats output.Formats, mediaTypes media.Types, cfg config.Provider) (pub DestinationPublisher, err error) {
pub = DestinationPublisher{fs: fs}
pub.min, err = minifiers.New(mediaTypes, outputFormats, cfg)
if err != nil {
return
func NewDestinationPublisher(rs *resources.Spec, outputFormats output.Formats, mediaTypes media.Types) (pub DestinationPublisher, err error) {
fs := rs.BaseFs.PublishFs
cfg := rs.Cfg
var classCollector *htmlElementsCollector
if rs.BuildConfig.WriteStats {
classCollector = newHTMLElementsCollector()
}
pub = DestinationPublisher{fs: fs, htmlElementsCollector: classCollector}
pub.min, err = minifiers.New(mediaTypes, outputFormats, cfg)
return
}
@@ -111,16 +116,38 @@ func (p DestinationPublisher) Publish(d Descriptor) error {
}
defer f.Close()
_, err = io.Copy(f, src)
var w io.Writer = f
if p.htmlElementsCollector != nil && d.OutputFormat.IsHTML {
w = io.MultiWriter(w, newHTMLElementsCollectorWriter(p.htmlElementsCollector))
}
_, err = io.Copy(w, src)
if err == nil && d.StatCounter != nil {
atomic.AddUint64(d.StatCounter, uint64(1))
}
return err
}
func (p DestinationPublisher) PublishStats() PublishStats {
if p.htmlElementsCollector == nil {
return PublishStats{}
}
return PublishStats{
HTMLElements: p.htmlElementsCollector.getHTMLElements(),
}
}
type PublishStats struct {
HTMLElements HTMLElements `json:"htmlElements"`
}
// Publisher publishes a result file.
type Publisher interface {
Publish(d Descriptor) error
PublishStats() PublishStats
}
// XML transformer := transform.New(urlreplacers.NewAbsURLInXMLTransformer(path))