Skip to content

Commit 095bf64

Browse files
committed
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 gohugoio#6999
1 parent 7791a80 commit 095bf64

File tree

10 files changed

+501
-29
lines changed

10 files changed

+501
-29
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dock.sh
2020
GoBuilds
2121
dist
2222

23+
hugolib/hugo_stats.json
2324
resources/sunset.jpg
2425

2526
vendor

config/commonConfig.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,16 @@ import (
2929

3030
var DefaultBuild = Build{
3131
UseResourceCacheWhen: "fallback",
32+
WriteStats: false,
3233
}
3334

3435
// Build holds some build related condfiguration.
3536
type Build struct {
3637
UseResourceCacheWhen string // never, fallback, always. Default is fallback
38+
39+
// When enabled, will collect and write a hugo_stats.json with some build
40+
// related aggregated data (e.g. CSS class names).
41+
WriteStats bool
3742
}
3843

3944
func (b Build) UseResourceCache(err error) bool {

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ require (
5555
go.opencensus.io v0.22.0 // indirect
5656
gocloud.dev v0.15.0
5757
golang.org/x/image v0.0.0-20191214001246-9130b4cfad52
58-
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
58+
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553
5959
golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0 // indirect
6060
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
6161
golang.org/x/sys v0.0.0-20200107144601-ef85f5a75ddf // indirect

hugolib/hugo_sites.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,11 @@ func applyDeps(cfg deps.DepsCfg, sites ...*Site) error {
408408
s.Deps = d
409409

410410
// Set up the main publishing chain.
411-
pub, err := publisher.NewDestinationPublisher(d.PathSpec.BaseFs.PublishFs, s.outputFormatsConfig, s.mediaTypesConfig, cfg.Cfg)
411+
pub, err := publisher.NewDestinationPublisher(
412+
d.ResourceSpec,
413+
s.outputFormatsConfig,
414+
s.mediaTypesConfig,
415+
)
412416

413417
if err != nil {
414418
return err

hugolib/hugo_sites_build.go

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,17 @@ package hugolib
1616
import (
1717
"bytes"
1818
"context"
19+
"encoding/json"
1920
"fmt"
2021
"os"
22+
"path/filepath"
2123
"runtime/trace"
2224
"strings"
2325

26+
"github.com/gohugoio/hugo/publisher"
27+
28+
"github.com/gohugoio/hugo/hugofs"
29+
2430
"github.com/gohugoio/hugo/common/para"
2531
"github.com/gohugoio/hugo/config"
2632
"github.com/gohugoio/hugo/resources/postpub"
@@ -146,10 +152,10 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
146152
if err != nil {
147153
h.SendError(err)
148154
}
149-
}
150155

151-
if err := h.postProcess(); err != nil {
152-
h.SendError(err)
156+
if err = h.postProcess(); err != nil {
157+
h.SendError(err)
158+
}
153159
}
154160

155161
if h.Metrics != nil {
@@ -337,6 +343,12 @@ func (h *HugoSites) render(config *BuildCfg) error {
337343
}
338344

339345
func (h *HugoSites) postProcess() error {
346+
// Make sure to write any build stats to disk first so it's available
347+
// to the post processors.
348+
if err := h.writeBuildStats(); err != nil {
349+
return err
350+
}
351+
340352
var toPostProcess []resource.OriginProvider
341353
for _, s := range h.Sites {
342354
for _, v := range s.ResourceSpec.PostProcessResources {
@@ -422,3 +434,47 @@ func (h *HugoSites) postProcess() error {
422434
return g.Wait()
423435

424436
}
437+
438+
type publishStats struct {
439+
CSSClasses string `json:"cssClasses"`
440+
}
441+
442+
func (h *HugoSites) writeBuildStats() error {
443+
if !h.ResourceSpec.BuildConfig.WriteStats {
444+
return nil
445+
}
446+
447+
htmlElements := &publisher.HTMLElements{}
448+
for _, s := range h.Sites {
449+
stats := s.publisher.PublishStats()
450+
htmlElements.Merge(stats.HTMLElements)
451+
}
452+
453+
htmlElements.Sort()
454+
455+
stats := publisher.PublishStats{
456+
HTMLElements: *htmlElements,
457+
}
458+
459+
js, err := json.MarshalIndent(stats, "", " ")
460+
if err != nil {
461+
return err
462+
}
463+
464+
filename := filepath.Join(h.WorkingDir, "hugo_stats.json")
465+
466+
// Make sure it's always written to the OS fs.
467+
if err := afero.WriteFile(hugofs.Os, filename, js, 0666); err != nil {
468+
return err
469+
}
470+
471+
// Write to the destination, too, if a mem fs is in play.
472+
if h.Fs.Source != hugofs.Os {
473+
if err := afero.WriteFile(h.Fs.Destination, filename, js, 0666); err != nil {
474+
return err
475+
}
476+
}
477+
478+
return nil
479+
480+
}

hugolib/site_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,3 +980,47 @@ func TestRefIssues(t *testing.T) {
980980
b.AssertFileContent("public/post/nested-a/content-a/index.html", `Content: http://example.com/post/nested-b/content-b/`)
981981

982982
}
983+
984+
func TestClassCollector(t *testing.T) {
985+
b := newTestSitesBuilder(t)
986+
b.WithConfigFile("toml", `
987+
988+
[build]
989+
writeStats = true
990+
991+
`)
992+
993+
b.WithTemplates("index.html", `
994+
995+
<div id="el1" class="a b c">Foo</div>
996+
997+
Some text.
998+
999+
<div class="c d e" id="el2">Foo</div>
1000+
`)
1001+
1002+
b.WithContent("p1.md", "")
1003+
1004+
b.Build(BuildCfg{})
1005+
1006+
b.AssertFileContent("hugo_stats.json", `
1007+
{
1008+
"htmlElements": {
1009+
"tags": [
1010+
"div"
1011+
],
1012+
"classes": [
1013+
"a",
1014+
"b",
1015+
"c",
1016+
"d",
1017+
"e"
1018+
],
1019+
"ids": [
1020+
"el1",
1021+
"el2"
1022+
]
1023+
}
1024+
}
1025+
`)
1026+
}

0 commit comments

Comments
 (0)