github.com/olliephillips/hugo@v0.42.2/hugolib/config.go (about) 1 // Copyright 2016-present The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package hugolib 15 16 import ( 17 "errors" 18 "fmt" 19 20 "github.com/gohugoio/hugo/hugolib/paths" 21 22 "io" 23 "strings" 24 25 "github.com/gohugoio/hugo/langs" 26 27 "github.com/gohugoio/hugo/config" 28 "github.com/gohugoio/hugo/config/privacy" 29 "github.com/gohugoio/hugo/config/services" 30 "github.com/gohugoio/hugo/helpers" 31 "github.com/spf13/afero" 32 "github.com/spf13/viper" 33 ) 34 35 // SiteConfig represents the config in .Site.Config. 36 type SiteConfig struct { 37 // This contains all privacy related settings that can be used to 38 // make the YouTube template etc. GDPR compliant. 39 Privacy privacy.Config 40 41 // Services contains config for services such as Google Analytics etc. 42 Services services.Config 43 } 44 45 func loadSiteConfig(cfg config.Provider) (scfg SiteConfig, err error) { 46 privacyConfig, err := privacy.DecodeConfig(cfg) 47 if err != nil { 48 return 49 } 50 51 servicesConfig, err := services.DecodeConfig(cfg) 52 if err != nil { 53 return 54 } 55 56 scfg.Privacy = privacyConfig 57 scfg.Services = servicesConfig 58 59 return 60 } 61 62 // ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.). 63 type ConfigSourceDescriptor struct { 64 Fs afero.Fs 65 66 // Full path to the config file to use, i.e. /my/project/config.toml 67 Filename string 68 69 // The path to the directory to look for configuration. Is used if Filename is not 70 // set. 71 Path string 72 73 // The project's working dir. Is used to look for additional theme config. 74 WorkingDir string 75 } 76 77 func (d ConfigSourceDescriptor) configFilenames() []string { 78 return strings.Split(d.Filename, ",") 79 } 80 81 // LoadConfigDefault is a convenience method to load the default "config.toml" config. 82 func LoadConfigDefault(fs afero.Fs) (*viper.Viper, error) { 83 v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs, Filename: "config.toml"}) 84 return v, err 85 } 86 87 var ErrNoConfigFile = errors.New("Unable to locate Config file. Perhaps you need to create a new site.\n Run `hugo help new` for details.\n") 88 89 // LoadConfig loads Hugo configuration into a new Viper and then adds 90 // a set of defaults. 91 func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provider) error) (*viper.Viper, []string, error) { 92 var configFiles []string 93 94 fs := d.Fs 95 v := viper.New() 96 v.SetFs(fs) 97 98 if d.Path == "" { 99 d.Path = "." 100 } 101 102 configFilenames := d.configFilenames() 103 v.AutomaticEnv() 104 v.SetEnvPrefix("hugo") 105 v.SetConfigFile(configFilenames[0]) 106 v.AddConfigPath(d.Path) 107 108 var configFileErr error 109 110 err := v.ReadInConfig() 111 if err != nil { 112 if _, ok := err.(viper.ConfigParseError); ok { 113 return nil, configFiles, err 114 } 115 configFileErr = ErrNoConfigFile 116 } 117 118 if configFileErr == nil { 119 120 if cf := v.ConfigFileUsed(); cf != "" { 121 configFiles = append(configFiles, cf) 122 } 123 124 for _, configFile := range configFilenames[1:] { 125 var r io.Reader 126 var err error 127 if r, err = fs.Open(configFile); err != nil { 128 return nil, configFiles, fmt.Errorf("Unable to open Config file.\n (%s)\n", err) 129 } 130 if err = v.MergeConfig(r); err != nil { 131 return nil, configFiles, fmt.Errorf("Unable to parse/merge Config file (%s).\n (%s)\n", configFile, err) 132 } 133 configFiles = append(configFiles, configFile) 134 } 135 136 } 137 138 if err := loadDefaultSettingsFor(v); err != nil { 139 return v, configFiles, err 140 } 141 142 if configFileErr == nil { 143 144 themeConfigFiles, err := loadThemeConfig(d, v) 145 if err != nil { 146 return v, configFiles, err 147 } 148 149 if len(themeConfigFiles) > 0 { 150 configFiles = append(configFiles, themeConfigFiles...) 151 } 152 } 153 154 // We create languages based on the settings, so we need to make sure that 155 // all configuration is loaded/set before doing that. 156 for _, d := range doWithConfig { 157 if err := d(v); err != nil { 158 return v, configFiles, err 159 } 160 } 161 162 if err := loadLanguageSettings(v, nil); err != nil { 163 return v, configFiles, err 164 } 165 166 return v, configFiles, configFileErr 167 168 } 169 170 func loadLanguageSettings(cfg config.Provider, oldLangs langs.Languages) error { 171 172 defaultLang := cfg.GetString("defaultContentLanguage") 173 174 var languages map[string]interface{} 175 176 languagesFromConfig := cfg.GetStringMap("languages") 177 disableLanguages := cfg.GetStringSlice("disableLanguages") 178 179 if len(disableLanguages) == 0 { 180 languages = languagesFromConfig 181 } else { 182 languages = make(map[string]interface{}) 183 for k, v := range languagesFromConfig { 184 for _, disabled := range disableLanguages { 185 if disabled == defaultLang { 186 return fmt.Errorf("cannot disable default language %q", defaultLang) 187 } 188 189 if strings.EqualFold(k, disabled) { 190 v.(map[string]interface{})["disabled"] = true 191 break 192 } 193 } 194 languages[k] = v 195 } 196 } 197 198 var ( 199 languages2 langs.Languages 200 err error 201 ) 202 203 if len(languages) == 0 { 204 languages2 = append(languages2, langs.NewDefaultLanguage(cfg)) 205 } else { 206 languages2, err = toSortedLanguages(cfg, languages) 207 if err != nil { 208 return fmt.Errorf("Failed to parse multilingual config: %s", err) 209 } 210 } 211 212 if oldLangs != nil { 213 // When in multihost mode, the languages are mapped to a server, so 214 // some structural language changes will need a restart of the dev server. 215 // The validation below isn't complete, but should cover the most 216 // important cases. 217 var invalid bool 218 if languages2.IsMultihost() != oldLangs.IsMultihost() { 219 invalid = true 220 } else { 221 if languages2.IsMultihost() && len(languages2) != len(oldLangs) { 222 invalid = true 223 } 224 } 225 226 if invalid { 227 return errors.New("language change needing a server restart detected") 228 } 229 230 if languages2.IsMultihost() { 231 // We need to transfer any server baseURL to the new language 232 for i, ol := range oldLangs { 233 nl := languages2[i] 234 nl.Set("baseURL", ol.GetString("baseURL")) 235 } 236 } 237 } 238 239 // The defaultContentLanguage is something the user has to decide, but it needs 240 // to match a language in the language definition list. 241 langExists := false 242 for _, lang := range languages2 { 243 if lang.Lang == defaultLang { 244 langExists = true 245 break 246 } 247 } 248 249 if !langExists { 250 return fmt.Errorf("site config value %q for defaultContentLanguage does not match any language definition", defaultLang) 251 } 252 253 cfg.Set("languagesSorted", languages2) 254 cfg.Set("multilingual", len(languages2) > 1) 255 256 multihost := languages2.IsMultihost() 257 258 if multihost { 259 cfg.Set("defaultContentLanguageInSubdir", true) 260 cfg.Set("multihost", true) 261 } 262 263 if multihost { 264 // The baseURL may be provided at the language level. If that is true, 265 // then every language must have a baseURL. In this case we always render 266 // to a language sub folder, which is then stripped from all the Permalink URLs etc. 267 for _, l := range languages2 { 268 burl := l.GetLocal("baseURL") 269 if burl == nil { 270 return errors.New("baseURL must be set on all or none of the languages") 271 } 272 } 273 274 } 275 276 return nil 277 } 278 279 func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) ([]string, error) { 280 themesDir := paths.AbsPathify(d.WorkingDir, v1.GetString("themesDir")) 281 themes := config.GetStringSlicePreserveString(v1, "theme") 282 283 // CollectThemes(fs afero.Fs, themesDir string, themes []strin 284 themeConfigs, err := paths.CollectThemes(d.Fs, themesDir, themes) 285 if err != nil { 286 return nil, err 287 } 288 v1.Set("allThemes", themeConfigs) 289 290 var configFilenames []string 291 for _, tc := range themeConfigs { 292 if tc.ConfigFilename != "" { 293 configFilenames = append(configFilenames, tc.ConfigFilename) 294 if err := applyThemeConfig(v1, tc); err != nil { 295 return nil, err 296 } 297 } 298 } 299 300 return configFilenames, nil 301 302 } 303 304 func applyThemeConfig(v1 *viper.Viper, theme paths.ThemeConfig) error { 305 306 const ( 307 paramsKey = "params" 308 languagesKey = "languages" 309 menuKey = "menu" 310 ) 311 312 v2 := theme.Cfg 313 314 for _, key := range []string{paramsKey, "outputformats", "mediatypes"} { 315 mergeStringMapKeepLeft("", key, v1, v2) 316 } 317 318 themeLower := strings.ToLower(theme.Name) 319 themeParamsNamespace := paramsKey + "." + themeLower 320 321 // Set namespaced params 322 if v2.IsSet(paramsKey) && !v1.IsSet(themeParamsNamespace) { 323 // Set it in the default store to make sure it gets in the same or 324 // behind the others. 325 v1.SetDefault(themeParamsNamespace, v2.Get(paramsKey)) 326 } 327 328 // Only add params and new menu entries, we do not add language definitions. 329 if v1.IsSet(languagesKey) && v2.IsSet(languagesKey) { 330 v1Langs := v1.GetStringMap(languagesKey) 331 for k, _ := range v1Langs { 332 langParamsKey := languagesKey + "." + k + "." + paramsKey 333 mergeStringMapKeepLeft(paramsKey, langParamsKey, v1, v2) 334 } 335 v2Langs := v2.GetStringMap(languagesKey) 336 for k, _ := range v2Langs { 337 if k == "" { 338 continue 339 } 340 langParamsKey := languagesKey + "." + k + "." + paramsKey 341 langParamsThemeNamespace := langParamsKey + "." + themeLower 342 // Set namespaced params 343 if v2.IsSet(langParamsKey) && !v1.IsSet(langParamsThemeNamespace) { 344 v1.SetDefault(langParamsThemeNamespace, v2.Get(langParamsKey)) 345 } 346 347 langMenuKey := languagesKey + "." + k + "." + menuKey 348 if v2.IsSet(langMenuKey) { 349 // Only add if not in the main config. 350 v2menus := v2.GetStringMap(langMenuKey) 351 for k, v := range v2menus { 352 menuEntry := menuKey + "." + k 353 menuLangEntry := langMenuKey + "." + k 354 if !v1.IsSet(menuEntry) && !v1.IsSet(menuLangEntry) { 355 v1.Set(menuLangEntry, v) 356 } 357 } 358 } 359 } 360 } 361 362 // Add menu definitions from theme not found in project 363 if v2.IsSet("menu") { 364 v2menus := v2.GetStringMap(menuKey) 365 for k, v := range v2menus { 366 menuEntry := menuKey + "." + k 367 if !v1.IsSet(menuEntry) { 368 v1.SetDefault(menuEntry, v) 369 } 370 } 371 } 372 373 return nil 374 375 } 376 377 func mergeStringMapKeepLeft(rootKey, key string, v1, v2 config.Provider) { 378 if !v2.IsSet(key) { 379 return 380 } 381 382 if !v1.IsSet(key) && !(rootKey != "" && rootKey != key && v1.IsSet(rootKey)) { 383 v1.Set(key, v2.Get(key)) 384 return 385 } 386 387 m1 := v1.GetStringMap(key) 388 m2 := v2.GetStringMap(key) 389 390 for k, v := range m2 { 391 if _, found := m1[k]; !found { 392 if rootKey != "" && v1.IsSet(rootKey+"."+k) { 393 continue 394 } 395 m1[k] = v 396 } 397 } 398 } 399 400 func loadDefaultSettingsFor(v *viper.Viper) error { 401 402 c, err := helpers.NewContentSpec(v) 403 if err != nil { 404 return err 405 } 406 407 v.RegisterAlias("indexes", "taxonomies") 408 409 v.SetDefault("cleanDestinationDir", false) 410 v.SetDefault("watch", false) 411 v.SetDefault("metaDataFormat", "toml") 412 v.SetDefault("contentDir", "content") 413 v.SetDefault("layoutDir", "layouts") 414 v.SetDefault("staticDir", "static") 415 v.SetDefault("resourceDir", "resources") 416 v.SetDefault("archetypeDir", "archetypes") 417 v.SetDefault("publishDir", "public") 418 v.SetDefault("dataDir", "data") 419 v.SetDefault("i18nDir", "i18n") 420 v.SetDefault("themesDir", "themes") 421 v.SetDefault("buildDrafts", false) 422 v.SetDefault("buildFuture", false) 423 v.SetDefault("buildExpired", false) 424 v.SetDefault("uglyURLs", false) 425 v.SetDefault("verbose", false) 426 v.SetDefault("ignoreCache", false) 427 v.SetDefault("canonifyURLs", false) 428 v.SetDefault("relativeURLs", false) 429 v.SetDefault("removePathAccents", false) 430 v.SetDefault("titleCaseStyle", "AP") 431 v.SetDefault("taxonomies", map[string]string{"tag": "tags", "category": "categories"}) 432 v.SetDefault("permalinks", make(PermalinkOverrides, 0)) 433 v.SetDefault("sitemap", Sitemap{Priority: -1, Filename: "sitemap.xml"}) 434 v.SetDefault("pygmentsStyle", "monokai") 435 v.SetDefault("pygmentsUseClasses", false) 436 v.SetDefault("pygmentsCodeFences", false) 437 v.SetDefault("pygmentsUseClassic", false) 438 v.SetDefault("pygmentsOptions", "") 439 v.SetDefault("disableLiveReload", false) 440 v.SetDefault("pluralizeListTitles", true) 441 v.SetDefault("preserveTaxonomyNames", false) 442 v.SetDefault("forceSyncStatic", false) 443 v.SetDefault("footnoteAnchorPrefix", "") 444 v.SetDefault("footnoteReturnLinkContents", "") 445 v.SetDefault("newContentEditor", "") 446 v.SetDefault("paginate", 10) 447 v.SetDefault("paginatePath", "page") 448 v.SetDefault("summaryLength", 70) 449 v.SetDefault("blackfriday", c.BlackFriday) 450 v.SetDefault("rSSUri", "index.xml") 451 v.SetDefault("rssLimit", -1) 452 v.SetDefault("sectionPagesMenu", "") 453 v.SetDefault("disablePathToLower", false) 454 v.SetDefault("hasCJKLanguage", false) 455 v.SetDefault("enableEmoji", false) 456 v.SetDefault("pygmentsCodeFencesGuessSyntax", false) 457 v.SetDefault("useModTimeAsFallback", false) 458 v.SetDefault("defaultContentLanguage", "en") 459 v.SetDefault("defaultContentLanguageInSubdir", false) 460 v.SetDefault("enableMissingTranslationPlaceholders", false) 461 v.SetDefault("enableGitInfo", false) 462 v.SetDefault("ignoreFiles", make([]string, 0)) 463 v.SetDefault("disableAliases", false) 464 v.SetDefault("debug", false) 465 v.SetDefault("disableFastRender", false) 466 v.SetDefault("timeout", 10000) // 10 seconds 467 468 // Remove in Hugo 0.39 469 470 if v.GetBool("useModTimeAsFallback") { 471 472 helpers.Deprecated("Site config", "useModTimeAsFallback", `Replace with this in your config.toml: 473 474 [frontmatter] 475 date = [ "date",":fileModTime", ":default"] 476 lastmod = ["lastmod" ,":fileModTime", ":default"] 477 `, false) 478 479 } 480 481 return nil 482 }