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  }