// Copyright 2019 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. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "fmt" "regexp" "sort" "strings" "github.com/bep/logg" "github.com/gobwas/glob" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/herrors" "github.com/mitchellh/mapstructure" "github.com/spf13/cast" ) type BaseConfig struct { WorkingDir string CacheDir string ThemesDir string PublishDir string } type CommonDirs struct { // The directory where Hugo will look for themes. ThemesDir string // Where to put the generated files. PublishDir string // The directory to put the generated resources files. This directory should in most situations be considered temporary // and not be committed to version control. But there may be cached content in here that you want to keep, // e.g. resources/_gen/images for performance reasons or CSS built from SASS when your CI server doesn't have the full setup. ResourceDir string // The project root directory. WorkingDir string // The root directory for all cache files. CacheDir string // The content source directory. // Deprecated: Use module mounts. ContentDir string // Deprecated: Use module mounts. // The data source directory. DataDir string // Deprecated: Use module mounts. // The layout source directory. LayoutDir string // Deprecated: Use module mounts. // The i18n source directory. I18nDir string // Deprecated: Use module mounts. // The archetypes source directory. ArcheTypeDir string // Deprecated: Use module mounts. // The assets source directory. AssetDir string } type LoadConfigResult struct { Cfg Provider ConfigFiles []string BaseConfig BaseConfig } var defaultBuild = BuildConfig{ UseResourceCacheWhen: "fallback", BuildStats: BuildStats{}, CacheBusters: []CacheBuster{ { Source: `(postcss|tailwind)\.config\.js`, Target: cssTargetCachebusterRe, }, }, } // BuildConfig holds some build related configuration. type BuildConfig struct { // When to use the resource file cache. // One of never, fallback, always. Default is fallback UseResourceCacheWhen string // When enabled, will collect and write a hugo_stats.json with some build // related aggregated data (e.g. CSS class names). // Note that this was a bool <= v0.115.0. BuildStats BuildStats // Can be used to toggle off writing of the IntelliSense /assets/jsconfig.js // file. NoJSConfigInAssets bool // Can used to control how the resource cache gets evicted on rebuilds. CacheBusters []CacheBuster } // BuildStats configures if and what to write to the hugo_stats.json file. type BuildStats struct { Enable bool DisableTags bool DisableClasses bool DisableIDs bool } func (w BuildStats) Enabled() bool { if !w.Enable { return false } return !w.DisableTags || !w.DisableClasses || !w.DisableIDs } func (b BuildConfig) clone() BuildConfig { b.CacheBusters = append([]CacheBuster{}, b.CacheBusters...) return b } func (b BuildConfig) UseResourceCache(err error) bool { if b.UseResourceCacheWhen == "never" { return false } if b.UseResourceCacheWhen == "fallback" { return herrors.IsFeatureNotAvailableError(err) } return true } // MatchCacheBuster returns the cache buster for the given path p, nil if none. func (s BuildConfig) MatchCacheBuster(logger loggers.Logger, p string) (func(string) bool, error) { var matchers []func(string) bool for _, cb := range s.CacheBusters { if matcher := cb.compiledSource(p); matcher != nil { matchers = append(matchers, matcher) } } if len(matchers) > 0 { return (func(cacheKey string) bool { for _, m := range matchers { if m(cacheKey) { return true } } return false }), nil } return nil, nil } func (b *BuildConfig) CompileConfig(logger loggers.Logger) error { for i, cb := range b.CacheBusters { if err := cb.CompileConfig(logger); err != nil { return fmt.Errorf("failed to compile cache buster %q: %w", cb.Source, err) } b.CacheBusters[i] = cb } return nil } func DecodeBuildConfig(cfg Provider) BuildConfig { m := cfg.GetStringMap("build") b := defaultBuild.clone() if m == nil { return b } // writeStats was a bool <= v0.115.0. if writeStats, ok := m["writestats"]; ok { if bb, ok := writeStats.(bool); ok { m["buildstats"] = BuildStats{Enable: bb} } } err := mapstructure.WeakDecode(m, &b) if err != nil { return b } b.UseResourceCacheWhen = strings.ToLower(b.UseResourceCacheWhen) when := b.UseResourceCacheWhen if when != "never" && when != "always" && when != "fallback" { b.UseResourceCacheWhen = "fallback" } return b } // SitemapConfig configures the sitemap to be generated. type SitemapConfig struct { // The page change frequency. ChangeFreq string // The priority of the page. Priority float64 // The sitemap filename. Filename string // Whether to disable page inclusion. Disable bool } func DecodeSitemap(prototype SitemapConfig, input map[string]any) (SitemapConfig, error) { err := mapstructure.WeakDecode(input, &prototype) return prototype, err } // Config for the dev server. type Server struct { Headers []Headers Redirects []Redirect compiledHeaders []glob.Glob compiledRedirects []glob.Glob } func (s *Server) CompileConfig(logger loggers.Logger) error { if s.compiledHeaders != nil { return nil } for _, h := range s.Headers { s.compiledHeaders = append(s.compiledHeaders, glob.MustCompile(h.For)) } for _, r := range s.Redirects { s.compiledRedirects = append(s.compiledRedirects, glob.MustCompile(r.From)) } return nil } func (s *Server) MatchHeaders(pattern string) []types.KeyValueStr { if s.compiledHeaders == nil { return nil } var matches []types.KeyValueStr for i, g := range s.compiledHeaders { if g.Match(pattern) { h := s.Headers[i] for k, v := range h.Values { matches = append(matches, types.KeyValueStr{Key: k, Value: cast.ToString(v)}) } } } sort.Slice(matches, func(i, j int) bool { return matches[i].Key < matches[j].Key }) return matches } func (s *Server) MatchRedirect(pattern string) Redirect { if s.compiledRedirects == nil { return Redirect{} } pattern = strings.TrimSuffix(pattern, "index.html") for i, g := range s.compiledRedirects { redir := s.Redirects[i] // No redirect to self. if redir.To == pattern { return Redirect{} } if g.Match(pattern) { return redir } } return Redirect{} } type Headers struct { For string Values map[string]any } type Redirect struct { From string To string // HTTP status code to use for the redirect. // A status code of 200 will trigger a URL rewrite. Status int // Forcode redirect, even if original request path exists. Force bool } // CacheBuster configures cache busting for assets. type CacheBuster struct { // Trigger for files matching this regexp. Source string // Cache bust targets matching this regexp. // This regexp can contain group matches (e.g. $1) from the source regexp. Target string compiledSource func(string) func(string) bool } func (c *CacheBuster) CompileConfig(logger loggers.Logger) error { if c.compiledSource != nil { return nil } source := c.Source sourceRe, err := regexp.Compile(source) if err != nil { return fmt.Errorf("failed to compile cache buster source %q: %w", c.Source, err) } target := c.Target var compileErr error debugl := logger.Logger().WithLevel(logg.LevelDebug).WithField(loggers.FieldNameCmd, "cachebuster") c.compiledSource = func(s string) func(string) bool { m := sourceRe.FindStringSubmatch(s) matchString := "no match" match := m != nil if match { matchString = "match!" } debugl.Logf("Matching %q with source %q: %s", s, source, matchString) if !match { return nil } groups := m[1:] currentTarget := target // Replace $1, $2 etc. in target. for i, g := range groups { currentTarget = strings.ReplaceAll(target, fmt.Sprintf("$%d", i+1), g) } targetRe, err := regexp.Compile(currentTarget) if err != nil { compileErr = fmt.Errorf("failed to compile cache buster target %q: %w", currentTarget, err) return nil } return func(ss string) bool { match = targetRe.MatchString(ss) matchString := "no match" if match { matchString = "match!" } logger.Debugf("Matching %q with target %q: %s", ss, currentTarget, matchString) return match } } return compileErr } func (r Redirect) IsZero() bool { return r.From == "" } const ( // Keep this a little coarse grained, some false positives are OK. cssTargetCachebusterRe = `(css|styles|scss|sass)` ) func DecodeServer(cfg Provider) (Server, error) { s := &Server{} _ = mapstructure.WeakDecode(cfg.GetStringMap("server"), s) for i, redir := range s.Redirects { // Get it in line with the Hugo server for OK responses. // We currently treat the 404 as a special case, they are always "ugly", so keep them as is. if redir.Status != 404 { redir.To = strings.TrimSuffix(redir.To, "index.html") if !strings.HasPrefix(redir.To, "https") && !strings.HasSuffix(redir.To, "/") { // There are some tricky infinite loop situations when dealing // when the target does not have a trailing slash. // This can certainly be handled better, but not time for that now. return Server{}, fmt.Errorf("unsupported redirect to value %q in server config; currently this must be either a remote destination or a local folder, e.g. \"/blog/\" or \"/blog/index.html\"", redir.To) } } s.Redirects[i] = redir } if len(s.Redirects) == 0 { // Set up a default redirect for 404s. s.Redirects = []Redirect{ { From: "**", To: "/404.html", Status: 404, }, } } return *s, nil }