site_render.go (9082B)
1 // Copyright 2019 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 "fmt" 18 "path" 19 "strings" 20 "sync" 21 22 "github.com/gohugoio/hugo/tpl" 23 24 "github.com/gohugoio/hugo/config" 25 26 "errors" 27 28 "github.com/gohugoio/hugo/output" 29 30 "github.com/gohugoio/hugo/resources/page" 31 "github.com/gohugoio/hugo/resources/page/pagemeta" 32 ) 33 34 type siteRenderContext struct { 35 cfg *BuildCfg 36 37 // Zero based index for all output formats combined. 38 sitesOutIdx int 39 40 // Zero based index of the output formats configured within a Site. 41 // Note that these outputs are sorted. 42 outIdx int 43 44 multihost bool 45 } 46 47 // Whether to render 404.html, robotsTXT.txt which usually is rendered 48 // once only in the site root. 49 func (s siteRenderContext) renderSingletonPages() bool { 50 if s.multihost { 51 // 1 per site 52 return s.outIdx == 0 53 } 54 55 // 1 for all sites 56 return s.sitesOutIdx == 0 57 } 58 59 // renderPages renders pages each corresponding to a markdown file. 60 // TODO(bep np doc 61 func (s *Site) renderPages(ctx *siteRenderContext) error { 62 numWorkers := config.GetNumWorkerMultiplier() 63 64 results := make(chan error) 65 pages := make(chan *pageState, numWorkers) // buffered for performance 66 errs := make(chan error) 67 68 go s.errorCollator(results, errs) 69 70 wg := &sync.WaitGroup{} 71 72 for i := 0; i < numWorkers; i++ { 73 wg.Add(1) 74 go pageRenderer(ctx, s, pages, results, wg) 75 } 76 77 cfg := ctx.cfg 78 79 s.pageMap.pageTrees.Walk(func(ss string, n *contentNode) bool { 80 if cfg.shouldRender(n.p) { 81 select { 82 case <-s.h.Done(): 83 return true 84 default: 85 pages <- n.p 86 } 87 } 88 return false 89 }) 90 91 close(pages) 92 93 wg.Wait() 94 95 close(results) 96 97 err := <-errs 98 if err != nil { 99 return fmt.Errorf("failed to render pages: %w", err) 100 } 101 return nil 102 } 103 104 func pageRenderer( 105 ctx *siteRenderContext, 106 s *Site, 107 pages <-chan *pageState, 108 results chan<- error, 109 wg *sync.WaitGroup) { 110 defer wg.Done() 111 112 for p := range pages { 113 if p.m.buildConfig.PublishResources { 114 if err := p.renderResources(); err != nil { 115 s.SendError(p.errorf(err, "failed to render page resources")) 116 continue 117 } 118 } 119 120 if !p.render { 121 // Nothing more to do for this page. 122 continue 123 } 124 125 templ, found, err := p.resolveTemplate() 126 if err != nil { 127 s.SendError(p.errorf(err, "failed to resolve template")) 128 continue 129 } 130 131 if !found { 132 s.logMissingLayout("", p.Layout(), p.Kind(), p.f.Name) 133 continue 134 } 135 136 targetPath := p.targetPaths().TargetFilename 137 138 if err := s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "page "+p.Title(), targetPath, p, templ); err != nil { 139 results <- err 140 } 141 142 if p.paginator != nil && p.paginator.current != nil { 143 if err := s.renderPaginator(p, templ); err != nil { 144 results <- err 145 } 146 } 147 } 148 } 149 150 func (s *Site) logMissingLayout(name, layout, kind, outputFormat string) { 151 log := s.Log.Warn() 152 if name != "" && infoOnMissingLayout[name] { 153 log = s.Log.Info() 154 } 155 156 errMsg := "You should create a template file which matches Hugo Layouts Lookup Rules for this combination." 157 var args []any 158 msg := "found no layout file for" 159 if outputFormat != "" { 160 msg += " %q" 161 args = append(args, outputFormat) 162 } 163 164 if layout != "" { 165 msg += " for layout %q" 166 args = append(args, layout) 167 } 168 169 if kind != "" { 170 msg += " for kind %q" 171 args = append(args, kind) 172 } 173 174 if name != "" { 175 msg += " for %q" 176 args = append(args, name) 177 } 178 179 msg += ": " + errMsg 180 181 log.Printf(msg, args...) 182 } 183 184 // renderPaginator must be run after the owning Page has been rendered. 185 func (s *Site) renderPaginator(p *pageState, templ tpl.Template) error { 186 paginatePath := s.Cfg.GetString("paginatePath") 187 188 d := p.targetPathDescriptor 189 f := p.s.rc.Format 190 d.Type = f 191 192 if p.paginator.current == nil || p.paginator.current != p.paginator.current.First() { 193 panic(fmt.Sprintf("invalid paginator state for %q", p.pathOrTitle())) 194 } 195 196 // Render pages for the rest 197 for current := p.paginator.current; current != nil; current = current.Next() { 198 199 p.paginator.current = current 200 d.Addends = fmt.Sprintf("/%s/%d", paginatePath, current.PageNumber()) 201 targetPaths := page.CreateTargetPaths(d) 202 203 if f.IsHTML && current.Prev() == nil { 204 if err := s.writeDestAlias(targetPaths.TargetFilename, p.Permalink(), f, nil); err != nil { 205 return err 206 } 207 } else { 208 if err := s.renderAndWritePage( 209 &s.PathSpec.ProcessingStats.PaginatorPages, 210 p.Title(), 211 targetPaths.TargetFilename, p, templ); err != nil { 212 return err 213 } 214 } 215 216 } 217 218 return nil 219 } 220 221 func (s *Site) render404() error { 222 p, err := newPageStandalone(&pageMeta{ 223 s: s, 224 kind: kind404, 225 urlPaths: pagemeta.URLPath{ 226 URL: "404.html", 227 }, 228 }, 229 output.HTMLFormat, 230 ) 231 if err != nil { 232 return err 233 } 234 235 if !p.render { 236 return nil 237 } 238 239 var d output.LayoutDescriptor 240 d.Kind = kind404 241 242 templ, found, err := s.Tmpl().LookupLayout(d, output.HTMLFormat) 243 if err != nil { 244 return err 245 } 246 if !found { 247 return nil 248 } 249 250 targetPath := p.targetPaths().TargetFilename 251 252 if targetPath == "" { 253 return errors.New("failed to create targetPath for 404 page") 254 } 255 256 return s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "404 page", targetPath, p, templ) 257 } 258 259 func (s *Site) renderSitemap() error { 260 p, err := newPageStandalone(&pageMeta{ 261 s: s, 262 kind: kindSitemap, 263 urlPaths: pagemeta.URLPath{ 264 URL: s.siteCfg.sitemap.Filename, 265 }, 266 }, 267 output.HTMLFormat, 268 ) 269 if err != nil { 270 return err 271 } 272 273 if !p.render { 274 return nil 275 } 276 277 targetPath := p.targetPaths().TargetFilename 278 279 if targetPath == "" { 280 return errors.New("failed to create targetPath for sitemap") 281 } 282 283 templ := s.lookupLayouts("sitemap.xml", "_default/sitemap.xml", "_internal/_default/sitemap.xml") 284 285 return s.renderAndWriteXML(&s.PathSpec.ProcessingStats.Sitemaps, "sitemap", targetPath, p, templ) 286 } 287 288 func (s *Site) renderRobotsTXT() error { 289 if !s.Cfg.GetBool("enableRobotsTXT") { 290 return nil 291 } 292 293 p, err := newPageStandalone(&pageMeta{ 294 s: s, 295 kind: kindRobotsTXT, 296 urlPaths: pagemeta.URLPath{ 297 URL: "robots.txt", 298 }, 299 }, 300 output.RobotsTxtFormat) 301 if err != nil { 302 return err 303 } 304 305 if !p.render { 306 return nil 307 } 308 309 templ := s.lookupLayouts("robots.txt", "_default/robots.txt", "_internal/_default/robots.txt") 310 311 return s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "Robots Txt", p.targetPaths().TargetFilename, p, templ) 312 } 313 314 // renderAliases renders shell pages that simply have a redirect in the header. 315 func (s *Site) renderAliases() error { 316 var err error 317 s.pageMap.pageTrees.WalkLinkable(func(ss string, n *contentNode) bool { 318 p := n.p 319 if len(p.Aliases()) == 0 { 320 return false 321 } 322 323 pathSeen := make(map[string]bool) 324 325 for _, of := range p.OutputFormats() { 326 if !of.Format.IsHTML { 327 continue 328 } 329 330 f := of.Format 331 332 if pathSeen[f.Path] { 333 continue 334 } 335 pathSeen[f.Path] = true 336 337 plink := of.Permalink() 338 339 for _, a := range p.Aliases() { 340 isRelative := !strings.HasPrefix(a, "/") 341 342 if isRelative { 343 // Make alias relative, where "." will be on the 344 // same directory level as the current page. 345 basePath := path.Join(p.targetPaths().SubResourceBaseLink, "..") 346 a = path.Join(basePath, a) 347 348 } else { 349 // Make sure AMP and similar doesn't clash with regular aliases. 350 a = path.Join(f.Path, a) 351 } 352 353 if s.UglyURLs && !strings.HasSuffix(a, ".html") { 354 a += ".html" 355 } 356 357 lang := p.Language().Lang 358 359 if s.h.multihost && !strings.HasPrefix(a, "/"+lang) { 360 // These need to be in its language root. 361 a = path.Join(lang, a) 362 } 363 364 err = s.writeDestAlias(a, plink, f, p) 365 if err != nil { 366 return true 367 } 368 } 369 } 370 return false 371 }) 372 373 return err 374 } 375 376 // renderMainLanguageRedirect creates a redirect to the main language home, 377 // depending on if it lives in sub folder (e.g. /en) or not. 378 func (s *Site) renderMainLanguageRedirect() error { 379 if !s.h.multilingual.enabled() || s.h.IsMultihost() { 380 // No need for a redirect 381 return nil 382 } 383 384 html, found := s.outputFormatsConfig.GetByName("HTML") 385 if found { 386 mainLang := s.h.multilingual.DefaultLang 387 if s.Info.defaultContentLanguageInSubdir { 388 mainLangURL := s.PathSpec.AbsURL(mainLang.Lang+"/", false) 389 s.Log.Debugf("Write redirect to main language %s: %s", mainLang, mainLangURL) 390 if err := s.publishDestAlias(true, "/", mainLangURL, html, nil); err != nil { 391 return err 392 } 393 } else { 394 mainLangURL := s.PathSpec.AbsURL("", false) 395 s.Log.Debugf("Write redirect to main language %s: %s", mainLang, mainLangURL) 396 if err := s.publishDestAlias(true, mainLang.Lang, mainLangURL, html, nil); err != nil { 397 return err 398 } 399 } 400 } 401 402 return nil 403 }