summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPhil Pennock <pdp@spodhuis.org>2013-11-18 04:35:56 -0500
committerNoah Campbell <noahcampbell@gmail.com>2013-11-18 13:32:56 -0800
commit07978e4a4922bc21c230fee65052232b829bd1ab (patch)
tree02ec2b8a301becbea7660ff8dfbadc528737e75c
parent4f335f0c7f83daa32906e8e40c7ac225efa113de (diff)
configurable permalinks support
A sample config.yaml for a site might contain: ```yaml permalinks: post: /:year/:month/:title/ ``` Then, any article in the `post` section, will have the canonical URL formed via the permalink specification given. Signed-off-by: Noah Campbell <noahcampbell@gmail.com>
-rw-r--r--hugolib/config.go6
-rw-r--r--hugolib/page.go50
-rw-r--r--hugolib/permalinks.go149
-rw-r--r--hugolib/permalinks_test.go75
-rw-r--r--hugolib/site.go18
5 files changed, 277 insertions, 21 deletions
diff --git a/hugolib/config.go b/hugolib/config.go
index b9b5d54bd..9a737d7b7 100644
--- a/hugolib/config.go
+++ b/hugolib/config.go
@@ -34,6 +34,7 @@ type Config struct {
Indexes map[string]string // singular, plural
ProcessFilters map[string][]string
Params map[string]interface{}
+ Permalinks PermalinkOverrides
BuildDrafts, UglyUrls, Verbose bool
}
@@ -70,6 +71,11 @@ func SetupConfig(cfgfile *string, path *string) *Config {
c.Indexes["category"] = "categories"
}
+ // ensure map exists, albeit empty
+ if c.Permalinks == nil {
+ c.Permalinks = make(PermalinkOverrides, 0)
+ }
+
if !strings.HasSuffix(c.BaseUrl, "/") {
c.BaseUrl = c.BaseUrl + "/"
}
diff --git a/hugolib/page.go b/hugolib/page.go
index f0ec4063e..f9af82448 100644
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -251,23 +251,35 @@ func (p *Page) permalink() (*url.URL, error) {
pSlug := strings.TrimSpace(p.Slug)
pUrl := strings.TrimSpace(p.Url)
var permalink string
- if len(pSlug) > 0 {
- if p.Site.Config != nil && p.Site.Config.UglyUrls {
- permalink = path.Join(dir, p.Slug, p.Extension)
- } else {
- permalink = dir + "/" + p.Slug + "/"
+ var err error
+
+ if override, ok := p.Site.Permalinks[p.Section]; ok {
+ permalink, err = override.Expand(p)
+ if err != nil {
+ return nil, err
}
- } else if len(pUrl) > 2 {
- permalink = pUrl
+ //fmt.Printf("have an override for %q in section %s → %s\n", p.Title, p.Section, permalink)
} else {
- _, t := path.Split(p.FileName)
- if p.Site.Config != nil && p.Site.Config.UglyUrls {
- x := replaceExtension(strings.TrimSpace(t), p.Extension)
- permalink = path.Join(dir, x)
+
+ if len(pSlug) > 0 {
+ if p.Site.Config != nil && p.Site.Config.UglyUrls {
+ permalink = path.Join(dir, p.Slug, p.Extension)
+ } else {
+ permalink = dir + "/" + p.Slug + "/"
+ }
+ } else if len(pUrl) > 2 {
+ permalink = pUrl
} else {
- file, _ := fileExt(strings.TrimSpace(t))
- permalink = path.Join(dir, file)
+ _, t := path.Split(p.FileName)
+ if p.Site.Config != nil && p.Site.Config.UglyUrls {
+ x := replaceExtension(strings.TrimSpace(t), p.Extension)
+ permalink = path.Join(dir, x)
+ } else {
+ file, _ := fileExt(strings.TrimSpace(t))
+ permalink = path.Join(dir, file)
+ }
}
+
}
base, err := url.Parse(baseUrl)
@@ -555,6 +567,18 @@ func (p *Page) TargetPath() (outfile string) {
return
}
+ // If there's a Permalink specification, we use that
+ if override, ok := p.Site.Permalinks[p.Section]; ok {
+ var err error
+ outfile, err = override.Expand(p)
+ if err == nil {
+ if strings.HasSuffix(outfile, "/") {
+ outfile += "index.html"
+ }
+ return
+ }
+ }
+
if len(strings.TrimSpace(p.Slug)) > 0 {
outfile = strings.TrimSpace(p.Slug) + "." + p.Extension
} else {
diff --git a/hugolib/permalinks.go b/hugolib/permalinks.go
new file mode 100644
index 000000000..41e797ea3
--- /dev/null
+++ b/hugolib/permalinks.go
@@ -0,0 +1,149 @@
+package hugolib
+
+import (
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+
+ helper "github.com/spf13/hugo/template"
+)
+
+// PathPattern represents a string which builds up a URL from attributes
+type PathPattern string
+
+// PageToPermaAttribute is the type of a function which, given a page and a tag
+// can return a string to go in that position in the page (or an error)
+type PageToPermaAttribute func(*Page, string) (string, error)
+
+// PermalinkOverrides maps a section name to a PathPattern
+type PermalinkOverrides map[string]PathPattern
+
+// knownPermalinkAttributes maps :tags in a permalink specification to a
+// function which, given a page and the tag, returns the resulting string
+// to be used to replace that tag.
+var knownPermalinkAttributes map[string]PageToPermaAttribute
+
+// validate determines if a PathPattern is well-formed
+func (pp PathPattern) validate() bool {
+ if pp[0] != '/' {
+ return false
+ }
+ fragments := strings.Split(string(pp[1:]), "/")
+ var bail = false
+ for i := range fragments {
+ if bail {
+ return false
+ }
+ if len(fragments[i]) == 0 {
+ bail = true
+ continue
+ }
+ if !strings.HasPrefix(fragments[i], ":") {
+ continue
+ }
+ k := strings.ToLower(fragments[i][1:])
+ if _, ok := knownPermalinkAttributes[k]; !ok {
+ return false
+ }
+ }
+ return true
+}
+
+type permalinkExpandError struct {
+ pattern PathPattern
+ section string
+ err error
+}
+
+func (pee *permalinkExpandError) Error() string {
+ return fmt.Sprintf("error expanding %q section %q: %s", string(pee.pattern), pee.section, pee.err)
+}
+
+var (
+ errPermalinkIllFormed = errors.New("permalink ill-formed")
+ errPermalinkAttributeUnknown = errors.New("permalink attribute not recognised")
+)
+
+// Expand on a PathPattern takes a Page and returns the fully expanded Permalink
+// or an error explaining the failure.
+func (pp PathPattern) Expand(p *Page) (string, error) {
+ if !pp.validate() {
+ return "", &permalinkExpandError{pattern: pp, section: "<all>", err: errPermalinkIllFormed}
+ }
+ sections := strings.Split(string(pp), "/")
+ for i, field := range sections {
+ if len(field) == 0 || field[0] != ':' {
+ continue
+ }
+ attr := field[1:]
+ callback, ok := knownPermalinkAttributes[attr]
+ if !ok {
+ return "", &permalinkExpandError{pattern: pp, section: strconv.Itoa(i), err: errPermalinkAttributeUnknown}
+ }
+ newField, err := callback(p, attr)
+ if err != nil {
+ return "", &permalinkExpandError{pattern: pp, section: strconv.Itoa(i), err: err}
+ }
+ sections[i] = newField
+ }
+ return strings.Join(sections, "/"), nil
+}
+
+func pageToPermalinkDate(p *Page, dateField string) (string, error) {
+ // a Page contains a Node which provides a field Date, time.Time
+ switch dateField {
+ case "year":
+ return strconv.Itoa(p.Date.Year()), nil
+ case "month":
+ return fmt.Sprintf("%02d", int(p.Date.Month())), nil
+ case "monthname":
+ return p.Date.Month().String(), nil
+ case "day":
+ return fmt.Sprintf("%02d", int(p.Date.Day())), nil
+ case "weekday":
+ return strconv.Itoa(int(p.Date.Weekday())), nil
+ case "weekdayname":
+ return p.Date.Weekday().String(), nil
+ case "yearday":
+ return strconv.Itoa(p.Date.YearDay()), nil
+ }
+ //TODO: support classic strftime escapes too
+ // (and pass those through despite not being in the map)
+ panic("coding error: should not be here")
+}
+
+// pageToPermalinkTitle returns the URL-safe form of the title
+func pageToPermalinkTitle(p *Page, _ string) (string, error) {
+ // Page contains Node which has Title
+ // (also contains UrlPath which has Slug, sometimes)
+ return helper.Urlize(p.Title), nil
+}
+
+// if the page has a slug, return the slug, else return the title
+func pageToPermalinkSlugElseTitle(p *Page, a string) (string, error) {
+ if p.Slug != "" {
+ return p.Slug, nil
+ }
+ return pageToPermalinkTitle(p, a)
+}
+
+func pageToPermalinkSection(p *Page, _ string) (string, error) {
+ // Page contains Node contains UrlPath which has Section
+ return p.Section, nil
+}
+
+func init() {
+ knownPermalinkAttributes = map[string]PageToPermaAttribute{
+ "year": pageToPermalinkDate,
+ "month": pageToPermalinkDate,
+ "monthname": pageToPermalinkDate,
+ "day": pageToPermalinkDate,
+ "weekday": pageToPermalinkDate,
+ "weekdayname": pageToPermalinkDate,
+ "yearday": pageToPermalinkDate,
+ "section": pageToPermalinkSection,
+ "title": pageToPermalinkTitle,
+ "slug": pageToPermalinkSlugElseTitle,
+ }
+}
diff --git a/hugolib/permalinks_test.go b/hugolib/permalinks_test.go
new file mode 100644
index 000000000..019b23c2f
--- /dev/null
+++ b/hugolib/permalinks_test.go
@@ -0,0 +1,75 @@
+package hugolib
+
+import (
+ "strings"
+ "testing"
+)
+
+// testdataPermalinks is used by a couple of tests; the expandsTo content is
+// subject to the data in SIMPLE_PAGE_JSON.
+var testdataPermalinks = []struct {
+ spec string
+ valid bool
+ expandsTo string
+}{
+ {"/:year/:month/:title/", true, "/2012/04/spf13-vim-3.0-release-and-new-website/"},
+ {"/:title", true, "/spf13-vim-3.0-release-and-new-website"},
+ {":title", false, ""},
+ {"/blog/:year/:yearday/:title", true, "/blog/2012/97/spf13-vim-3.0-release-and-new-website"},
+ {":fred", false, ""},
+ {"/blog/:fred", false, ""},
+ {"/:year//:title", false, ""},
+ {
+ "/:section/:year/:month/:day/:weekdayname/:yearday/:title",
+ true,
+ "/blue/2012/04/06/Friday/97/spf13-vim-3.0-release-and-new-website",
+ },
+ {
+ "/:weekday/:weekdayname/:month/:monthname",
+ true,
+ "/5/Friday/04/April",
+ },
+ {
+ "/:slug/:title",
+ true,
+ "/spf13-vim-3-0-release-and-new-website/spf13-vim-3.0-release-and-new-website",
+ },
+}
+
+func TestPermalinkValidation(t *testing.T) {
+ for _, item := range testdataPermalinks {
+ pp := PathPattern(item.spec)
+ have := pp.validate()
+ if have == item.valid {
+ continue
+ }
+ var howBad string
+ if have {
+ howBad = "validates but should not have"
+ } else {
+ howBad = "should have validated but did not"
+ }
+ t.Errorf("permlink spec %q %s", item.spec, howBad)
+ }
+}
+
+func TestPermalinkExpansion(t *testing.T) {
+ page, err := ReadFrom(strings.NewReader(SIMPLE_PAGE_JSON), "blue/test-page.md")
+ if err != nil {
+ t.Fatalf("failed before we began, could not parse SIMPLE_PAGE_JSON: %s", err)
+ }
+ for _, item := range testdataPermalinks {
+ if !item.valid {
+ continue
+ }
+ pp := PathPattern(item.spec)
+ result, err := pp.Expand(page)
+ if err != nil {
+ t.Errorf("failed to expand page: %s", err)
+ continue
+ }
+ if result != item.expandsTo {
+ t.Errorf("expansion mismatch!\n\tExpected: %q\n\tReceived: %q", item.expandsTo, result)
+ }
+ }
+}
diff --git a/hugolib/site.go b/hugolib/site.go
index b1e1113dd..128b23962 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -46,14 +46,14 @@ func MakePermalink(base *url.URL, path *url.URL) *url.URL {
//
// 2. Pages contain sections (based on the file they were generated from),
// aliases and slugs (included in a pages frontmatter) which are the
-// various targets that will get generated. There will be canonical
-// listing.
+// various targets that will get generated. There will be canonical
+// listing. The canonical path can be overruled based on a pattern.
//
// 3. Indexes are created via configuration and will present some aspect of
// the final page and typically a perm url.
//
// 4. All Pages are passed through a template based on their desired
-// layout based on numerous different elements.
+// layout based on numerous different elements.
//
// 5. The entire collection of files is written to disk.
type Site struct {
@@ -80,6 +80,7 @@ type SiteInfo struct {
LastChange time.Time
Title string
Config *Config
+ Permalinks PermalinkOverrides
Params map[string]interface{}
}
@@ -220,11 +221,12 @@ func (s *Site) initialize() (err error) {
func (s *Site) initializeSiteInfo() {
s.Info = SiteInfo{
- BaseUrl: template.URL(s.Config.BaseUrl),
- Title: s.Config.Title,
- Recent: &s.Pages,
- Config: &s.Config,
- Params: s.Config.Params,
+ BaseUrl: template.URL(s.Config.BaseUrl),
+ Title: s.Config.Title,
+ Recent: &s.Pages,
+ Config: &s.Config,
+ Params: s.Config.Params,
+ Permalinks: s.Config.Permalinks,
}
}