github.com/rabbouni145/gg@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 }