Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Go time format strings in permalinks #6489

Merged
merged 1 commit into from
Nov 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/content/en/content-management/urls.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ permalinks:

Only the content under `posts/` will have the new URL structure. For example, the file `content/posts/sample-entry.md` with `date: 2017-02-27T19:20:00-05:00` in its front matter will render to `public/2017/02/sample-entry/index.html` at build time and therefore be reachable at `https://example.com/2017/02/sample-entry/`.

If the standard date-based permalink configuration does not meet your needs, you can also format URL segments using [Go time formatting directives](https://golang.org/pkg/time/#Time.Format). For example, a URL structure with two digit years and month and day digits without zero padding can be accomplished with:

{{< code-toggle file="config" copy="false" >}}
permalinks:
posts: /:06/:1/:2/:title/
{{< /code-toggle >}}

You can also configure permalinks of taxonomies with the same syntax, by using the plural form of the taxonomy instead of the section. You will probably only want to use the configuration values `:slug` or `:title`.

### Permalink Configuration Values
Expand Down Expand Up @@ -80,6 +87,8 @@ The following is a list of values that can be used in a `permalink` definition i
`:filename`
: the content's filename (without extension)

Additionally, a Go time format string prefixed with `:` may be used.

## Aliases

Aliases can be used to create redirects to your page from other URLs.
Expand Down
30 changes: 24 additions & 6 deletions resources/page/permalinks.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"regexp"
"strconv"
"strings"
"time"

"github.com/pkg/errors"

Expand All @@ -38,6 +39,24 @@ type PermalinkExpander struct {
ps *helpers.PathSpec
}

// Time for checking date formats. Every field is different than the
// Go reference time for date formatting. This ensures that formatting this date
// with a Go time format always has a different output than the format itself.
var referenceTime = time.Date(2019, time.November, 9, 23, 1, 42, 1, time.UTC)

// Return the callback for the given permalink attribute and a boolean indicating if the attribute is valid or not.
func (p PermalinkExpander) callback(attr string) (pageToPermaAttribute, bool) {
if callback, ok := p.knownPermalinkAttributes[attr]; ok {
return callback, true
}

if referenceTime.Format(attr) != attr {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smart!

return p.pageToPermalinkDate, true
}

return nil, false
}

// NewPermalinkExpander creates a new PermalinkExpander configured by the given
// PathSpec.
func NewPermalinkExpander(ps *helpers.PathSpec) (PermalinkExpander, error) {
Expand Down Expand Up @@ -109,7 +128,7 @@ func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Pa
replacement := m[0]
attr := replacement[1:]
replacements[i] = replacement
callback, ok := l.knownPermalinkAttributes[attr]
callback, ok := l.callback(attr)

if !ok {
return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkAttributeUnknown}
Expand Down Expand Up @@ -173,8 +192,8 @@ func (l PermalinkExpander) validate(pp string) bool {
}

for _, match := range matches {
k := strings.ToLower(match[0][1:])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lower casing the key turns out to be a bug: If you use :TITLE as the key in the permalink configuration, it will pass this check, but then when trying to expand the permalink, it will fail because TITLE isn't in the map and the code would panic.

I decided to remove this to lower because it makes date formats like Friday not work because friday is not a valid Go time format.

I added a test for the behavior that non-normalized permalink configuration keys aren't valid. It would pass on master as well, due to the bug noted above.

if _, ok := l.knownPermalinkAttributes[k]; !ok {
k := match[0][1:]
if _, ok := l.callback(k); !ok {
return false
}
}
Expand Down Expand Up @@ -214,9 +233,8 @@ func (l PermalinkExpander) pageToPermalinkDate(p Page, dateField string) (string
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")

return p.Date().Format(dateField), nil
}

// pageToPermalinkTitle returns the URL-safe form of the title
Expand Down
15 changes: 10 additions & 5 deletions resources/page/permalinks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,21 @@ var testdataPermalinks = []struct {
{":title", true, "spf13-vim-3.0-release-and-new-website"},
{"/:year-:month-:title", true, "/2012-04-spf13-vim-3.0-release-and-new-website"},
{"/:year/:yearday/:month/:monthname/:day/:weekday/:weekdayname/", true, "/2012/97/04/April/06/5/Friday/"}, // Dates
{"/:section/", true, "/blue/"}, // Section
{"/:title/", true, "/spf13-vim-3.0-release-and-new-website/"}, // Title
{"/:slug/", true, "/the-slug/"}, // Slug
{"/:filename/", true, "/test-page/"}, // Filename
{"/:section/", true, "/blue/"}, // Section
{"/:title/", true, "/spf13-vim-3.0-release-and-new-website/"}, // Title
{"/:slug/", true, "/the-slug/"}, // Slug
{"/:filename/", true, "/test-page/"}, // Filename
{"/:06-:1-:2-:Monday", true, "/12-4-6-Friday"}, // Dates with Go formatting
{"/:2006_01_02_15_04_05.000", true, "/2012_04_06_03_01_59.000"}, // Complicated custom date format
// TODO(moorereason): need test scaffolding for this.
//{"/:sections/", false, "/blue/"}, // Sections

// Failures
{"/blog/:fred", false, ""},
{"/:year//:title", false, ""},
{"/:TITLE", false, ""}, // case is not normalized
{"/:2017", false, ""}, // invalid date format
{"/:2006-01-02", false, ""}, // valid date format but invalid attribute name
}

func TestPermalinkExpansion(t *testing.T) {
Expand All @@ -51,7 +56,7 @@ func TestPermalinkExpansion(t *testing.T) {

page := newTestPageWithFile("/test-page/index.md")
page.title = "Spf13 Vim 3.0 Release and new website"
d, _ := time.Parse("2006-01-02", "2012-04-06")
d, _ := time.Parse("2006-01-02 15:04:05", "2012-04-06 03:01:59")
page.date = d
page.section = "blue"
page.slug = "The Slug"
Expand Down