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 }