github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/hugolib/site_render.go (about) 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 "context" 18 "fmt" 19 "path" 20 "strings" 21 "sync" 22 23 "github.com/gohugoio/hugo/config" 24 "github.com/gohugoio/hugo/tpl" 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 if f.IsHTML { 197 // Write alias for page 1 198 d.Addends = fmt.Sprintf("/%s/%d", paginatePath, 1) 199 targetPaths := page.CreateTargetPaths(d) 200 201 if err := s.writeDestAlias(targetPaths.TargetFilename, p.Permalink(), f, p); err != nil { 202 return err 203 } 204 } 205 206 // Render pages for the rest 207 for current := p.paginator.current.Next(); current != nil; current = current.Next() { 208 209 p.paginator.current = current 210 d.Addends = fmt.Sprintf("/%s/%d", paginatePath, current.PageNumber()) 211 targetPaths := page.CreateTargetPaths(d) 212 213 if err := s.renderAndWritePage( 214 &s.PathSpec.ProcessingStats.PaginatorPages, 215 p.Title(), 216 targetPaths.TargetFilename, p, templ); err != nil { 217 return err 218 } 219 220 } 221 222 return nil 223 } 224 225 func (s *Site) render404() error { 226 p, err := newPageStandalone(&pageMeta{ 227 s: s, 228 kind: kind404, 229 urlPaths: pagemeta.URLPath{ 230 URL: "404.html", 231 }, 232 }, 233 output.HTMLFormat, 234 ) 235 if err != nil { 236 return err 237 } 238 239 if !p.render { 240 return nil 241 } 242 243 var d output.LayoutDescriptor 244 d.Kind = kind404 245 246 templ, found, err := s.Tmpl().LookupLayout(d, output.HTMLFormat) 247 if err != nil { 248 return err 249 } 250 if !found { 251 return nil 252 } 253 254 targetPath := p.targetPaths().TargetFilename 255 256 if targetPath == "" { 257 return errors.New("failed to create targetPath for 404 page") 258 } 259 260 return s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "404 page", targetPath, p, templ) 261 } 262 263 func (s *Site) renderSitemap() error { 264 p, err := newPageStandalone(&pageMeta{ 265 s: s, 266 kind: kindSitemap, 267 urlPaths: pagemeta.URLPath{ 268 URL: s.siteCfg.sitemap.Filename, 269 }, 270 }, 271 output.HTMLFormat, 272 ) 273 if err != nil { 274 return err 275 } 276 277 if !p.render { 278 return nil 279 } 280 281 targetPath := p.targetPaths().TargetFilename 282 ctx := tpl.SetPageInContext(context.Background(), p) 283 284 if targetPath == "" { 285 return errors.New("failed to create targetPath for sitemap") 286 } 287 288 templ := s.lookupLayouts("sitemap.xml", "_default/sitemap.xml", "_internal/_default/sitemap.xml") 289 290 return s.renderAndWriteXML(ctx, &s.PathSpec.ProcessingStats.Sitemaps, "sitemap", targetPath, p, templ) 291 } 292 293 func (s *Site) renderRobotsTXT() error { 294 if !s.Cfg.GetBool("enableRobotsTXT") { 295 return nil 296 } 297 298 p, err := newPageStandalone(&pageMeta{ 299 s: s, 300 kind: kindRobotsTXT, 301 urlPaths: pagemeta.URLPath{ 302 URL: "robots.txt", 303 }, 304 }, 305 output.RobotsTxtFormat) 306 if err != nil { 307 return err 308 } 309 310 if !p.render { 311 return nil 312 } 313 314 templ := s.lookupLayouts("robots.txt", "_default/robots.txt", "_internal/_default/robots.txt") 315 316 return s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "Robots Txt", p.targetPaths().TargetFilename, p, templ) 317 } 318 319 // renderAliases renders shell pages that simply have a redirect in the header. 320 func (s *Site) renderAliases() error { 321 var err error 322 s.pageMap.pageTrees.WalkLinkable(func(ss string, n *contentNode) bool { 323 p := n.p 324 if len(p.Aliases()) == 0 { 325 return false 326 } 327 328 pathSeen := make(map[string]bool) 329 330 for _, of := range p.OutputFormats() { 331 if !of.Format.IsHTML { 332 continue 333 } 334 335 f := of.Format 336 337 if pathSeen[f.Path] { 338 continue 339 } 340 pathSeen[f.Path] = true 341 342 plink := of.Permalink() 343 344 for _, a := range p.Aliases() { 345 isRelative := !strings.HasPrefix(a, "/") 346 347 if isRelative { 348 // Make alias relative, where "." will be on the 349 // same directory level as the current page. 350 basePath := path.Join(p.targetPaths().SubResourceBaseLink, "..") 351 a = path.Join(basePath, a) 352 353 } else { 354 // Make sure AMP and similar doesn't clash with regular aliases. 355 a = path.Join(f.Path, a) 356 } 357 358 if s.UglyURLs && !strings.HasSuffix(a, ".html") { 359 a += ".html" 360 } 361 362 lang := p.Language().Lang 363 364 if s.h.multihost && !strings.HasPrefix(a, "/"+lang) { 365 // These need to be in its language root. 366 a = path.Join(lang, a) 367 } 368 369 err = s.writeDestAlias(a, plink, f, p) 370 if err != nil { 371 return true 372 } 373 } 374 } 375 return false 376 }) 377 378 return err 379 } 380 381 // renderMainLanguageRedirect creates a redirect to the main language home, 382 // depending on if it lives in sub folder (e.g. /en) or not. 383 func (s *Site) renderMainLanguageRedirect() error { 384 if !s.h.multilingual.enabled() || s.h.IsMultihost() { 385 // No need for a redirect 386 return nil 387 } 388 389 html, found := s.outputFormatsConfig.GetByName("HTML") 390 if found { 391 mainLang := s.h.multilingual.DefaultLang 392 if s.Info.defaultContentLanguageInSubdir { 393 mainLangURL := s.PathSpec.AbsURL(mainLang.Lang+"/", false) 394 s.Log.Debugf("Write redirect to main language %s: %s", mainLang, mainLangURL) 395 if err := s.publishDestAlias(true, "/", mainLangURL, html, nil); err != nil { 396 return err 397 } 398 } else { 399 mainLangURL := s.PathSpec.AbsURL("", false) 400 s.Log.Debugf("Write redirect to main language %s: %s", mainLang, mainLangURL) 401 if err := s.publishDestAlias(true, mainLang.Lang, mainLangURL, html, nil); err != nil { 402 return err 403 } 404 } 405 } 406 407 return nil 408 }