github.com/schumacherfm/hugo@v0.47.1/hugolib/page_paths.go (about)

     1  // Copyright 2017 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/filepath"
    19  
    20  	"net/url"
    21  	"strings"
    22  
    23  	"github.com/gohugoio/hugo/helpers"
    24  	"github.com/gohugoio/hugo/output"
    25  )
    26  
    27  // targetPathDescriptor describes how a file path for a given resource
    28  // should look like on the file system. The same descriptor is then later used to
    29  // create both the permalinks and the relative links, paginator URLs etc.
    30  //
    31  // The big motivating behind this is to have only one source of truth for URLs,
    32  // and by that also get rid of most of the fragile string parsing/encoding etc.
    33  //
    34  // Page.createTargetPathDescriptor is the Page adapter.
    35  //
    36  type targetPathDescriptor struct {
    37  	PathSpec *helpers.PathSpec
    38  
    39  	Type output.Format
    40  	Kind string
    41  
    42  	Sections []string
    43  
    44  	// For regular content pages this is either
    45  	// 1) the Slug, if set,
    46  	// 2) the file base name (TranslationBaseName).
    47  	BaseName string
    48  
    49  	// Source directory.
    50  	Dir string
    51  
    52  	// Language prefix, set if multilingual and if page should be placed in its
    53  	// language subdir.
    54  	LangPrefix string
    55  
    56  	// Whether this is a multihost multilingual setup.
    57  	IsMultihost bool
    58  
    59  	// URL from front matter if set. Will override any Slug etc.
    60  	URL string
    61  
    62  	// Used to create paginator links.
    63  	Addends string
    64  
    65  	// The expanded permalink if defined for the section, ready to use.
    66  	ExpandedPermalink string
    67  
    68  	// Some types cannot have uglyURLs, even if globally enabled, RSS being one example.
    69  	UglyURLs bool
    70  }
    71  
    72  // createTargetPathDescriptor adapts a Page and the given output.Format into
    73  // a targetPathDescriptor. This descriptor can then be used to create paths
    74  // and URLs for this Page.
    75  func (p *Page) createTargetPathDescriptor(t output.Format) (targetPathDescriptor, error) {
    76  	if p.targetPathDescriptorPrototype == nil {
    77  		panic(fmt.Sprintf("Must run initTargetPathDescriptor() for page %q, kind %q", p.title, p.Kind))
    78  	}
    79  	d := *p.targetPathDescriptorPrototype
    80  	d.Type = t
    81  	return d, nil
    82  }
    83  
    84  func (p *Page) initTargetPathDescriptor() error {
    85  	d := &targetPathDescriptor{
    86  		PathSpec:    p.s.PathSpec,
    87  		Kind:        p.Kind,
    88  		Sections:    p.sections,
    89  		UglyURLs:    p.s.Info.uglyURLs(p),
    90  		Dir:         filepath.ToSlash(p.Source.Dir()),
    91  		URL:         p.frontMatterURL,
    92  		IsMultihost: p.s.owner.IsMultihost(),
    93  	}
    94  
    95  	if p.Slug != "" {
    96  		d.BaseName = p.Slug
    97  	} else {
    98  		d.BaseName = p.TranslationBaseName()
    99  	}
   100  
   101  	if p.shouldAddLanguagePrefix() {
   102  		d.LangPrefix = p.Lang()
   103  	}
   104  
   105  	// Expand only KindPage and KindTaxonomy; don't expand other Kinds of Pages
   106  	// like KindSection or KindTaxonomyTerm because they are "shallower" and
   107  	// the permalink configuration values are likely to be redundant, e.g.
   108  	// naively expanding /category/:slug/ would give /category/categories/ for
   109  	// the "categories" KindTaxonomyTerm.
   110  	if p.Kind == KindPage || p.Kind == KindTaxonomy {
   111  		if override, ok := p.Site.Permalinks[p.Section()]; ok {
   112  			opath, err := override.Expand(p)
   113  			if err != nil {
   114  				return err
   115  			}
   116  
   117  			opath, _ = url.QueryUnescape(opath)
   118  			opath = filepath.FromSlash(opath)
   119  			d.ExpandedPermalink = opath
   120  		}
   121  	}
   122  
   123  	p.targetPathDescriptorPrototype = d
   124  	return nil
   125  
   126  }
   127  
   128  func (p *Page) initURLs() error {
   129  	if len(p.outputFormats) == 0 {
   130  		p.outputFormats = p.s.outputFormats[p.Kind]
   131  	}
   132  	target := filepath.ToSlash(p.createRelativeTargetPath())
   133  	rel := p.s.PathSpec.URLizeFilename(target)
   134  
   135  	var err error
   136  	f := p.outputFormats[0]
   137  	p.permalink, err = p.s.permalinkForOutputFormat(rel, f)
   138  	if err != nil {
   139  		return err
   140  	}
   141  
   142  	p.relTargetPathBase = strings.TrimPrefix(strings.TrimSuffix(target, f.MediaType.FullSuffix()), "/")
   143  	if prefix := p.s.GetLanguagePrefix(); prefix != "" {
   144  		// Any language code in the path will be added later.
   145  		p.relTargetPathBase = strings.TrimPrefix(p.relTargetPathBase, prefix+"/")
   146  	}
   147  	p.relPermalink = p.s.PathSpec.PrependBasePath(rel)
   148  	p.layoutDescriptor = p.createLayoutDescriptor()
   149  	return nil
   150  }
   151  
   152  func (p *Page) initPaths() error {
   153  	if err := p.initTargetPathDescriptor(); err != nil {
   154  		return err
   155  	}
   156  	if err := p.initURLs(); err != nil {
   157  		return err
   158  	}
   159  	return nil
   160  }
   161  
   162  // createTargetPath creates the target filename for this Page for the given
   163  // output.Format. Some additional URL parts can also be provided, the typical
   164  // use case being pagination.
   165  func (p *Page) createTargetPath(t output.Format, noLangPrefix bool, addends ...string) (string, error) {
   166  	d, err := p.createTargetPathDescriptor(t)
   167  	if err != nil {
   168  		return "", nil
   169  	}
   170  
   171  	if noLangPrefix {
   172  		d.LangPrefix = ""
   173  	}
   174  
   175  	if len(addends) > 0 {
   176  		d.Addends = filepath.Join(addends...)
   177  	}
   178  
   179  	return createTargetPath(d), nil
   180  }
   181  
   182  func createTargetPath(d targetPathDescriptor) string {
   183  
   184  	pagePath := helpers.FilePathSeparator
   185  
   186  	// The top level index files, i.e. the home page etc., needs
   187  	// the index base even when uglyURLs is enabled.
   188  	needsBase := true
   189  
   190  	isUgly := d.UglyURLs && !d.Type.NoUgly
   191  
   192  	if d.ExpandedPermalink == "" && d.BaseName != "" && d.BaseName == d.Type.BaseName {
   193  		isUgly = true
   194  	}
   195  
   196  	if d.Kind != KindPage && d.URL == "" && len(d.Sections) > 0 {
   197  		if d.ExpandedPermalink != "" {
   198  			pagePath = filepath.Join(pagePath, d.ExpandedPermalink)
   199  		} else {
   200  			pagePath = filepath.Join(d.Sections...)
   201  		}
   202  		needsBase = false
   203  	}
   204  
   205  	if d.Type.Path != "" {
   206  		pagePath = filepath.Join(pagePath, d.Type.Path)
   207  	}
   208  
   209  	if d.Kind != KindHome && d.URL != "" {
   210  		if d.IsMultihost && d.LangPrefix != "" && !strings.HasPrefix(d.URL, "/"+d.LangPrefix) {
   211  			pagePath = filepath.Join(d.LangPrefix, pagePath, d.URL)
   212  		} else {
   213  			pagePath = filepath.Join(pagePath, d.URL)
   214  		}
   215  
   216  		if d.Addends != "" {
   217  			pagePath = filepath.Join(pagePath, d.Addends)
   218  		}
   219  
   220  		if strings.HasSuffix(d.URL, "/") || !strings.Contains(d.URL, ".") {
   221  			pagePath = filepath.Join(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix())
   222  		}
   223  
   224  	} else if d.Kind == KindPage {
   225  		if d.ExpandedPermalink != "" {
   226  			pagePath = filepath.Join(pagePath, d.ExpandedPermalink)
   227  
   228  		} else {
   229  			if d.Dir != "" {
   230  				pagePath = filepath.Join(pagePath, d.Dir)
   231  			}
   232  			if d.BaseName != "" {
   233  				pagePath = filepath.Join(pagePath, d.BaseName)
   234  			}
   235  		}
   236  
   237  		if d.Addends != "" {
   238  			pagePath = filepath.Join(pagePath, d.Addends)
   239  		}
   240  
   241  		if isUgly {
   242  			pagePath += d.Type.MediaType.FullSuffix()
   243  		} else {
   244  			pagePath = filepath.Join(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix())
   245  		}
   246  
   247  		if d.LangPrefix != "" {
   248  			pagePath = filepath.Join(d.LangPrefix, pagePath)
   249  		}
   250  	} else {
   251  		if d.Addends != "" {
   252  			pagePath = filepath.Join(pagePath, d.Addends)
   253  		}
   254  
   255  		needsBase = needsBase && d.Addends == ""
   256  
   257  		// No permalink expansion etc. for node type pages (for now)
   258  		base := ""
   259  
   260  		if needsBase || !isUgly {
   261  			base = helpers.FilePathSeparator + d.Type.BaseName
   262  		}
   263  
   264  		pagePath += base + d.Type.MediaType.FullSuffix()
   265  
   266  		if d.LangPrefix != "" {
   267  			pagePath = filepath.Join(d.LangPrefix, pagePath)
   268  		}
   269  	}
   270  
   271  	pagePath = filepath.Join(helpers.FilePathSeparator, pagePath)
   272  
   273  	// Note: MakePathSanitized will lower case the path if
   274  	// disablePathToLower isn't set.
   275  	return d.PathSpec.MakePathSanitized(pagePath)
   276  }
   277  
   278  func (p *Page) createRelativeTargetPath() string {
   279  
   280  	if len(p.outputFormats) == 0 {
   281  		if p.Kind == kindUnknown {
   282  			panic(fmt.Sprintf("Page %q has unknown kind", p.title))
   283  		}
   284  		panic(fmt.Sprintf("Page %q missing output format(s)", p.title))
   285  	}
   286  
   287  	// Choose the main output format. In most cases, this will be HTML.
   288  	f := p.outputFormats[0]
   289  
   290  	return p.createRelativeTargetPathForOutputFormat(f)
   291  
   292  }
   293  
   294  func (p *Page) createRelativePermalinkForOutputFormat(f output.Format) string {
   295  	return p.s.PathSpec.URLizeFilename(p.createRelativeTargetPathForOutputFormat(f))
   296  }
   297  
   298  func (p *Page) createRelativeTargetPathForOutputFormat(f output.Format) string {
   299  	tp, err := p.createTargetPath(f, p.s.owner.IsMultihost())
   300  
   301  	if err != nil {
   302  		p.s.Log.ERROR.Printf("Failed to create permalink for page %q: %s", p.FullFilePath(), err)
   303  		return ""
   304  	}
   305  
   306  	// For /index.json etc. we must  use the full path.
   307  	if strings.HasSuffix(f.BaseFilename(), "html") {
   308  		tp = strings.TrimSuffix(tp, f.BaseFilename())
   309  	}
   310  
   311  	return tp
   312  }